19declare(strict_types=1);
27use PHPUnit\Framework\Attributes\DataProvider;
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);';
45 #[DataProvider('mappings')]
49 array $previous_forms,
64 $four_ish =
'nplurals=3; plural=(n == 1 ? 0 : n <= 4 ? 1 : 2);';
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],
70 'unambiguous: every form of a 1-form language' => [
71 self::ONE_FORM, self::TWO_FORMS, [
'L'], [0 => 0, 1 => 0],
73 'ambiguous, the old forms carry the same value' => [
74 self::THREE_FORMS, self::TWO_FORMS, [
'E',
'X',
'X'], [0 => 0, 1 => 1],
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],
79 'ambiguous, no default form: left out' => [
80 self::THREE_FORMS, $four_ish, [
'E',
'FEW',
'VIELE'], [0 => 0, 2 => 2],
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],
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],
93 $overlay = self::pluralCatalog(self::THREE_FORMS, [
'item' => [
'E',
'LOKAL',
'VIELE']]);
94 $shipped = self::pluralCatalog(self::THREE_FORMS, [
'item' => [
'E',
'F',
'V']]);
96 $this->assertSame($overlay, MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE, $not_taken_over));
97 $this->assertSame([], $not_taken_over);
107 $overlay = self::pluralCatalog(
109 [
'item' => [
'Eintrag',
'LOKAL-FEW',
'LOKAL-VIELE']],
110 [
'item' => [
'Eintrag',
'Einträge-few',
'Einträge-viele']]
112 $shipped = self::pluralCatalog(self::TWO_FORMS, [
'item' => [
'Eintrag',
'Einträge']]);
114 $result = MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE, $not_taken_over);
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);
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']]);
134 $result = MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE, $not_taken_over);
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);
144 $overlay = self::pluralCatalog(self::THREE_FORMS, [
'item' => [
'E',
'F',
'V']]);
146 $singular->translate(
'LOKAL-PLAIN');
147 $overlay->add($singular);
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']]);
154 $result = MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE);
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');
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']]
174 $result = MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE, $not_taken_over);
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);
188 $overlay = self::pluralCatalog(
190 [
'item' => [
'E',
'FEW',
'VIELE']],
191 [
'item' => [
'E',
'F',
'V']]
193 $shipped = self::pluralCatalog(
'nplurals=3; plural=(n == 1 ? 0 : n <= 4 ? 1 : 2);', []);
195 $singular->translate(
'Singular');
196 $shipped->add($singular);
198 $result = MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE, $not_taken_over);
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');
213 $overlay = self::pluralCatalog(
'nplurals=2; plural=n != 1;', [
'item' => [
'E',
'LOKAL']]);
214 $shipped = self::pluralCatalog(
'nplurals=2; plural=( n != 1 );', [
'item' => [
'E',
'V']]);
216 $this->assertSame($overlay, MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped, self::MODULE, $not_taken_over));
217 $this->assertSame([], $not_taken_over);
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);
230 $result = MigratedLanguageFileSync::loadModuleTranslations($this->manager,
'de', self::MODULE, $this->client_data_dir);
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']);
241 $this->shipPlural([
'item' => [
'Eintrag']], self::ONE_FORM);
242 $this->syncLocal([
'item [0]' =>
'LOKAL']);
243 $this->shipPlural([
'item' => [
'Eintrag',
'Einträge']], self::TWO_FORMS);
245 $result = MigratedLanguageFileSync::loadLocalChanges($this->manager,
'de', self::MODULE, $this->client_data_dir);
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']);
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");
264 $result = MigratedLanguageFileSync::loadLocalChanges($this->manager,
'de', self::MODULE, $this->client_data_dir);
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());
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");
282 $result = MigratedLanguageFileSync::loadLocalChanges($this->manager,
'de', self::MODULE, $this->client_data_dir);
284 $this->assertSame(
'LOKAL-PLAIN', $result[
'plain'][
'value']);
293 $this->shipPlural([
'item' => [
'Eintrag',
'Einträge']], self::TWO_FORMS);
295 $this->syncLocal([
'item [0]' =>
'Eintrag',
'item [1]' =>
'LOKAL',
'item [2]' =>
'GEISTER-FORM']);
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'));
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);
314 mkdir($this->fixture_directory .
'/backup');
316 $result = ShippedPoMerger::merge(
320 $this->client_data_dir,
321 $this->fixture_directory .
'/backup'
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());
338 $catalog->setHeader(
'Content-Type',
'text/plain; charset=UTF-8');
339 $catalog->setHeader(
'Plural-Forms', $header);
340 foreach ($entries as $identifier => $forms) {
342 $entry->setPlural($identifier .
'_plural', $forms);
343 foreach ($originals[$identifier] ?? [] as $form => $original) {
344 LocalChangeComments::setOriginal($entry, $original, $form);
346 $catalog->add($entry);
355 private function shipPlural(array $entries,
string $header): void
358 $catalog->setHeader(
'Content-Type',
'text/plain; charset=UTF-8');
359 $catalog->setHeader(
'Plural-Forms', $header);
360 foreach ($entries as $identifier => $value) {
362 if (is_array($value)) {
363 $entry->setPlural($identifier .
'_plural', $value);
365 $entry->translate($value);
367 $catalog->add($entry);
377 $this->sync($entries);
385 $file = (string) ini_get(
'error_log');
387 return $file !==
'' && is_file($file) ? (string) file_get_contents($file) :
'';
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).
Reads the shipped .po of a migrated module and maintains its per-installation overlay: the ....
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...
shipPlural(array $entries, string $header)
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),...
testWithPluralRuleOfReturnsTheSameObjectIfTheRuleIsTheSame()
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...
testWithPluralRuleOfLeavesTheInputUnchangedAndKeepsSingularEntriesAndOtherModules()
syncLocal(array $entries)
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)
testLoadLocalChangesServesAnOverlayOfTheOldRuleWithTheFormsOfTheShippedRule()
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)