19declare(strict_types=1);
28use PHPUnit\Framework\Attributes\DataProvider;
29use PHPUnit\Framework\TestCase;
40 #[DataProvider('refreshFlags')]
43 $this->
seedShipped([
'greeting' =>
'Hallo',
'farewell' =>
'Tschüss']);
44 $shipped_before = file_get_contents($this->
shippedPo());
46 $this->
sync([
'greeting' =>
'Hallo',
'farewell' =>
'Tschüss'], $refresh);
48 $this->assertFileDoesNotExist($this->
overlayBase() .
'.po');
51 $this->assertDirectoryDoesNotExist(dirname($this->
overlayBase()));
52 $this->assertSame($shipped_before, file_get_contents($this->
shippedPo()),
'the shipped .po is never written');
57 return [
'admin edit (refresh = false)' => [
false],
'reconciling write (refresh = true)' => [
true]];
64 $this->
sync([
'greeting' =>
'Servus']);
66 $greeting = $this->
overlayPo()->find(
null,
'greeting');
67 $this->assertSame(
'Servus', $greeting->getTranslation());
68 $this->assertSame(
'Hallo', LocalChangeComments::getOriginal($greeting));
69 $this->assertMatchesRegularExpression(
70 '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/',
71 (
string) LocalChangeComments::getLocalChange($greeting)
79 $this->
sync([
'greeting' =>
'Hallo',
'custom' =>
'Eigener Wert']);
81 $custom = $this->
overlayPo()->find(
null,
'custom');
82 $this->assertSame(
'Eigener Wert', $custom->getTranslation());
83 $this->assertNull(LocalChangeComments::getOriginal($custom));
84 $this->assertNotNull(LocalChangeComments::getLocalChange($custom));
95 'unchanged' => [
'value' =>
'Hello',
'fuzzy' =>
true],
96 'changed' => [
'value' =>
'Bye',
'fuzzy' =>
true],
97 'reviewed' => [
'value' =>
'Hallo'],
100 $this->
sync([
'unchanged' =>
'Hello',
'changed' =>
'Tschüss',
'reviewed' =>
'Hallo']);
105 $this->assertNull($overlay->find(
null,
'unchanged'));
106 $this->assertFalse($overlay->find(
null,
'changed')->hasFlag(
'fuzzy'));
107 $this->assertNull($overlay->find(
null,
'reviewed'));
117 $this->
seedShipped([
'greeting' =>
'<script>alert(1)</script>Hallo']);
119 $this->
sync([
'greeting' =>
'<script>alert(1)</script>Hallo']);
121 $this->assertFileDoesNotExist($this->
overlayBase() .
'.po');
131 $catalog->find(self::MODULE,
'greeting')->addExtractedComment(
'shown on the login page');
134 $this->
sync([
'greeting' =>
'Servus']);
136 $this->assertSame([
'shown on the login page'], $this->
overlayPo()->find(
null,
'greeting')->getExtractedComments());
142 $catalog->setHeader(
'Language',
'de');
143 $catalog->setHeader(
'Plural-Forms',
'nplurals=2; plural=(n != 1);');
147 $this->
sync([
'greeting' =>
'Servus']);
149 $this->assertSame(
'de', $this->
overlayPo()->getHeader(
'Language'));
150 $this->assertSame(
'nplurals=2; plural=(n != 1);', $this->
overlayPo()->getHeader(
'Plural-Forms'));
160 $this->
seedOverlay([
'greeting' => [
'value' =>
'Hallo',
'original' =>
'Hallo']]);
162 $this->
sync([
'greeting' =>
'Servus']);
164 $greeting = $this->
overlayPo()->find(
null,
'greeting');
165 $this->assertSame(
'Servus', $greeting->getTranslation());
166 $this->assertSame(
'Hallo', LocalChangeComments::getOriginal($greeting));
167 $this->assertNotNull(LocalChangeComments::getLocalChange($greeting));
173 'kept' => [
'value' =>
'Hello',
'fuzzy' =>
true],
174 'edited' => [
'value' =>
'Bye',
'fuzzy' =>
true],
177 'kept' => [
'value' =>
'Hello',
'original' =>
'Hello',
'fuzzy' =>
true],
178 'edited' => [
'value' =>
'Bye',
'original' =>
'Bye',
'fuzzy' =>
true],
181 $this->
sync([
'kept' =>
'Hello',
'edited' =>
'Tschüss']);
185 $this->assertNull($overlay->find(
null,
'kept'));
186 $this->assertFalse($overlay->find(
null,
'edited')->hasFlag(
'fuzzy'));
193 'greeting' => [
'value' =>
'Servus',
'original' =>
'Hallo',
'local_change' =>
'2020-01-01T00:00:00Z'],
196 $this->
sync([
'greeting' =>
'Hallo']);
199 $this->assertFileDoesNotExist($this->
overlayBase() .
'.po');
201 $this->assertFalse(MigratedLanguageFileSync::loadModuleTranslations(
205 $this->client_data_dir
206 )[
'greeting'][
'local_change']);
215 $this->
seedShipped([
'greeting' =>
'Hallo',
'farewell' =>
'Tschüss']);
217 'greeting' => [
'value' =>
'Servus',
'original' =>
'Hallo',
'local_change' =>
'2020-01-01T00:00:00Z'],
218 'farewell' => [
'value' =>
'Tschüss',
'original' =>
'Tschüss'],
221 $this->
sync([
'greeting' =>
'Servus',
'farewell' =>
'Pfiat di']);
224 $this->assertSame(
'2020-01-01T00:00:00Z', LocalChangeComments::getLocalChange($overlay->find(
null,
'greeting')));
225 $farewell_change = LocalChangeComments::getLocalChange($overlay->find(
null,
'farewell'));
226 $this->assertNotNull($farewell_change);
227 $this->assertNotSame(
'2020-01-01T00:00:00Z', $farewell_change);
235 #[DataProvider('refreshFlags')]
240 'greeting' => [
'value' =>
'Servus',
'original' =>
'Hallo',
'local_change' =>
'2020-01-01T00:00:00Z'],
242 $legacy_overlay->find(self::MODULE,
'greeting')->addExtractedComment(
'shown on the login page');
244 $this->assertNotNull($this->
overlayPo()->find(self::MODULE,
'greeting'),
'precondition: seeded with the module as msgctxt');
247 $this->
sync([
'greeting' =>
'Servus'], $refresh);
249 $this->assertNull($this->
overlayPo()->find(self::MODULE,
'greeting'),
'no longer stored under the module context');
250 $migrated = $this->
overlayPo()->find(
null,
'greeting');
251 $this->assertNotNull($migrated);
252 $this->assertSame(
'Servus', $migrated->getTranslation());
253 $this->assertSame(
'Hallo', LocalChangeComments::getOriginal($migrated));
254 $this->assertSame(
'2020-01-01T00:00:00Z', LocalChangeComments::getLocalChange($migrated));
258 $this->assertSame([
'shown on the login page'], $migrated->getExtractedComments());
269 $this->
seedShipped([
'unchanged_one' =>
'A',
'unchanged_two' =>
'B',
'changed' =>
'C']);
273 'unchanged_one' => [
'value' =>
'A',
'original' =>
'A'],
274 'unchanged_two' => [
'value' =>
'B',
'original' =>
'B'],
275 'changed' => [
'value' =>
'C, lokal',
'original' =>
'C',
'local_change' =>
'2020-01-01T00:00:00Z'],
278 $this->
sync([
'unchanged_one' =>
'A',
'unchanged_two' =>
'B',
'changed' =>
'C, lokal'],
true);
282 '2020-01-01T00:00:00Z',
283 LocalChangeComments::getLocalChange($this->
overlayPo()->find(
null,
'changed'))
289 $this->
seedShipped([
'greeting' =>
'Hallo, überarbeitet']);
290 $this->
seedOverlay([
'greeting' => [
'value' =>
'Hallo',
'original' =>
'Hallo']]);
292 $this->
sync([
'greeting' =>
'Hallo, überarbeitet'],
true);
295 $this->assertFileDoesNotExist($this->
overlayBase() .
'.po');
297 [
'value' =>
'Hallo, überarbeitet',
'local_change' =>
false,
'local_change_date' =>
null,
'original' =>
'Hallo, überarbeitet'],
298 MigratedLanguageFileSync::loadModuleTranslations($this->manager,
'de', self::MODULE, $this->client_data_dir)[
'greeting']
304 $this->
seedShipped([
'greeting' =>
'Hallo, überarbeitet']);
306 'greeting' => [
'value' =>
'Servus',
'original' =>
'Hallo',
'local_change' =>
'2020-01-01T00:00:00Z'],
309 $this->
sync([
'greeting' =>
'Servus'],
true);
311 $greeting = $this->
overlayPo()->find(
null,
'greeting');
312 $this->assertSame(
'Servus', $greeting->getTranslation());
313 $this->assertSame(
'Hallo, überarbeitet', LocalChangeComments::getOriginal($greeting));
314 $this->assertSame(
'2020-01-01T00:00:00Z', LocalChangeComments::getLocalChange($greeting));
326 'greeting' => [
'value' =>
'Hallo',
'original' =>
'Hallo'],
327 'dropped' => [
'value' =>
'Neu',
'original' =>
'Alt',
'local_change' =>
'2020-01-01T00:00:00Z'],
330 $this->
sync([
'greeting' =>
'Hallo',
'dropped' =>
'Neu'],
true);
332 $dropped = $this->
overlayPo()->find(
null,
'dropped');
333 $this->assertSame(
'Alt', LocalChangeComments::getOriginal($dropped));
334 $this->assertSame(
'2020-01-01T00:00:00Z', LocalChangeComments::getLocalChange($dropped));
344 #[DataProvider('refreshFuzzyCases')]
349 ?
bool $expected_fuzzy
354 $this->
sync([
'greeting' => $value],
true);
358 if ($expected_fuzzy ===
null) {
359 $this->assertFileDoesNotExist($this->
overlayBase() .
'.po');
362 $this->assertSame($expected_fuzzy, $this->
overlayPo()->find(
null,
'greeting')->hasFlag(
'fuzzy'));
371 'shipped fuzzy, value is shipped' => [
372 [
'value' =>
'Hello',
'fuzzy' =>
true], [
'value' =>
'Hello',
'original' =>
'Hello'],
'Hello',
null,
374 'shipped fuzzy, value differs' => [
375 [
'value' =>
'Hello',
'fuzzy' =>
true], [
'value' =>
'Hallo',
'original' =>
'Hello',
'fuzzy' =>
true],
'Hallo',
false,
377 'shipped reviewed now, overlay still fuzzy' => [
378 [
'value' =>
'Hallo'], [
'value' =>
'Hallo',
'original' =>
'Hello',
'fuzzy' =>
true],
'Hallo',
null,
385 $this->seedShipped([
'greeting' =>
'Hallo',
'farewell' =>
'Tschüss']);
388 'greeting' => [
'value' =>
'Servus',
'original' =>
'Hallo',
'local_change' =>
'2020-01-01T00:00:00Z'],
389 'farewell' => [
'value' =>
'Pfiat di',
'original' =>
'Tschüss',
'local_change' =>
'2020-01-01T00:00:00Z'],
392 $this->sync([
'greeting' =>
'Servus']);
394 $this->assertSame([
'greeting'], $this->overlayIdentifiers());
401 $this->seedShipped([
'greeting' =>
'Hallo']);
402 $this->seedOverlay([
'greeting' => [
'value' =>
'Servus',
'original' =>
'Hallo',
'local_change' =>
'2020-01-01T00:00:00Z']]);
406 $this->assertFileDoesNotExist($this->overlayBase() .
'.po');
418 $this->seedShipped([
'greeting' =>
'Hallo']);
420 'greeting' => [
'value' =>
'Hallo',
'original' =>
'Hallo'],
421 'foreign' => [
'value' =>
'Fremd',
'context' =>
'other_module'],
425 $this->sync([
'greeting' =>
'Servus']);
427 $this->assertNull($this->overlayPo()->find(
'other_module',
'foreign'));
436 $this->seedShipped([
'b' =>
'B']);
438 $this->sync([
'b' =>
'B',
'a' =>
'A',
'C' =>
'C',
'9' =>
'neun',
'10' =>
'zehn']);
441 $this->assertSame([
'10',
'9',
'C',
'a'], $this->overlayIdentifiers());
An in-memory gettext catalog (header block plus messages) and the only place of the component that re...
Reads the shipped .po of a migrated module and maintains its per-installation overlay: the ....
sync() computing the delta overlay: creation, admin edit, reconciling write (refresh),...
testTheOverlayTakesTheHeadersOfTheShippedPo()
static refreshFuzzyCases()
testEntriesOfAnotherContextInTheOldOverlayAreNotCarriedOver()
Entries of a different context (another module) are never taken over into this module's overlay: the ...
testSyncingNoEntriesRemovesTheOverlayButKeepsTheShippedFile()
An empty delta removes the overlay files - the shipped file and the lock file are kept.
testTheOverlayTakesTheExtractedCommentsOfTheShippedEntry()
Extracted comments ("#. ...", context for translators) are part of the shipped file and are carried i...
testEntriesAreWrittenSortedByIdentifier()
Sorted byte-wise by identifier, independent of the order of $entries - keeps the file diff-stable.
testTheDeltaIsComparedAgainstTheRawShippedValueNotTheMarkupCleanedOne()
The delta is compared against the RAW shipped .po value (TranslationCatalog::fromPoFile()),...
testRefreshRecomputesFuzzyFromTheShippedEntry(array $shipped, array $overlay, string $value, ?bool $expected_fuzzy)
On refresh fuzzy is recomputed from the shipped file: set exactly when the value is the shipped value...
testCreatingTheOverlayWithADeviatingValueRecordsShippedOriginalAndALocalChange()
testTheLocalChangeTimestampOfAnUntouchedEntrySurvivesAnUnrelatedWrite()
A full-replace write re-sends every value; an entry whose value did not change must keep its existing...
testSyncingOnlyShippedValuesWritesNoOverlay(bool $refresh)
A module whose values are all shipped ones needs no overlay: neither file, lock nor directory is crea...
testReplaceSemanticsRemoveEveryEntryThatIsNotPassed()
testALegacyEntryWithTheModuleAsMsgctxtIsMigratedToNoContextPreservingItsBookkeeping(bool $refresh)
An overlay of an earlier version carried the module as msgctxt on every entry (see seedOverlay()); th...
testRefreshMovesTheOriginalButKeepsALocalCustomizationFlagged()
testCreatingTheOverlayKeepsShippedFuzzyOnlyForTheUnchangedShippedValue()
A shipped "fuzzy" (an unreviewed value, usually the English fallback) must survive the very first syn...
testRefreshKeepsTheOriginalOfAnEntryTheShippedPoNoLongerContains()
An entry the shipped .po no longer contains (kept by the caller as a real local change,...
testAnAdminEditNeverMovesTheOriginal()
refresh = false (an ordinary admin edit): the baseline "original" must never move - even when the shi...
testRefreshMovesTheOriginalToTheNewShippedValueAndClearsTheLocalChangeOfAnUnmodifiedEntry()
testWritingTheOriginalValueBackClearsTheLocalChange()
testCreatingTheOverlayGivesAnEntryWithoutShippedCounterpartNoOriginalButALocalChange()
testAnAdminEditRemovesFuzzyOnlyFromTheValuesItActuallyChanges()
testALegacyFullOverlayShrinksToTheDeltaOnTheNextReconcilingWrite()
An overlay written by the pre-delta design carried EVERY entry of the module.
Fixtures of the MigratedLanguageFileSync tests.
sync(array $entries, bool $refresh=false, ?string $client_data_dir='default')
seedOverlay(array $entries, string $lang_key='de')
seedShipped(array $entries, string $lang_key='de', string $module=self::MODULE)
overlayBase(string $lang_key='de')
shippedPo(string $lang_key='de', string $module=self::MODULE)
static writePair(string $base_path, TranslationCatalog $catalog)
Writes "<base>.po" and, like MigratedLanguageFileSync::sync(), its compiled revision named by current...
static catalog(string $module, array $entries)
static readMo(string $file)
static writePo(string $file, TranslationCatalog $catalog)
static overlayLock(string $base_path)
static overlayMo(string $base_path)
The compiled catalog of the revision current names; a non-existing path if there is none.
static readPo(string $file)
A directory of language files with a path and a prefix for them.