I18nData.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. <?php
  2. declare(strict_types=1);
  3. class I18nData {
  4. public const REFERENCE_LANGUAGE = 'en';
  5. /** @param array<string,array<string,array<string,I18nValue>>> $data */
  6. public function __construct(private array $data) {
  7. $this->addMissingKeysFromReference();
  8. $this->removeExtraKeysFromOtherLanguages();
  9. $this->processValueStates();
  10. }
  11. /**
  12. * @return array<string,array<string,array<string,I18nValue>>>
  13. */
  14. public function getData(): array {
  15. return $this->data;
  16. }
  17. private function addMissingKeysFromReference(): void {
  18. $reference = $this->getReferenceLanguage();
  19. $languages = $this->getNonReferenceLanguages();
  20. foreach ($reference as $file => $refValues) {
  21. foreach ($refValues as $key => $refValue) {
  22. foreach ($languages as $language) {
  23. if (!array_key_exists($file, $this->data[$language]) || !array_key_exists($key, $this->data[$language][$file])) {
  24. $this->data[$language][$file][$key] = clone $refValue;
  25. }
  26. $value = $this->data[$language][$file][$key];
  27. if ($refValue->equal($value) && !$value->isIgnore()) {
  28. $value->markAsTodo();
  29. }
  30. }
  31. }
  32. }
  33. }
  34. private function removeExtraKeysFromOtherLanguages(): void {
  35. $reference = $this->getReferenceLanguage();
  36. foreach ($this->getNonReferenceLanguages() as $language) {
  37. foreach ($this->getLanguage($language) as $file => $values) {
  38. foreach ($values as $key => $value) {
  39. if (!array_key_exists($key, $reference[$file])) {
  40. unset($this->data[$language][$file][$key]);
  41. }
  42. }
  43. }
  44. }
  45. }
  46. private function processValueStates(): void {
  47. $reference = $this->getReferenceLanguage();
  48. $languages = $this->getNonReferenceLanguages();
  49. foreach ($reference as $file => $refValues) {
  50. foreach ($refValues as $key => $refValue) {
  51. foreach ($languages as $language) {
  52. $value = $this->data[$language][$file][$key];
  53. if ($refValue->equal($value) && !$value->isIgnore()) {
  54. $value->markAsTodo();
  55. continue;
  56. }
  57. if (!$refValue->equal($value) && $value->isTodo()) {
  58. $value->markAsDirty();
  59. continue;
  60. }
  61. }
  62. }
  63. }
  64. }
  65. /**
  66. * Return the available languages
  67. * @return array<string>
  68. */
  69. public function getAvailableLanguages(): array {
  70. $languages = array_keys($this->data);
  71. sort($languages);
  72. return $languages;
  73. }
  74. /**
  75. * Return all available languages without the reference language
  76. * @return array<string>
  77. */
  78. private function getNonReferenceLanguages(): array {
  79. return array_filter(array_keys($this->data),
  80. static fn(string $value) => static::REFERENCE_LANGUAGE !== $value);
  81. }
  82. /**
  83. * Add a new language. It’s a copy of the reference language.
  84. * @throws Exception
  85. */
  86. public function addLanguage(string $language, ?string $reference = null): void {
  87. if (array_key_exists($language, $this->data)) {
  88. throw new Exception('The selected language already exist.');
  89. }
  90. if (!is_string($reference) || !array_key_exists($reference, $this->data)) {
  91. $reference = static::REFERENCE_LANGUAGE;
  92. }
  93. $this->data[$language] = $this->data[$reference];
  94. }
  95. /**
  96. * Check if the key is known.
  97. */
  98. public function isKnown(string $key): bool {
  99. return array_key_exists($this->getFilenamePrefix($key), $this->data[static::REFERENCE_LANGUAGE]) &&
  100. array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)]);
  101. }
  102. /**
  103. * Return the parent key for a specified key.
  104. * To get the parent key, you need to remove the last section of the key. Each
  105. * is separated into sections. The parent of a section is the concatenation of
  106. * all sections before the selected key. For instance, if the key is 'a.b.c.d.e',
  107. * the parent key is 'a.b.c.d'.
  108. */
  109. private function getParentKey(string $key): string {
  110. return substr($key, 0, strrpos($key, '.') ?: null);
  111. }
  112. /**
  113. * Return the siblings for a specified key.
  114. * To get the siblings, we need to find all matches with the parent.
  115. *
  116. * @return array<string>
  117. */
  118. private function getSiblings(string $key): array {
  119. if (!array_key_exists($this->getFilenamePrefix($key), $this->data[static::REFERENCE_LANGUAGE])) {
  120. return [];
  121. }
  122. $keys = array_keys($this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)]);
  123. $parent = $this->getParentKey($key);
  124. return array_values(array_filter($keys, static fn(string $element) => str_contains($element, $parent)));
  125. }
  126. /**
  127. * Check if the key is an only child.
  128. * To be an only child, there must be only one sibling and that sibling must
  129. * be the empty sibling. The empty sibling is the parent.
  130. */
  131. private function isOnlyChild(string $key): bool {
  132. $siblings = $this->getSiblings($key);
  133. if (1 !== count($siblings)) {
  134. return false;
  135. }
  136. return '_' === $siblings[0][-1];
  137. }
  138. /**
  139. * Return the parent key as an empty sibling.
  140. * When a key has children, it cannot have its value directly. The value
  141. * needs to be attached to an empty sibling represented by "_".
  142. */
  143. private function getEmptySibling(string $key): string {
  144. return "{$key}._";
  145. }
  146. /**
  147. * Check if a key is a parent key.
  148. * To be a parent key, there must be at least one key starting with the key
  149. * under test. Of course, it cannot be itself.
  150. */
  151. private function isParent(string $key): bool {
  152. if (!array_key_exists($this->getFilenamePrefix($key), $this->data[static::REFERENCE_LANGUAGE])) {
  153. return false;
  154. }
  155. $keys = array_keys($this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)]);
  156. $children = array_values(array_filter($keys, static function (string $element) use ($key) {
  157. if ($element === $key) {
  158. return false;
  159. }
  160. return str_contains($element, $key);
  161. }));
  162. return count($children) !== 0;
  163. }
  164. /**
  165. * Add a new key to all languages.
  166. * @throws Exception
  167. */
  168. public function addKey(string $key, string $value): void {
  169. if ($this->isParent($key)) {
  170. $key = $this->getEmptySibling($key);
  171. }
  172. if ($this->isKnown($key)) {
  173. throw new Exception('The selected key already exist.');
  174. }
  175. $parentKey = $this->getParentKey($key);
  176. if ($this->isKnown($parentKey)) {
  177. // The parent key exists, that means that we need to convert it to an array.
  178. // To create an array, we need to change the key by appending an empty section.
  179. foreach ($this->getAvailableLanguages() as $language) {
  180. $parentValue = $this->data[$language][$this->getFilenamePrefix($parentKey)][$parentKey];
  181. $this->data[$language][$this->getFilenamePrefix($this->getEmptySibling($parentKey))][$this->getEmptySibling($parentKey)] =
  182. new I18nValue($parentValue);
  183. }
  184. }
  185. $value = new I18nValue($value);
  186. $value->markAsTodo();
  187. foreach ($this->getAvailableLanguages() as $language) {
  188. if (!array_key_exists($key, $this->data[$language][$this->getFilenamePrefix($key)])) {
  189. $this->data[$language][$this->getFilenamePrefix($key)][$key] = $value;
  190. }
  191. }
  192. if ($this->isKnown($parentKey)) {
  193. $this->removeKey($parentKey);
  194. }
  195. }
  196. /**
  197. * Add a value for a key for the selected language.
  198. *
  199. * @throws Exception
  200. */
  201. public function addValue(string $key, string $value, string $language): void {
  202. if (!in_array($language, $this->getAvailableLanguages(), true)) {
  203. throw new Exception('The selected language does not exist.');
  204. }
  205. if (!array_key_exists($this->getFilenamePrefix($key), $this->data[static::REFERENCE_LANGUAGE]) ||
  206. !array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
  207. throw new Exception('The selected key does not exist for the selected language.');
  208. }
  209. $value = new I18nValue($value);
  210. if (static::REFERENCE_LANGUAGE === $language) {
  211. $previousValue = $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)][$key];
  212. foreach ($this->getAvailableLanguages() as $lang) {
  213. $currentValue = $this->data[$lang][$this->getFilenamePrefix($key)][$key];
  214. if ($currentValue->equal($previousValue)) {
  215. $this->data[$lang][$this->getFilenamePrefix($key)][$key] = $value;
  216. }
  217. }
  218. } else {
  219. $this->data[$language][$this->getFilenamePrefix($key)][$key] = $value;
  220. }
  221. }
  222. /**
  223. * Remove a key in all languages
  224. */
  225. public function removeKey(string $key): void {
  226. if (!$this->isKnown($key) && !$this->isKnown($this->getEmptySibling($key))) {
  227. throw new Exception('The selected key does not exist.');
  228. }
  229. if (!$this->isKnown($key)) {
  230. // The key has children, it needs to be appended with an empty section.
  231. $key = $this->getEmptySibling($key);
  232. }
  233. foreach ($this->getAvailableLanguages() as $language) {
  234. if (array_key_exists($key, $this->data[$language][$this->getFilenamePrefix($key)])) {
  235. unset($this->data[$language][$this->getFilenamePrefix($key)][$key]);
  236. }
  237. }
  238. if ($this->isOnlyChild($key)) {
  239. $parentKey = $this->getParentKey($key);
  240. foreach ($this->getAvailableLanguages() as $language) {
  241. $parentValue = $this->data[$language][$this->getFilenamePrefix($this->getEmptySibling($parentKey))][$this->getEmptySibling($parentKey)];
  242. $this->data[$language][$this->getFilenamePrefix($parentKey)][$parentKey] = $parentValue;
  243. }
  244. $this->removeKey($this->getEmptySibling($parentKey));
  245. }
  246. }
  247. /**
  248. * Ignore a key from a language, or revert an existing ignore on a key.
  249. */
  250. public function ignore(string $key, string $language, bool $revert = false): void {
  251. $value = $this->data[$language][$this->getFilenamePrefix($key)][$key];
  252. if ($revert) {
  253. $value->unmarkAsIgnore();
  254. } else {
  255. $value->markAsIgnore();
  256. }
  257. }
  258. /**
  259. * Ignore all unmodified keys from a language, or revert all existing ignores on unmodified keys.
  260. */
  261. public function ignore_unmodified(string $language, bool $revert = false): void {
  262. $my_language = $this->getLanguage($language);
  263. foreach ($this->getReferenceLanguage() as $file => $ref_language) {
  264. foreach ($ref_language as $key => $ref_value) {
  265. if (array_key_exists($key, $my_language[$file])) {
  266. if ($ref_value->equal($my_language[$file][$key])) {
  267. $this->ignore($key, $language, $revert);
  268. }
  269. }
  270. }
  271. }
  272. }
  273. /**
  274. * @return array<string,array<string,I18nValue>>
  275. */
  276. public function getLanguage(string $language): array {
  277. return $this->data[$language];
  278. }
  279. /**
  280. * @return array<string,array<string,I18nValue>>
  281. */
  282. public function getReferenceLanguage(): array {
  283. return $this->getLanguage(static::REFERENCE_LANGUAGE);
  284. }
  285. private function getFilenamePrefix(string $key): string {
  286. return preg_replace('/\..*/', '.php', $key) ?? '';
  287. }
  288. }