19declare(strict_types=1);
28use PHPUnit\Framework\Attributes\DataProvider;
29use PHPUnit\Framework\TestCase;
41 #[DataProvider('refreshFlags')]
45 $this->
sync([
'greeting' =>
'Servus']);
46 $po_before = file_get_contents($this->
overlayBase() .
'.po');
48 $po_inode = fileinode($this->
overlayBase() .
'.po');
50 $cache =
new ReflectionProperty(\
ILIAS\Language\ComponentTranslation\TranslationGettextRepository::class,
'states');
51 $cache->setValue(
null, [self::MODULE .
'|de' => [
'greeting' =>
'cached']]);
54 $this->
sync([
'greeting' =>
'Servus'], $refresh);
56 $this->assertSame([self::MODULE .
'|de' => [
'greeting' =>
'cached']], $cache->getValue(),
'no invalidation');
58 $cache->setValue(
null, []);
61 $this->assertSame($po_before, file_get_contents($this->
overlayBase() .
'.po'));
63 $this->assertSame($po_inode, fileinode($this->
overlayBase() .
'.po'),
'the .po was not replaced');
71 $this->
sync([
'greeting' =>
'Servus']);
72 $po_before = file_get_contents($this->
overlayBase() .
'.po');
74 $po_inode = fileinode($this->
overlayBase() .
'.po');
77 $this->
sync([
'greeting' =>
'Servus']);
80 $this->assertSame($po_before, file_get_contents($this->
overlayBase() .
'.po'));
81 $this->assertSame($po_inode, fileinode($this->
overlayBase() .
'.po'),
'the unchanged .po is not rewritten');
92 #[DataProvider('staleMoContents')]
95 $this->
seedShipped([
'greeting' =>
'Hallo',
'farewell' =>
'Tschüss']);
96 $this->
sync([
'greeting' =>
'Servus',
'farewell' =>
'Tschüss']);
97 $po_before = file_get_contents($this->
overlayBase() .
'.po');
98 $po_inode = fileinode($this->
overlayBase() .
'.po');
101 $cache =
new ReflectionProperty(\
ILIAS\Language\ComponentTranslation\TranslationGettextRepository::class,
'states');
102 $cache->setValue(
null, [self::MODULE .
'|de' => [
'greeting' =>
'stale']]);
105 $this->
sync([
'greeting' =>
'Servus',
'farewell' =>
'Tschüss'], $refresh);
107 $this->assertSame([], $cache->getValue(),
'the rebuilt .mo invalidates the read cache');
109 $cache->setValue(
null, []);
114 $this->assertSame($mo_before, $mo_after);
118 $this->assertSame($po_before, file_get_contents($this->
overlayBase() .
'.po'));
119 $this->assertSame($po_inode, fileinode($this->
overlayBase() .
'.po'),
'the unchanged .po is not rewritten');
128 'garbage' => [
static fn(
string $mo):
string =>
'SENTINEL',
false],
129 'truncated' => [
static fn(
string $mo):
string => substr($mo, 0, intdiv(strlen($mo), 2)),
false],
130 'one byte missing' => [
static fn(
string $mo):
string => substr($mo, 0, -1),
false],
131 'empty' => [
static fn(
string $mo):
string =>
'',
false],
132 'another catalog' => [
136 'garbage, reconciling write' => [
static fn(
string $mo):
string =>
'SENTINEL',
true],
144 $this->
sync([
'greeting' =>
'Moin']);
147 $this->
sync([
'greeting' =>
'Servus']);
159 $this->
seedShipped([
'greeting' =>
'Hallo',
'farewell' =>
'Tschüss']);
161 MigratedLanguageFileSync::sync(
166 [
'greeting' =>
'Servus',
'farewell' =>
'Pfiat di'],
167 $this->client_data_dir,
171 [
'greeting' =>
'2026-10-02 09:36:30',
'farewell' =>
'not a timestamp']
174 $this->assertSame(
'2026-10-02T09:36:30Z', LocalChangeComments::getLocalChange($this->
overlayPo()->find(
null,
'greeting')));
175 $this->assertNotNull(LocalChangeComments::getLocalChange($this->
overlayPo()->find(
null,
'farewell')),
'an unreadable date: the time of the write');
176 $this->assertNotSame(
'2026-10-02T09:36:30Z', LocalChangeComments::getLocalChange($this->
overlayPo()->find(
null,
'farewell')));
182 $cache =
new ReflectionProperty(\
ILIAS\Language\ComponentTranslation\TranslationGettextRepository::class,
'states');
183 $cache->setValue(
null, [self::MODULE .
'|de' => [
'greeting' =>
'stale'],
'other|de' => [
'x' =>
'y']]);
186 $this->
sync([
'greeting' =>
'Servus']);
188 $this->assertSame([
'other|de' => [
'x' =>
'y']], $cache->getValue());
190 $cache->setValue(
null, []);
203 $this->
seedShipped([
'zero' =>
'1',
'greeting' =>
'Hallo']);
205 $this->
sync([
'zero' =>
'0',
'greeting' =>
'Hallo']);
207 $po = file_get_contents($this->
overlayBase() .
'.po');
208 $this->assertStringContainsString(
"msgid \"zero\"\nmsgstr \"0\"\n", $po);
210 $zero = $this->
overlayPo()->find(
null,
'zero');
211 $this->assertSame(
'1', LocalChangeComments::getOriginal($zero));
212 $this->assertNotNull(LocalChangeComments::getLocalChange($zero));
214 $po_inode = fileinode($this->
overlayBase() .
'.po');
216 $this->
sync([
'zero' =>
'0',
'greeting' =>
'Hallo'],
true);
217 $this->
sync([
'zero' =>
'0',
'greeting' =>
'Hallo']);
220 $this->assertSame($po_inode, fileinode($this->
overlayBase() .
'.po'),
'the .po was not replaced');
227 $this->
sync([
'count' =>
'1']);
229 $this->
sync([
'count' =>
'0']);
231 $count = $this->
overlayPo()->find(
null,
'count');
232 $this->assertSame(
'0', $count?->getTranslation());
233 $this->assertSame(
'1', LocalChangeComments::getOriginal($count));
234 $this->assertNotNull(LocalChangeComments::getLocalChange($count));
244 $shipped = [
'lf' =>
"Zeile 1\nZeile 2",
'crlf' =>
"Zeile 1\r\nZeile 2",
'backslash' =>
'C:\\pfad',
'literal' =>
'a\\nb'];
247 $this->
sync($shipped);
248 $this->
sync($shipped,
true);
249 $this->
sync($shipped);
252 $this->assertFileDoesNotExist($this->
overlayBase() .
'.po');
253 $translations = MigratedLanguageFileSync::loadModuleTranslations($this->manager,
'de', self::MODULE, $this->client_data_dir);
254 foreach ($shipped as $identifier => $value) {
255 $this->assertSame($value, $translations[$identifier][
'value'], $identifier);
256 $this->assertFalse($translations[$identifier][
'local_change'], $identifier);
263 $this->
sync([
'lf' =>
"Zeile 1\nZeile 2"]);
265 $this->
sync([
'lf' =>
"Zeile 1\r\nZeile 2"]);
267 $entry = $this->
overlayPo()->find(
null,
'lf');
268 $this->assertSame(
"Zeile 1\nZeile 2", LocalChangeComments::getOriginal($entry));
269 $this->assertNotNull(LocalChangeComments::getLocalChange($entry));
280 $catalog =
MigratedPoFixture::catalog(self::MODULE, [
'lf' => [
'value' =>
"Zeile 1\nZeile 2",
'local_change' =>
'2020-01-01T00:00:00Z']]);
281 $catalog->find(self::MODULE,
'lf')->addTranslatorComment(
'original: Zeile 1 Zeile 2');
284 $this->
sync([
'lf' =>
"Zeile 1\nZeile 2"],
true);
288 $this->assertFileDoesNotExist($this->
overlayBase() .
'.po');
289 $this->assertFalse(MigratedLanguageFileSync::loadModuleTranslations(
293 $this->client_data_dir
294 )[
'lf'][
'local_change']);
305 $this->
seedShipped([
'path' =>
'C:\\neu',
'greeting' =>
'Hallo']);
306 $catalog =
MigratedPoFixture::catalog(self::MODULE, [
'path' =>
'C:\\pfad',
'greeting' => [
'value' =>
'Hallo',
'original' =>
'Hallo']]);
307 $catalog->find(self::MODULE,
'path')->addTranslatorComment(
'original: C:\\pfad');
310 $this->
sync([
'path' =>
'C:\\pfad',
'greeting' =>
'Servus']);
312 $path = $this->
overlayPo()->find(
null,
'path');
313 $this->assertSame(
'C:\\pfad', LocalChangeComments::getOriginal($path));
314 $this->assertNull(LocalChangeComments::getLocalChange($path));
315 $this->assertContains(
'original: C:\\pfad', $path->getTranslatorComments(),
'an admin edit never moves the original');
321 $this->
seedOverlay([
'lf' => [
'value' =>
"Zeile 1\nZeile 2",
'original' =>
"Zeile 1\nZeile 2"]]);
325 MigratedLanguageFileSync::loadModuleTranslations($this->manager,
'de', self::MODULE, $this->client_data_dir)[
'lf'][
'original']
333 'greeting' => [
'value' =>
'Hallo',
'original' =>
'Hallo'],
334 'dropped' => [
'value' =>
"Neu\nZeile",
'original' =>
"Alt\nZeile",
'local_change' =>
'2020-01-01T00:00:00Z'],
337 $this->
sync([
'greeting' =>
'Hallo',
'dropped' =>
"Neu\nZeile"],
true);
339 $dropped = $this->
overlayPo()->find(
null,
'dropped');
340 $this->assertSame(
"Alt\nZeile", LocalChangeComments::getOriginal($dropped));
341 $this->assertSame(
'2020-01-01T00:00:00Z', LocalChangeComments::getLocalChange($dropped));
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() and the overlay files: no-op guard, self-healing .mo, cache invalidation, value "0",...
testAChangedValueRewritesBothFiles()
testASyncInvalidatesTheIlLanguageReadCache()
testAStaleMoNextToAnUnchangedPoIsRebuilt(\Closure $stale_content, bool $refresh)
Self-healing: a .mo that does not hold exactly what TranslationCatalog::toMoString() compiles from th...
testAMissingMoIsRecompiledEvenWhenThePoIsUnchanged()
testALegacyOriginalWithABackslashIsReadVerbatimByAnAdminEdit()
An "original: " comment of the legacy format containing a backslash is read verbatim: an admin edit o...
testAValueChangedToZeroIsALocalChangeAndServedAsZero()
testAnUnchangedMultiLineShippedValueIsNoLocalChangeAndResyncingIsANoOp()
A shipped value with line breaks or backslashes is stored as "original" losslessly: re-syncing it is ...
testTheValueZeroSurvivesTheSyncAndResyncingIsANoOp()
gettext/gettext treats a falsy translation as absent; the adapter's workaround must hold on the whole...
testChangingOnlyTheLineBreakOfAMultiLineValueIsALocalChange()
testRefreshKeepsAnEscapedOriginalOfAnEntryTheShippedPoNoLongerContains()
testAReconcilingWriteRepairsALegacyFlattenedOriginal()
An overlay written by an earlier version has the multi-line original flattened into spaces - a reconc...
testAnUnchangedOverlayWithACorrectMoWritesNothing(bool $refresh)
No-op guard: an unchanged .po next to a .mo equal to what TranslationCatalog::toMoString() compiles w...
testANewOverlayEntryTakesItsLocalChangeDateFromTheGivenDates()
An entry that enters the overlay with a write that knows when it was changed locally (setup update: l...
testLoadModuleTranslationsReturnsTheDecodedMultiLineOriginal()
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')
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 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.
Interface Observer \BackgroundTasks Contains several chained tasks and infos about them.