ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
MigratedLanguageFileSyncPluralRuleChangeTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
27use PHPUnit\Framework\Attributes\DataProvider;
28
36{
37 private const string ONE_FORM = 'nplurals=1; plural=0;';
38 private const string TWO_FORMS = 'nplurals=2; plural=(n != 1);';
39 private const string THREE_FORMS = 'nplurals=3; plural=(n == 1 ? 0 : n >= 2 && n <= 4 ? 1 : 2);';
40
45 #[DataProvider('mappings')]
47 string $previous,
48 string $rule,
49 array $previous_forms,
50 array $expected
51 ): void {
52 $this->assertSame($expected, MigratedLanguageFileSync::mapPluralForms(
53 PluralForms::fromHeader($previous),
54 PluralForms::fromHeader($rule),
55 $previous_forms
56 ));
57 }
58
62 public static function mappings(): array
63 {
64 $four_ish = 'nplurals=3; plural=(n == 1 ? 0 : n <= 4 ? 1 : 2);';
65
66 return [
67 'unambiguous: the new forms 1 and 2 were served by the old form 1' => [
68 self::TWO_FORMS, self::THREE_FORMS, ['E', 'V'], [0 => 0, 1 => 1, 2 => 1],
69 ],
70 'unambiguous: every form of a 1-form language' => [
71 self::ONE_FORM, self::TWO_FORMS, ['L'], [0 => 0, 1 => 0],
72 ],
73 'ambiguous, the old forms carry the same value' => [
74 self::THREE_FORMS, self::TWO_FORMS, ['E', 'X', 'X'], [0 => 0, 1 => 1],
75 ],
76 'ambiguous, the new default form takes the old default form' => [
77 self::THREE_FORMS, self::TWO_FORMS, ['E', 'FEW', 'VIELE'], [0 => 0, 1 => 2],
78 ],
79 'ambiguous, no default form: left out' => [
80 self::THREE_FORMS, $four_ish, ['E', 'FEW', 'VIELE'], [0 => 0, 2 => 2],
81 ],
82 'ambiguous with equal values, no default form: taken over' => [
83 self::THREE_FORMS, $four_ish, ['E', 'X', 'X'], [0 => 0, 1 => 1, 2 => 2],
84 ],
85 'a form no sampled quantity selects: left out' => [
86 self::TWO_FORMS, 'nplurals=3; plural=(n == 1 ? 0 : 1);', ['E', 'V'], [0 => 0, 1 => 1],
87 ],
88 ];
89 }
90
92 {
93 $overlay = self::pluralCatalog(self::THREE_FORMS, ['item' => ['E', 'LOKAL', 'VIELE']]);
94 $shipped = self::pluralCatalog(self::THREE_FORMS, ['item' => ['E', 'F', 'V']]);
95
96 $this->assertSame($overlay, MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE, $not_taken_over));
97 $this->assertSame([], $not_taken_over);
98 }
99
106 {
107 $overlay = self::pluralCatalog(
108 self::THREE_FORMS,
109 ['item' => ['Eintrag', 'LOKAL-FEW', 'LOKAL-VIELE']],
110 ['item' => ['Eintrag', 'Einträge-few', 'Einträge-viele']]
111 );
112 $shipped = self::pluralCatalog(self::TWO_FORMS, ['item' => ['Eintrag', 'Einträge']]);
113
114 $result = MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE, $not_taken_over);
115
116 $this->assertNotSame($overlay, $result);
117 $this->assertSame(self::TWO_FORMS, $result->getHeader('Plural-Forms'));
118 $item = $result->find(self::MODULE, 'item');
119 $this->assertSame(['Eintrag', 'LOKAL-VIELE'], $item?->getPluralTranslations());
120 $this->assertSame('Eintrag', LocalChangeComments::getOriginal($item, 0));
121 $this->assertSame('Einträge-viele', LocalChangeComments::getOriginal($item, 1), 'the original of the form its value came from');
122 $this->assertSame(['item [1]'], $not_taken_over);
123 }
124
130 {
131 $overlay = self::pluralCatalog(self::TWO_FORMS, ['item' => ['Eintrag', 'LOKAL']], ['item' => ['Eintrag', 'Einträge']]);
132 $shipped = self::pluralCatalog('nplurals=3; plural=(n == 1 ? 0 : 1);', ['item' => ['Eintrag', 'Einträge', 'Eintrage-nie']]);
133
134 $result = MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE, $not_taken_over);
135
136 $item = $result->find(self::MODULE, 'item');
137 $this->assertSame(['Eintrag', 'LOKAL', 'Eintrage-nie'], $item?->getPluralTranslations());
138 $this->assertSame('Eintrage-nie', LocalChangeComments::getOriginal($item, 2));
139 $this->assertSame([], $not_taken_over);
140 }
141
143 {
144 $overlay = self::pluralCatalog(self::THREE_FORMS, ['item' => ['E', 'F', 'V']]);
145 $singular = new TranslationEntry(self::MODULE, 'plain');
146 $singular->translate('LOKAL-PLAIN');
147 $overlay->add($singular);
148 $foreign = new TranslationEntry('other', 'item');
149 $foreign->setPlural('item_plural', ['A', 'B', 'C']);
150 $overlay->add($foreign);
151 $before = $overlay->toPoString();
152 $shipped = self::pluralCatalog(self::TWO_FORMS, ['item' => ['E', 'V']]);
153
154 $result = MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE);
155
156 $this->assertSame($before, $overlay->toPoString(), 'the overlay passed in is not changed');
157 $this->assertSame('LOKAL-PLAIN', $result->find(self::MODULE, 'plain')?->getTranslation());
158 $this->assertSame(['A', 'B', 'C'], $result->find('other', 'item')?->getPluralTranslations(), 'only the module is rewritten');
159 }
160
167 {
168 $overlay = self::pluralCatalog(self::TWO_FORMS, ['item' => ['LOKAL-EIN', 'Éléments']], ['item' => ['Élément', 'Éléments']]);
169 $shipped = self::pluralCatalog(
170 'nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;',
171 ['item' => ['Élément-fr', 'Éléments-million', 'Éléments-fr']]
172 );
173
174 $result = MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE, $not_taken_over);
175
176 $item = $result->find(self::MODULE, 'item');
177 $this->assertSame(['Élément-fr', 'Éléments', 'Éléments'], $item?->getPluralTranslations());
178 $this->assertSame('Élément-fr', LocalChangeComments::getOriginal($item, 0), 'no local change');
179 $this->assertSame(['item [0]'], $not_taken_over);
180 }
181
187 {
188 $overlay = self::pluralCatalog(
189 self::THREE_FORMS,
190 ['item' => ['E', 'FEW', 'VIELE']],
191 ['item' => ['E', 'F', 'V']]
192 );
193 $shipped = self::pluralCatalog('nplurals=3; plural=(n == 1 ? 0 : n <= 4 ? 1 : 2);', []);
194 $singular = new TranslationEntry(self::MODULE, 'item');
195 $singular->translate('Singular');
196 $shipped->add($singular);
197
198 $result = MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE, $not_taken_over);
199
200 $item = $result->find(self::MODULE, 'item');
201 $this->assertSame(['E', 'VIELE', 'VIELE'], $item?->getPluralTranslations());
202 $this->assertNull(LocalChangeComments::getOriginal($item, 1));
203 $this->assertSame('V', LocalChangeComments::getOriginal($item, 2));
204 $this->assertSame(['item [1]'], $not_taken_over, 'the value of the form without a meaning is reported');
205 }
206
212 {
213 $overlay = self::pluralCatalog('nplurals=2; plural=n != 1;', ['item' => ['E', 'LOKAL']]);
214 $shipped = self::pluralCatalog('nplurals=2; plural=( n != 1 );', ['item' => ['E', 'V']]);
215
216 $this->assertSame($overlay, MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE, $not_taken_over));
217 $this->assertSame([], $not_taken_over);
218 }
219
225 {
226 $this->shipPlural(['item' => ['Eintrag', 'Einträge-few', 'Einträge-viele']], self::THREE_FORMS);
227 $this->syncLocal(['item [0]' => 'Eintrag', 'item [1]' => 'LOKAL-FEW', 'item [2]' => 'LOKAL-VIELE']);
228 $this->shipPlural(['item' => ['Eintrag', 'Einträge']], self::TWO_FORMS);
229
230 $result = MigratedLanguageFileSync::loadModuleTranslations($this->manager, 'de', self::MODULE, $this->client_data_dir);
231
232 $this->assertSame(['item [0]', 'item [1]'], array_keys($result));
233 $this->assertSame('Eintrag', $result['item [0]']['value']);
234 $this->assertFalse($result['item [0]']['local_change']);
235 $this->assertSame('LOKAL-VIELE', $result['item [1]']['value']);
236 $this->assertTrue($result['item [1]']['local_change']);
237 }
238
240 {
241 $this->shipPlural(['item' => ['Eintrag']], self::ONE_FORM);
242 $this->syncLocal(['item [0]' => 'LOKAL']);
243 $this->shipPlural(['item' => ['Eintrag', 'Einträge']], self::TWO_FORMS);
244
245 $result = MigratedLanguageFileSync::loadLocalChanges($this->manager, 'de', self::MODULE, $this->client_data_dir);
246
247 $this->assertSame(['item [0]', 'item [1]'], array_keys($result));
248 $this->assertSame('LOKAL', $result['item [0]']['value']);
249 $this->assertSame('LOKAL', $result['item [1]']['value']);
250 }
251
258 {
259 $this->expectErrorLog();
260 $this->shipPlural(['item' => ['Eintrag', 'Einträge-few', 'Einträge-viele']], self::THREE_FORMS);
261 $this->syncLocal(['item [0]' => 'Eintrag', 'item [1]' => 'Einträge-few', 'item [2]' => 'LOKAL-VIELE']);
262 file_put_contents($this->shippedPo(), "msgid \"kaputt\n");
263
264 $result = MigratedLanguageFileSync::loadLocalChanges($this->manager, 'de', self::MODULE, $this->client_data_dir);
265
266 $this->assertSame(['item [0]', 'item [1]', 'item [2]'], array_keys($result), 'the forms of the overlay rule');
267 $this->assertSame('LOKAL-VIELE', $result['item [2]']['value']);
268 $this->assertStringContainsString('cannot be read', $this->errorLog());
269 }
270
277 {
278 $this->shipPlural(['item' => ['Eintrag', 'Einträge'], 'plain' => 'X'], self::TWO_FORMS);
279 $this->syncLocal(['plain' => 'LOKAL-PLAIN']);
280 file_put_contents($this->shippedPo(), "msgid \"kaputt\n");
281
282 $result = MigratedLanguageFileSync::loadLocalChanges($this->manager, 'de', self::MODULE, $this->client_data_dir);
283
284 $this->assertSame('LOKAL-PLAIN', $result['plain']['value']);
285 }
286
292 {
293 $this->shipPlural(['item' => ['Eintrag', 'Einträge']], self::TWO_FORMS);
294
295 $this->syncLocal(['item [0]' => 'Eintrag', 'item [1]' => 'LOKAL', 'item [2]' => 'GEISTER-FORM']);
296
297 $overlay = $this->overlayPo();
298 $this->assertSame(['item'], $this->overlayIdentifiers());
299 $this->assertSame(['Eintrag', 'LOKAL'], $overlay->find(null, 'item')?->getPluralTranslations());
300 $this->assertStringNotContainsString('GEISTER-FORM', (string) file_get_contents($this->overlayBase() . '.po'));
301 }
302
309 {
310 $this->shipPlural(['item' => ['Element', 'Elemente-few', 'Elemente-viele']], self::THREE_FORMS);
311 $this->syncLocal(['item [0]' => 'Element', 'item [1]' => 'LOKAL-FEW', 'item [2]' => 'LOKAL-VIELE']);
312 $this->shipPlural(['item' => ['Element', 'Elemente']], self::TWO_FORMS);
313 // below the ILIAS directory like the shipped `.po` (removed with the fixture directory)
314 mkdir($this->fixture_directory . '/backup');
315
316 $result = ShippedPoMerger::merge(
317 $this->manager,
318 $this->ilias_root,
319 'de',
320 $this->client_data_dir,
321 $this->fixture_directory . '/backup'
322 );
323
324 $this->assertSame([], $result['skipped']);
325 $this->assertSame([], $result['not_merged']);
326 $this->assertSame([self::MODULE => $this->shippedPo()], $result['written']);
327 $merged = TranslationCatalog::fromPoFile($this->shippedPo())->find(self::MODULE, 'item');
328 $this->assertSame(['Element', 'LOKAL-VIELE'], $merged?->getPluralTranslations());
329 }
330
335 private static function pluralCatalog(string $header, array $entries, array $originals = []): TranslationCatalog
336 {
337 $catalog = new TranslationCatalog();
338 $catalog->setHeader('Content-Type', 'text/plain; charset=UTF-8');
339 $catalog->setHeader('Plural-Forms', $header);
340 foreach ($entries as $identifier => $forms) {
341 $entry = new TranslationEntry(self::MODULE, $identifier);
342 $entry->setPlural($identifier . '_plural', $forms);
343 foreach ($originals[$identifier] ?? [] as $form => $original) {
344 LocalChangeComments::setOriginal($entry, $original, $form);
345 }
346 $catalog->add($entry);
347 }
348
349 return $catalog;
350 }
351
355 private function shipPlural(array $entries, string $header): void
356 {
357 $catalog = new TranslationCatalog();
358 $catalog->setHeader('Content-Type', 'text/plain; charset=UTF-8');
359 $catalog->setHeader('Plural-Forms', $header);
360 foreach ($entries as $identifier => $value) {
361 $entry = new TranslationEntry(self::MODULE, (string) $identifier);
362 if (is_array($value)) {
363 $entry->setPlural($identifier . '_plural', $value);
364 } else {
365 $entry->translate($value);
366 }
367 $catalog->add($entry);
368 }
369 MigratedPoFixture::writePo($this->shippedPo(), $catalog);
370 }
371
375 private function syncLocal(array $entries): void
376 {
377 $this->sync($entries);
378 }
379
383 private function errorLog(): string
384 {
385 $file = (string) ini_get('error_log');
386
387 return $file !== '' && is_file($file) ? (string) file_get_contents($file) : '';
388 }
389}
An in-memory gettext catalog (header block plus messages) and the only place of the component that re...
One message of a TranslationCatalog (a msgctxt/msgid/msgstr block of a .po file).
Tracks per overlay entry whether the current value still matches the value of the shipped ....
Reads the shipped .po of a migrated module and maintains its per-installation overlay: the ....
The plural rule of a language, as declared by the "Plural-Forms" header of a gettext catalog (nplural...
Definition: PluralForms.php:42
The "merge" maintenance action of developer mode (LANGMODE) for PO-maintained modules,...
A changed plural rule of a language (number of forms or formula, e.g.
errorLog()
What error_log() wrote during this test so far (PHPUnit redirects it to a file per test).
testLoadLocalChangesDoesNotParseTheShippedPoForAnOverlayWithoutAPluralMessageOfTheModule()
Without a plural message the shipped .po is not parsed (see MigratedLanguageFileSyncReadersTest::test...
testWithPluralRuleOfGivesAFormWithoutAPreviousFormTheShippedValueAndNoLocalChange()
A form no previous form maps to is no local change: it has the shipped value as value and as "origina...
testWithPluralRuleOfGivesAMessageThatIsNoLongerShippedAsPluralTheValueOfThePreviousDefaultForm()
A message that is not shipped as a plural one any more has no shipped forms to fall back to: a form w...
testWithPluralRuleOfReplacesAnAmbiguousNonDefaultFormByTheShippedValueAndReportsTheLostLocalValue()
de -> fr: the new form 0 (n = 0 and 1) covers the two old forms; they differ (one is a local change),...
testAFormKeyBeyondTheNumberOfFormsOfTheRuleIsDiscarded()
A form key of an import that the rule of the shipped .po does not have ("item [2]" with 2 forms) is d...
testWithPluralRuleOfKeepsTheLocalDefaultFormWhenTheNumberOfFormsShrinks()
3 -> 2 forms: the local default form is the one the new default form gets (it is the catch-all form o...
testMergeOfAnOverlayOfTheOldRuleWritesTheFormsByMeaning()
The merge into the shipped .po of an overlay written under another rule writes the forms by meaning (...
testWithPluralRuleOfDoesNotRewriteAnOverlayWhoseHeaderOnlyDiffersInSpelling()
A header that differs in white space or parentheses only selects the same forms: the overlay is not r...
static pluralCatalog(string $header, array $entries, array $originals=[])
testLoadLocalChangesOfAPluralOverlayFallsBackToTheOverlaysRuleIfTheShippedPoCannotBeRead()
The shipped .po is needed for the forms of an overlay with a plural message: if it cannot be read,...
testMapPluralFormsGivesAFormThePreviousFormWithItsMeaning(string $previous, string $rule, array $previous_forms, array $expected)
testLoadModuleTranslationsServesAnOverlayOfTheOldRuleWithTheFormsOfTheNewRule()
The overlay is written under 3 forms, the shipped .po has 2 (update before the next reconciling write...
Fixtures of the MigratedLanguageFileSync tests.
static writePo(string $file, TranslationCatalog $catalog)