19declare(strict_types=1);
26use PHPUnit\Framework\Attributes\DataProvider;
27use PHPUnit\Framework\Attributes\RunInSeparateProcess;
28use PHPUnit\Framework\TestCase;
41 private const string DOMAIN =
'ntg.de';
48 protected function setUp(): void
51 $this->root = sys_get_temp_dir() .
'/ilias_native_gettext_' . bin2hex(random_bytes(6));
52 mkdir($this->root, 0775,
true);
55 $catalog->setHeader(
'Plural-Forms',
'nplurals=2; plural=(n != 1);');
57 $item->setPlural(
'items', [
'Eintrag',
'Einträge']);
64 $this->assertNotNull($build);
87 yield
'nothing set before' => [
false,
false];
88 yield
'LANGUAGE set before' => [
'de:fr',
false];
89 yield
'LOCPATH set before' => [
false,
'/nonexistent/locpath'];
90 yield
'both set before' => [
'en',
'/nonexistent/locpath'];
93 #[DataProvider('previousProcessSettings')]
97 putenv($language ===
false ?
'LANGUAGE' :
'LANGUAGE=' . $language);
98 putenv($locpath ===
false ?
'LOCPATH' :
'LOCPATH=' . $locpath);
99 $messages_before = setlocale(LC_MESSAGES,
'0');
106 $this->assertSame($locpath, getenv(
'LOCPATH'),
'LOCPATH only lives for the setlocale() call (no system locale is found while it is set)');
112 $this->assertSame($messages_before, setlocale(LC_MESSAGES,
'0'));
113 $this->assertSame($language, getenv(
'LANGUAGE'));
114 $this->assertSame($locpath, getenv(
'LOCPATH'));
115 $this->assertSame(
'greeting',
NativeGettext::translate(self::DOMAIN,
'greeting'),
'without the locale native gettext answers with the message id');
121 setlocale(LC_MESSAGES,
'C');
122 putenv(
'LANGUAGE=fr');
126 $this->assertSame(
'C', setlocale(LC_MESSAGES,
'0'));
127 $this->assertSame(
'fr', getenv(
'LANGUAGE'));
132 putenv(
'LANGUAGE=de');
133 $messages = setlocale(LC_MESSAGES,
'0');
137 $this->assertSame(
'de', getenv(
'LANGUAGE'));
138 $this->assertSame($messages, setlocale(LC_MESSAGES,
'0'));
143 $before = setlocale(LC_MESSAGES,
'0');
145 setlocale(LC_MESSAGES, $before);
146 if ($found ===
false) {
156 $this->assertDirectoryDoesNotExist($this->root .
'/no-locale');
164 $this->assertFalse(getenv(
'LOCPATH'),
'the system branch never sets LOCPATH');
173 yield
'C locale' => [
'C',
'de'];
174 yield
'the system locale itself, LANGUAGE unset' => [
'system',
false];
178 #[DataProvider('processSettingsBeforeSystemLocaleActivation')]
188 putenv($language ===
false ?
'LANGUAGE' :
'LANGUAGE=' . $language);
194 $this->assertSame($locale, setlocale(LC_MESSAGES,
'0'),
'a real locale is not replaced');
200 $this->assertSame($locale, setlocale(LC_MESSAGES,
'0'));
201 $this->assertSame($language, getenv(
'LANGUAGE'));
210 yield
'LC_ALL environment variable set to C' => [
static function ():
void {
213 yield
'LC_ALL environment variable removed' => [
static function ():
void {
218 #[DataProvider('foreignChangesInTheSystemLocaleBranch')]
238 yield
'unset' => [
false];
240 yield
'another locale' => [
'de_DE.UTF-8'];
243 #[DataProvider('lcAllBeforeSystemLocaleActivation')]
248 putenv($lc_all ===
false ?
'LC_ALL' :
'LC_ALL=' . $lc_all);
252 $this->assertSame(setlocale(LC_MESSAGES,
'0'), getenv(
'LC_ALL'));
258 $this->assertSame($lc_all, getenv(
'LC_ALL'));
262 #[DataProvider('lcAllBeforeSystemLocaleActivation')]
267 putenv($lc_all ===
false ?
'LC_ALL' :
'LC_ALL=' . $lc_all);
271 putenv(
'LC_ALL=fr_FR.UTF-8');
273 $this->assertSame(setlocale(LC_MESSAGES,
'0'), getenv(
'LC_ALL'));
277 $this->assertSame($lc_all, getenv(
'LC_ALL'));
286 $active = setlocale(LC_MESSAGES,
'0');
291 $this->assertSame($active, getenv(
'LC_ALL'));
301 putenv(
'LC_ALL=fr_FR.UTF-8');
305 $this->assertSame(
'fr_FR.UTF-8', getenv(
'LC_ALL'));
311 putenv(
'LC_ALL=de_DE.UTF-8');
315 $this->assertSame(
'de_DE.UTF-8', getenv(
'LC_ALL'));
318 $this->assertSame(
'de_DE.UTF-8', getenv(
'LC_ALL'));
322 $this->assertFalse(getenv(
'LC_ALL'));
324 $this->assertFalse(getenv(
'LC_ALL'));
338 yield
'no locale directory' => [
'none'];
339 yield
'an empty locale directory' => [
'empty'];
342 #[DataProvider('directoriesWithoutAUsableLocale')]
346 $directory = $this->root .
'/locale-' . $kind;
348 putenv(
'LANGUAGE=de');
349 putenv(
'LOCPATH=/nonexistent/locpath');
350 $messages = setlocale(LC_MESSAGES,
'0');
356 $this->assertSame($messages, setlocale(LC_MESSAGES,
'0'));
357 $this->assertSame(
'de', getenv(
'LANGUAGE'));
358 $this->assertSame(
'/nonexistent/locpath', getenv(
'LOCPATH'));
366 yield
'LC_MESSAGES set to C' => [
static function ():
void {
367 setlocale(LC_MESSAGES,
'C');
369 yield
'LC_ALL set to C (what initLocale() does)' => [
static function ():
void {
370 setlocale(LC_ALL,
'C');
372 yield
'LANGUAGE changed' => [
static function ():
void {
373 putenv(
'LANGUAGE=de');
375 yield
'LANGUAGE removed' => [
static function ():
void {
380 #[DataProvider('foreignChanges')]
387 $this->assertSame(
'greeting',
NativeGettext::translate(self::DOMAIN,
'greeting'),
'precondition: nothing is translated now');
397 $before = setlocale(LC_MESSAGES,
'0');
398 $found = setlocale(LC_MESSAGES, self::REAL_LOCALE);
399 setlocale(LC_MESSAGES, $before);
400 if ($found ===
false) {
401 $this->markTestSkipped(
'the locale "' . self::REAL_LOCALE .
'" is not installed');
405 #[DataProvider('previousProcessSettings')]
410 setlocale(LC_MESSAGES, self::REAL_LOCALE);
411 putenv($language ===
false ?
'LANGUAGE' :
'LANGUAGE=' . $language);
412 putenv($locpath ===
false ?
'LOCPATH' :
'LOCPATH=' . $locpath);
417 $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES,
'0'));
419 $this->assertSame($locpath, getenv(
'LOCPATH'),
'LOCPATH is not touched when the locale stays');
425 $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES,
'0'),
'restore() leaves a locale it did not set');
426 $this->assertSame($language, getenv(
'LANGUAGE'));
427 $this->assertSame($locpath, getenv(
'LOCPATH'));
430 #[DataProvider('lcAllBeforeSystemLocaleActivation')]
435 setlocale(LC_MESSAGES, self::REAL_LOCALE);
436 putenv($lc_all ===
false ?
'LC_ALL' :
'LC_ALL=' . $lc_all);
440 $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES,
'0'));
441 $this->assertSame(self::REAL_LOCALE, getenv(
'LC_ALL'));
447 $this->assertSame($lc_all, getenv(
'LC_ALL'));
448 $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES,
'0'));
457 yield
'glibc branch (own locale)' => [
false];
458 yield
'system locale branch' => [
true];
461 #[DataProvider('branches')]
468 $locpath = getenv(
'LOCPATH');
469 setlocale(LC_ALL, self::REAL_LOCALE);
475 $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES,
'0'));
476 $this->assertSame($locpath, getenv(
'LOCPATH'));
481 $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES,
'0'),
'the locale of the other code is not ours to take back');
493 setlocale(LC_MESSAGES, self::REAL_LOCALE);
494 putenv(
'LANGUAGE=de');
495 $this->assertDirectoryDoesNotExist($this->root .
'/locale-none');
501 $this->assertSame(self::REAL_LOCALE, setlocale(LC_MESSAGES,
'0'));
502 $this->assertSame(
'de', getenv(
'LANGUAGE'));
510 yield
'C.UTF-8' => [
'C.UTF-8'];
511 yield
'C.utf8' => [
'C.utf8'];
512 yield
'POSIX' => [
'POSIX'];
516 #[DataProvider('localesThatAreNotReal')]
520 if (setlocale(LC_MESSAGES, $locale) ===
false) {
521 $this->markTestSkipped(
'the locale "' . $locale .
'" is not installed');
523 $before = setlocale(LC_MESSAGES,
'0');
532 $this->assertSame($before, setlocale(LC_MESSAGES,
'0'));
538 $before = setlocale(LC_MESSAGES,
'0');
550 $this->assertSame($before, setlocale(LC_MESSAGES,
'0'),
'the own locale is ours, so it is taken back');
554 #[RunInSeparateProcess]
559 if (!extension_loaded(
'intl')) {
560 $this->markTestSkipped(
'needs ext-intl');
562 setlocale(LC_ALL, [self::REAL_LOCALE]);
563 setlocale(LC_NUMERIC,
'C');
568 $this->assertSame(
'de_DE', \locale_get_default());
607 #[DataProvider('foreignChanges')]
626 setlocale(LC_MESSAGES,
'C');
640 yield
'the catalog of the build' => [
'build',
true];
641 yield
'garbage instead of a catalog' => [
'garbage',
false];
642 yield
'a catalog without header (not one of ours)' => [
'no-header',
false];
643 yield
'a domain without file' => [
'missing',
false];
646 #[DataProvider('catalogs')]
652 if ($kind !==
'build') {
653 $domain =
'other.' . $kind;
654 $directory = $this->root .
'/catalogs-' . $kind;
656 mkdir(dirname($file), 0775,
true);
657 if ($kind ===
'garbage') {
658 file_put_contents($file,
"this is no .mo file\n");
659 } elseif ($kind ===
'no-header') {
662 $entry->translate(
'Hallo');
663 $catalog->add($entry);
664 file_put_contents($file, $catalog->toMoString());
682 yield
'empty (would answer with the header)' => [
''];
683 yield
'longer than PHP accepts' => [
str_repeat(
'x', 4097)];
684 yield
'unknown' => [
'no_such_key'];
687 #[DataProvider('messageIdsThatCannotBeLookedUp')]
701 yield
'one' => [1,
'Eintrag'];
702 yield
'zero' => [0,
'Einträge'];
703 yield
'minus one counts like one' => [-1,
'Eintrag'];
704 yield
'minus five counts like five' => [-5,
'Einträge'];
705 yield
'the largest integer' => [PHP_INT_MAX,
'Einträge'];
706 yield
'the smallest integer (abs() would overflow)' => [PHP_INT_MIN,
'Einträge'];
709 #[DataProvider('quantities')]
trait RestoresProcessState
Whatever a test does to the process (locale, LANGUAGE/LC_*, LOCPATH, the static state of the runtime ...
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).
static buildDirectory(string $artifact_directory, string $build_id)
artifacts/language/<build>: one complete build, bound with bindtextdomain() as it is (the C library l...
static buildLocaleDirectory(string $build_directory)
The directory with the locale NativeGettext activates (NativeGettext::LOCALE_NAME): <build>/locale,...
NativeGettext changes process-wide state (LC_MESSAGES, LANGUAGE, LOCPATH, LC_ALL): what activate() le...
testRestoreChangesNothingInAThreadSafePhpInTheSystemLocaleBranch()
static foreignChangesInTheSystemLocaleBranch()
testReactivateIfLostRepairsWhatOtherCodeChangedInTheSystemLocaleBranch(\Closure $change)
static lcAllBeforeSystemLocaleActivation()
testSystemLocaleBranchTranslatesWithoutLocaleDirectoryAndLeavesLocpathAlone()
static processSettingsBeforeSystemLocaleActivation()
testRestoreWithoutActivationChangesNothing()
testReactivateIfLostDoesNothingWithoutAnActivationOrWhileActive()
testTranslatePluralSelectsTheFormOfTheAbsoluteQuantity(int $n, string $expected)
testTheIcuDefaultLocaleStaysTheOneOfTheAdministrator()
activateBuildInBranch(bool $system_locale)
testSystemLocaleBranchKeepsARealLocaleSetsLcAllToItAndRestoreGivesLcAllBack(string|false $lc_all)
static directoriesWithoutAUsableLocale()
testGlibcBranchNeverTouchesLcAll()
testActivateServesTheBuildAndRestoreGivesTheProcessStateBack(string|false $language, string|false $locpath)
testAfterRestoreReactivateIfLostActivatesTheLocaleAgain()
testReactivateIfLostRepairsAForeignResetInAThreadSafePhpToo(\Closure $change)
In a thread-safe PHP restore() does nothing, but another thread may reset what activate() set; the lo...
testTranslateAnswersTheMessageIdItself(string $message_id)
testReactivateIfLostKeepsALocaleOtherCodeSetForEverything(bool $system_locale)
static previousProcessSettings()
testReactivateIfLostRepairsWhatOtherCodeChanged(\Closure $change)
testRestoreChangesNothingInAThreadSafePhp()
testBindRefusesADirectoryThatDoesNotExist()
testReactivateIfLostFailsWithAProblemWhenTheLocaleIsGone()
testRestoreAfterARepeatedActivationGivesBackLcAllFromBeforeTheFirstOne(string|false $lc_all)
testRestoreLeavesASettingOtherCodeChangedSinceAsItIs()
testRestoreLeavesALcAllOtherCodeChangedSinceAsItIsInTheSystemLocaleBranch()
testActivateFailsWithAProblemAndLeavesTheProcessAsItWas(string $kind)
testActivateKeepsARealLocaleAndRestoreLeavesItWhileTakingLanguageBack(string|false $language, string|false $locpath)
testALocaleOfTheCFamilyIsNotRealSoTheOwnLocaleIsActivatedAndTakenBackAgain(string $locale)
testActivateStillFailsWithoutLocaleDirectoryWhenARealLocaleStays()
testSystemLocaleBranchSetsLcAllToTheActiveLocaleAndRestoreGivesItBackExactly(string|false $lc_all)
testSystemLocaleIsOnlyActiveAfterActivateAndRestoreGivesTheProcessStateBack(string $locale, string|false $language)
static messageIdsThatCannotBeLookedUp()
testTheOwnLocaleIsNeverTakenForARealOneAfterLanguageWasLost()
testIsLoadedTellsAReadableCatalogOfOurOwnByItsHeader(string $kind, bool $expected)
static localesThatAreNotReal()
static reactivateIfLost()
Activates the locale again if it was activated before and the settings changed since.
static translatePlural(string $domain, string $message_id, int $n)
The form of the plural message $message_id the "Plural-Forms" rule of the catalog selects for $n (a n...
static resetForTests()
Forgets the activation (without taking the settings back) - for tests only.
static activate(string $locale_directory)
Sets LANGUAGE and, if LC_MESSAGES holds no real locale, the locale LOCALE_NAME of $locale_directory (...
static isActive()
Whether activate() succeeded and its settings are still in place (other code, e.g.
static isThreadSafe()
Whether PHP is thread-safe (ZTS): the settings of activate() then apply to every thread and are not t...
static bind(string $domain, string $directory)
Binds $domain to $directory with the codeset UTF-8.
const string CATALOG_LANGUAGE
The value of LANGUAGE: the directory below a bound directory the catalogs are looked up in.
static useThreadSafeForTests(?bool $thread_safe)
Makes isThreadSafe() return $thread_safe (null: PHP_ZTS again) - for tests only.
static translate(string $domain, string $message_id)
The translation of $message_id in $domain, or $message_id itself if there is none (as the C library a...
const string CATALOG_DIRECTORY
static useSystemLocaleForTests(?bool $system_locale)
Makes usesSystemLocale() return $system_locale (null: by PHP_OS_FAMILY again) - for tests only.
static isLoaded(string $domain)
Whether the catalog of the bound $domain is readable: its header (the translation of the empty messag...
const array SYSTEM_LOCALES
LC_MESSAGES candidates where the system locale is used, see usesSystemLocale().
static restore()
Takes LANGUAGE, LC_MESSAGES (if activate() set it) and LC_ALL back to the state before the first acti...
const string PLURAL_CONTEXT
Fixture helpers for PO/MO migrated modules, built with the component's own gettext adapter (Translati...
static directory(string $prefix, string $path, bool $local=false)
static requireGlibcLocale(\PHPUnit\Framework\TestCase $test)
Skips $test without the glibc locale (macOS, BSD), otherwise makes NativeGettext use it.
static artifactDirectory()
static catalog(string $module, array $entries)
static writePo(string $file, TranslationCatalog $catalog)
static removeDirectory(string $directory)
static build(LanguageFileDirectoryManager $manager, string $ilias_absolute_path)
Builds the shipped .po files of $manager below $ilias_absolute_path into the artifact directory of re...
static resetRuntime(?string $artifact_directory=null)
Forgets the static state of the runtime read path and points build and runtime at $artifact_directory...