19declare(strict_types=1);
28use PHPUnit\Framework\Attributes\DataProvider;
29use PHPUnit\Framework\TestCase;
44 file_put_contents($this->
shippedPo(),
"msgid \"kaputt\n");
45 $this->
seedOverlay([
'greeting' => [
'value' =>
'Servus',
'original' =>
'Hallo',
'local_change' =>
'2020-01-01T00:00:00Z']]);
47 $result = MigratedLanguageFileSync::loadLocalChanges($this->manager,
'de', self::MODULE, $this->client_data_dir);
50 [
'value' =>
'Servus',
'local_change' =>
true,
'local_change_date' =>
'2020-01-01 00:00:00',
'original' =>
'Hallo'],
57 $this->
seedShipped([
'greeting' =>
'Hallo',
'farewell' =>
'Tschüss']);
58 $this->
seedOverlay([
'greeting' => [
'value' =>
'Servus',
'original' =>
'Hallo']]);
61 [
'greeting' => [
'value' =>
'Servus',
'local_change' =>
false,
'local_change_date' =>
null,
'original' =>
'Hallo']],
62 MigratedLanguageFileSync::loadLocalChanges($this->manager,
'de', self::MODULE, $this->client_data_dir)
70 $this->assertSame([], MigratedLanguageFileSync::loadLocalChanges($this->manager,
'de', self::MODULE, $this->client_data_dir));
77 $this->assertNull(MigratedLanguageFileSync::loadLocalChanges($this->manager,
'de', self::MODULE, $this->client_data_dir));
84 $this->assertNull(MigratedLanguageFileSync::loadLocalChanges($this->manager,
'de',
'other_module', $this->client_data_dir));
85 $this->assertNull(MigratedLanguageFileSync::loadLocalChanges($this->manager,
'de', self::MODULE,
null));
99 $this->expectException(RuntimeException::class);
100 $this->expectExceptionMessageMatches(
'/has no/');
101 MigratedLanguageFileSync::loadModuleTranslations($this->manager,
'de', self::MODULE, $this->client_data_dir);
113 $this->expectException(RuntimeException::class);
114 MigratedLanguageFileSync::loadLocalChanges($this->manager,
'de', self::MODULE, $this->client_data_dir);
126 $this->expectException(RuntimeException::class);
127 $this->expectExceptionMessageMatches(
'/no regular file/');
128 MigratedLanguageFileSync::loadModuleTranslations($this->manager,
'de', self::MODULE, $this->client_data_dir);
140 $this->expectException(RuntimeException::class);
141 $this->
sync([
'greeting' =>
'Hallo']);
156 'greeting' =>
'Hallo',
157 'foreign' => [
'value' =>
'Fremd',
'context' =>
'other_module'],
159 $this->
seedShipped([
'farewell' =>
'Tschüss'],
'de',
'stwo');
160 $this->
seedShipped([
'farewell' =>
'Bye'],
'en',
'stwo');
161 $this->
seedShipped([
'x' =>
'only english'],
'en',
'nomig');
164 [self::MODULE => [
'greeting' =>
'Hallo'],
'stwo' => [
'farewell' =>
'Tschüss']],
165 MigratedLanguageFileSync::loadShippedModules(
$manager, $this->ilias_root,
'de')
174 [self::MODULE => []],
175 MigratedLanguageFileSync::loadShippedModules($this->manager, $this->ilias_root,
'de')
188 [
'greeting' =>
'Hallo'],
189 MigratedLanguageFileSync::loadShippedModules($this->manager, $this->ilias_root,
'de')[self::MODULE]
195 [
'greeting' =>
'Servus'],
196 MigratedLanguageFileSync::loadShippedModules($this->manager, $this->ilias_root,
'de')[self::MODULE]
208 $cache =
new ReflectionProperty(MigratedLanguageFileSync::class,
'shipped_catalogs');
209 $cache->setValue(
null, []);
212 for ($i = 0; $i < 520; $i++) {
216 $this->fixture_directory .
'/' .
$module .
'_de.po',
219 MigratedLanguageFileSync::loadShippedModuleEntries($this->fixture_directory .
'/' .
$module .
'_de.po',
$module);
223 $this->assertLessThanOrEqual(512, count($cache->getValue()));
225 $cache->setValue(
null, []);
246 file_put_contents($this->
shippedPo(
'de',
'scorrupt'),
"msgid \"kaputt\n");
247 $this->
seedShipped([
'x' =>
'only english'],
'en',
'senonly');
250 [self::MODULE => $this->
shippedPo(),
'scorrupt' => $this->
shippedPo(
'de',
'scorrupt')],
251 MigratedLanguageFileSync::findShippedModuleFiles(
$manager, $this->ilias_root,
'de')
259 $this->assertFalse(MigratedLanguageFileSync::hasShippedModules($this->manager, $this->ilias_root, []));
260 $this->assertFalse(MigratedLanguageFileSync::hasShippedModules($this->manager, $this->ilias_root, [
'en']));
261 $this->assertTrue(MigratedLanguageFileSync::hasShippedModules($this->manager, $this->ilias_root, [
'en',
'de']));
271 'greeting' =>
"Hallo\nWelt",
272 'farewell' =>
'Tschüss',
273 'foreign' => [
'value' =>
'Fremd',
'context' =>
'other_module'],
275 $catalog->find(self::MODULE,
'greeting')->addExtractedComment(
'shown on the login page');
276 $catalog->find(self::MODULE,
'greeting')->addExtractedComment(
'keep it short');
277 $catalog->find(
'other_module',
'foreign')->addExtractedComment(
'not returned');
280 $entries = MigratedLanguageFileSync::loadShippedModuleEntries($this->
shippedPo(), self::MODULE);
285 'farewell' => [
'value' =>
'Tschüss',
'comment' =>
null],
286 'greeting' => [
'value' =>
"Hallo\nWelt",
'comment' =>
'shown on the login page keep it short'],
296 $catalog->find(self::MODULE,
'greeting')->addExtractedComment(
'en note');
297 $catalog->find(self::MODULE,
'greeting')->addTranslatorNote(
'egen anteckning');
298 $catalog->find(self::MODULE,
'farewell')->addExtractedComment(
'en note');
301 $entries = MigratedLanguageFileSync::loadShippedModuleEntries($this->
shippedPo(), self::MODULE);
303 $this->assertSame(
'egen anteckning', $entries[
'greeting'][
'comment']);
304 $this->assertSame(
'en note', $entries[
'farewell'][
'comment']);
306 [
'greeting' => [
'egen anteckning'],
'farewell' => [
'en note']],
307 MigratedLanguageFileSync::loadShippedNotes($this->
shippedPo(), self::MODULE)
313 file_put_contents($this->
shippedPo(),
"msgid \"kaputt\n");
315 $this->expectException(RuntimeException::class);
316 MigratedLanguageFileSync::loadShippedModuleEntries($this->
shippedPo(), self::MODULE);
323 'changed' => [
'value' =>
'Servus',
'original' =>
'Hallo',
'local_change' =>
'2026-09-21T10:11:12Z'],
324 'unchanged' => [
'value' =>
'Tschüss',
'original' =>
'Tschüss'],
325 'foreign' => [
'value' =>
'x',
'context' =>
'other_module'],
332 'local_change' =>
true,
333 'local_change_date' =>
'2026-09-21 10:11:12',
334 'original' =>
'Hallo',
337 'value' =>
'Tschüss',
338 'local_change' =>
false,
339 'local_change_date' =>
null,
340 'original' =>
'Tschüss',
343 MigratedLanguageFileSync::loadModuleTranslations($this->manager,
'de', self::MODULE, $this->client_data_dir)
351 $this->
seedOverlay([
'greeting' => [
'value' =>
'Servus',
'original' =>
'Hallo',
'local_change' =>
'2020-01-01T00:00:00Z']]);
355 [
'value' =>
'Servus',
'local_change' =>
true,
'local_change_date' =>
'2020-01-01 00:00:00',
'original' =>
'Hallo'],
356 MigratedLanguageFileSync::loadModuleTranslations($this->manager,
'de', self::MODULE, $this->client_data_dir)[
'greeting']
377 MigratedLanguageFileSync::loadModuleTranslations($this->manager,
'de', self::MODULE, $this->client_data_dir);
378 $this->fail(
'Expected a RuntimeException');
379 }
catch (RuntimeException
$e) {
380 $this->assertStringContainsString(
'has no',
$e->getMessage());
383 $this->
sync([
'greeting' =>
'Hallo']);
384 $this->fail(
'Expected a RuntimeException');
385 }
catch (RuntimeException) {
396 $this->assertSame([self::MODULE], MigratedLanguageFileSync::getMigratedModules($this->manager,
'de', $this->client_data_dir));
397 $this->assertSame([], MigratedLanguageFileSync::getMigratedModules($this->manager,
'en', $this->client_data_dir));
398 $this->assertSame([], MigratedLanguageFileSync::getMigratedModules($this->manager,
'de',
null));
409 $this->assertDirectoryDoesNotExist(dirname($this->
overlayBase()));
410 $this->assertSame([self::MODULE], MigratedLanguageFileSync::getMigratedModules($this->manager,
'de', $this->client_data_dir));
417 $shipped_before = file_get_contents($this->
shippedPo());
419 MigratedLanguageFileSync::removeOverlay($this->manager,
'de', self::MODULE, $this->client_data_dir);
421 $this->assertFileDoesNotExist($this->
overlayBase() .
'.po');
423 $this->assertSame($shipped_before, file_get_contents($this->
shippedPo()));
431 MigratedLanguageFileSync::removeOverlay($this->manager,
'de', self::MODULE, $this->client_data_dir);
433 $this->assertFileExists($this->
overlayBase(
'en') .
'.po');
439 MigratedLanguageFileSync::removeOverlay($this->manager,
'de', self::MODULE, $this->client_data_dir);
442 MigratedLanguageFileSync::removeOverlay($this->manager,
'de',
'other_module', $this->client_data_dir);
443 MigratedLanguageFileSync::removeOverlay($this->manager,
'de', self::MODULE,
null);
445 $this->assertFileExists($this->
overlayBase() .
'.po');
451 if (function_exists(
'posix_geteuid') && posix_geteuid() === 0) {
452 $this->markTestSkipped(
'A non-removable file cannot be forced for root (no root-proof variant for unlink()).');
455 $overlay_directory = dirname($this->
overlayBase());
456 chmod($overlay_directory, 0555);
459 $this->expectErrorLog();
460 set_error_handler(
static fn():
bool =>
true, E_WARNING);
462 $this->expectException(RuntimeException::class);
463 $this->expectExceptionMessageMatches(
'/overlay file/i');
464 MigratedLanguageFileSync::removeOverlay($this->manager,
'de', self::MODULE, $this->client_data_dir);
466 restore_error_handler();
467 chmod($overlay_directory, 0775);
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 ....
Read side of MigratedLanguageFileSync: local changes, shipped modules and entries,...
testLoadLocalChangesNeverParsesTheShippedPoOnlyChecksItExists()
loadLocalChanges() is loadModuleTranslations()'s pure overlay-delta half: only the shipped ....
testLoadLocalChangesReturnsNullForAnUnknownModuleOrWithoutClientDataDir()
testTheTranslatorCommentIsTheNoteAndBothKindsAreShippedComments()
The own note (translator comment) wins over the reference note ("#."); loadShippedNotes() agrees.
testLoadLocalChangesReturnsAnEmptyArrayWithoutAnyOverlay()
testAnOverlayMoWithoutItsPoIsUnreadableAndNotRemovedByAWrite()
Without an overlay .po at all, loadModuleTranslations() falls back to the shipped-only result (never ...
testHasShippedModulesOnlyForALanguageWithAShippedPo()
testFindShippedModuleFilesListsEveryModuleWithAShippedPoForTheLanguage()
Every module with a shipped .po for the language - also one whose .po cannot be parsed (the callers n...
testAnIrregularFileAtTheOverlayPoPositionThrows()
An irregular file (here: a directory) at the overlay .po position must not be silently skipped either...
testGetMigratedModulesListsAModuleByItsShippedPoAloneIndependentOfAnyOverlay()
testLoadLocalChangesThrowsForAnOverlayMoWithoutItsPo()
The same guard for loadLocalChanges(), which LanguageInstallationManager's loadOverlay() relies on to...
testGetMigratedModulesListsAModuleWithNoOverlayFileWhatsoever()
The essence of "migrated == has a shipped .po": a module without ANY overlay file at all (never had a...
testLoadShippedModulesReturnsTheShippedValuesOfEveryMigratedModuleForTheLanguage()
testLoadLocalChangesReturnsOnlyTheOverlayEntriesNotTheShippedOnes()
testLoadShippedModuleEntriesThrowsForACorruptPo()
testRemoveOverlayDeletesBothOverlayFilesButLeavesTheShippedPoUntouched()
testAChangedShippedPoWithinTheSameRequestIsReReadNotServedFromTheCache()
readShippedPo() caches a parsed shipped .po per request, keyed by its content hash - so a ....
testRemoveOverlayOnlyRemovesTheRequestedLanguage()
testLoadShippedModulesReturnsAnEmptyListForAModuleWhoseShippedPoIsEmpty()
testAnOverlayMoWithoutItsPoThrowsInsteadOfBeingTreatedAsAbsent()
An overlay .mo without its .po (which carries the bookkeeping) must never be treated as "no overlay" ...
testRemoveOverlayIsANoOpWithoutOverlayUnknownModuleOrClientDataDir()
testLoadLocalChangesReturnsNullWithoutAShippedPo()
testLoadModuleTranslationsReturnsValueLocalChangeDateAndOriginalPerEntry()
testTheShippedPoCacheNeverGrowsBeyondItsConfiguredLimit()
The cache is bounded (SHIPPED_CATALOG_CACHE_SIZE = 512, see the class docblock) - installing all ship...
testRemoveOverlayThrowsWhenAnOverlayFileCannotBeRemoved()
testSyncThrowsForAnOverlayMoWithoutItsPoInsteadOfRemovingOrRebuildingIt()
sync() itself must refuse to touch such an overlay too - not silently rebuild or remove it (see the c...
testLoadModuleTranslationsIgnoresAMissingOverlayMoAndStillReadsTheOverlayPo()
Only the overlay .po is read: a missing overlay .mo does not make the module "not migrated".
testLoadShippedModuleEntriesReturnsValueAndJoinedExtractedComment()
The extracted comments ("#.", the counterpart of a .lang "###" comment) of an entry are joined into o...
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)
LanguageFileDirectoryManager $manager
LanguageFileDirectory $directory
static directory(string $prefix, string $path, bool $local=false)
static catalog(string $module, array $entries)
static readMo(string $file)
static writePo(string $file, TranslationCatalog $catalog)
static overlayMo(string $base_path)
The compiled catalog of the revision current names; a non-existing path if there is none.
static writeOverlayRevision(string $base_path, TranslationCatalog $catalog)
Compiles $catalog into a revision of the overlay $base_path and points current to it,...
if(!isset($arguments[0])) if(count( $arguments) > 3) if(! $remove_from_lang &&isset($arguments[2]) &&!is_dir($arguments[2])) $module
A directory of language files with a path and a prefix for them.