ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild Class Reference

Builds what native gettext serves for the migrated modules (see TranslationGettextRepository) from every shipped .po of every contributed LanguageFileDirectory; run by Setup's build (ShippedLanguageFilesCompiledObjective) only, never by the web server (it does not write artifacts/; "merge" only writes the shipped .po, see ShippedPoMerger). More...

+ Collaboration diagram for ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild:

Public Member Functions

 __construct (private readonly LanguageFileDirectoryManager $language_file_directory_manager, private readonly string $ilias_absolute_path, private readonly ShippedTranslations $shipped_translations=new ShippedTranslations(), private readonly ?string $locale_source=null)
 
 run (callable $inform)
 

Static Public Member Functions

static readPointer (string $artifact_directory)
 The pointer of this PHP's locale kind in $artifact_directory, null if it is missing or not valid. More...
 
static readPointerOf (string $artifact_directory, bool $system_locale)
 
static servesShippedPo (string $artifact_directory, string $module, string $lang_key, string $shipped_po)
 Whether the build of $artifact_directory serves exactly the shipped .po $shipped_po of $module for $lang_key (its content hash, see the manifest) - false without a build or after a change of the .po since (e.g. More...
 
static platformProblem (string $build_directory)
 Why a build made with a system locale (macOS/BSD) cannot be activated by a PHP needing the shipped one. More...
 

Private Member Functions

 runLocked (string $artifact_directory, callable $inform)
 
 write (string $artifact_directory, string $build_directory, string $build_id, array $modules, ?string $locale_source, array $former_builds)
 Writes the build $build_id into a temporary directory and renames that to $build_directory. More...
 
 formerCatalog (string $artifact_directory, array $former_builds, string $module, string $lang_key)
 The catalog of $module/$lang_key in the first former build that has it intact and compiled the same way (see catalogCompatibility()). More...
 
 templateKeys (?string $template, string $module, array &$warnings)
 The identifiers of $module in its template (.pot), [] if there is none or it cannot be read (reported). More...
 
 findCollisions (array $values_per_language)
 The identifiers with different values in several modules, per language. More...
 
 assertNativeGettextServes (string $build_directory, array $sample)
 Looks $sample up through native gettext in the build $build_directory. More...
 
 assertActivatedNativeGettextServes (string $build_directory, array $sample)
 
 findShippedFiles (callable $inform)
 Every shipped .po of every contributed directory with a module prefix (the main lang/ directory has none and holds no .po), per module. More...
 
 buildId (array $source_parts, ?string $locale_source)
 
 sourceParts (array $modules)
 
 codeFingerprint ()
 Over the code that determines the build, see FORMAT_VERSION. More...
 
 directoryFingerprint (string $directory)
 Over the content of every file below $directory - a locale copied from another version of the C library may not be loadable by the one in use. More...
 
 listFiles (string $directory)
 
 findLocaleSource ()
 
 copyLocale (string $source, string $target, string $build_directory, array &$files)
 
 isIntact (string $build_directory, array $manifest)
 Whether every file of $manifest is in $build_directory as it was written - without fsync(), a crash can leave empty or truncated files behind. More...
 
 writePointer (string $artifact_directory, array $pointer)
 
 removeObsolete (string $artifact_directory, array $keep)
 Removes everything of a former build of this locale kind except the builds $keep: other builds, temporary directories and files of aborted builds, and the former layout (<lang>/<module>.mo, index.json). More...
 
 holdsOnlyCatalogs (string $directory)
 Whether $directory (of the former layout <lang>/<module>.mo) holds nothing but catalogs and temporary files of them. More...
 
 removeTree (string $artifact_directory, string $path)
 Removes $path recursively - only below $artifact_directory, and without following a symbolic link (a link itself is removed). More...
 
 writeFile (string $file, string $content)
 
 ensureDirectory (string $directory)
 
 relativePath (string $file)
 

Static Private Member Functions

static catalogCompatibility ()
 Fingerprint of the compiling and cleaning code: formerCatalog() only takes over catalogs with the same one. More...
 
static classesFingerprint (array $classes)
 
static readManifest (string $build_directory)
 
static hashes (mixed $value)
 
static stringList (mixed $value)
 

Private Attributes

const int FORMAT_VERSION = 4
 Raise to rebuild when the output changes for a reason the fingerprint of the involved classes does not capture, e.g. More...
 
const array CATALOG_CLASSES
 The classes that determine the bytes of a compiled catalog (MigratedLanguageFileSync: moduleEntries()). More...
 
const string MANIFEST_FILE = 'manifest.json'
 
const string MARKUP_WARNING = 'Markup not allowed in a language value'
 
const string LOCK_FILE = 'build.lock'
 
const array LOCALE_SOURCES
 Where the C library's "C.utf8" locale is looked for (Debian/Ubuntu, Fedora/RHEL, Arch, ...). More...
 

Static Private Attributes

static array $manifests = []
 
static array $system_locale_platforms = []
 

Detailed Description

Builds what native gettext serves for the migrated modules (see TranslationGettextRepository) from every shipped .po of every contributed LanguageFileDirectory; run by Setup's build (ShippedLanguageFilesCompiledObjective) only, never by the web server (it does not write artifacts/; "merge" only writes the shipped .po, see ShippedPoMerger).

Requirement: on glibc the locale is copied from the C library of the build host and checked in that (CLI) process only. The web server must use the same C library (glibc with C.utf8) and have the PHP extension "gettext"; with separate CLI/web images or a musl based system (which ignores LOCPATH) the runtime serves -identifier- and reports why (see MigratedTranslations::getProblem()). macOS/BSD ship no locale.

Layout below artifacts/language/ (see MigratedLanguageFilePaths):

  • <build>/messages/LC_MESSAGES/<module>.<lang>.mo: the shipped state of a module, see ShippedTranslations::compile();
  • <build>/keys/<module>.php: all identifiers of the module's .po files and template, the same for every language, as a PHP file for the opcache;
  • <build>/locale/: the locale native gettext is activated with (see NativeGettext), a copy of "C.utf8" (glibc only);
  • <build>/manifest.json: size and hash of every file, to tell an intact build, the hash of each built .po (see servesShippedPo()), the warnings and collisions, whether it ships a locale and the building platform (see platformProblem());
  • current.json (glibc) and current-system.json (system locale, see NativeGettext::usesSystemLocale()): the build the runtime of that locale kind serves, its languages per module, every contributed module ("shipped") and a fingerprint of the built files ("sources") - read on every request, so nothing else. A build writes only the pointer of its kind and keeps the builds of the other one, so a build on the host (e.g. macOS) does not take a build of the web server (e.g. a glibc container) away.

The build id fingerprints everything that determines the build: the shipped files, the compiling code (see FORMAT_VERSION) and the locale (or the platform using a system locale). An intact build with the same id (its manifest also keeps its warnings) is reused, otherwise a new one is written into a temporary directory and renamed to its id. Either way it is checked with a real native gettext lookup before its pointer is switched atomically; running requests keep their build. The previous build is kept, older ones (and the files of the former layout <lang>/<module>.mo, index.json) are removed. Builds are serialized by build.lock.

A .po that cannot be compiled is reported and served with the catalog of the served (or else the previous) build, so a broken file never turns a working language into -identifier-; without such a catalog the module is not served for that language. Values with disallowed markup are compiled cleaned and reported; identifiers with different values in several modules (txt() serves the one loaded last) are listed, grouped by modules (collision_summary) when the build is written. All of that stays in the manifest until fixed.

Files are written without fsync(): a build can be repeated any time, and the manifest reveals a build damaged by a crash.

@phpstan-type Manifest array{files: array<string, array{0: int, 1: string}>, sample: array{module: string, lang: string, key: string, value: string}, languages: array<string, list<string>>, hashes: array<string, array<string, string>>, warnings: list<string>, collisions: list<string>, compatibility: string, locale: 'shipped'|'system', platform: string} @phpstan-type Pointer array{build: ?string, previous: ?string, modules: array<string, list<string>>, shipped: ?list<string>, sources: ?string}

Definition at line 73 of file ShippedTranslationsBuild.php.

Constructor & Destructor Documentation

◆ __construct()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::__construct ( private readonly LanguageFileDirectoryManager  $language_file_directory_manager,
private readonly string  $ilias_absolute_path,
private readonly ShippedTranslations  $shipped_translations = new ShippedTranslations(),
private readonly ?string  $locale_source = null 
)
Parameters
string | null$locale_sourcethe locale directory to ship; default: found in LOCALE_SOURCES

Definition at line 126 of file ShippedTranslationsBuild.php.

131 {
132 }

Member Function Documentation

◆ assertActivatedNativeGettextServes()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::assertActivatedNativeGettextServes ( string  $build_directory,
array  $sample 
)
private
Parameters
array{modulestring, lang: string, key: string, value: string} $sample

Definition at line 592 of file ShippedTranslationsBuild.php.

592 : void
593 {
595 throw new RuntimeException(sprintf('Native gettext is not available: %s.', (string) NativeGettext::getProblem()));
596 }
597 $domain = MigratedLanguageFilePaths::shippedDomain($sample['module'], $sample['lang']);
598 if (!NativeGettext::bind($domain, $build_directory)) {
599 throw new RuntimeException(sprintf('Native gettext cannot bind the domain "%s" to "%s".', $domain, $build_directory));
600 }
601 $value = NativeGettext::translate($domain, $sample['key']);
602 if ($value !== $sample['value']) {
603 throw new RuntimeException(sprintf(
604 'Native gettext does not translate: "%s" in the domain "%s" gave "%s" instead of "%s".',
605 $sample['key'],
606 $domain,
607 $value,
608 $sample['value']
609 ));
610 }
611 }
static buildLocaleDirectory(string $build_directory)
The directory with the locale NativeGettext activates (NativeGettext::LOCALE_NAME): <build>/locale,...
static shippedDomain(string $module, string $lang_key)
The gettext domain of the shipped state of $module for $lang_key: "<module>.<lang>".
static activate(string $locale_directory)
Sets LANGUAGE and, if LC_MESSAGES holds no real locale, the locale LOCALE_NAME of $locale_directory (...
static bind(string $domain, string $directory)
Binds $domain to $directory with the codeset UTF-8.
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...

References Vendor\Package\$sample.

◆ assertNativeGettextServes()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::assertNativeGettextServes ( string  $build_directory,
array  $sample 
)
private

Looks $sample up through native gettext in the build $build_directory.

Parameters
array{modulestring, lang: string, key: string, value: string} $sample
Exceptions
RuntimeExceptionif native gettext does not serve the expected value

Definition at line 575 of file ShippedTranslationsBuild.php.

575 : void
576 {
577 $messages = setlocale(LC_MESSAGES, '0');
578 // with "C" activate() loads the shipped locale instead of keeping a real one, so the check covers it
579 setlocale(LC_MESSAGES, 'C');
580 try {
581 $this->assertActivatedNativeGettextServes($build_directory, $sample);
582 } finally {
583 if ($messages !== false) {
584 setlocale(LC_MESSAGES, $messages);
585 }
586 }
587 }
assertActivatedNativeGettextServes(string $build_directory, array $sample)

References Vendor\Package\$sample.

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked().

+ Here is the caller graph for this function:

◆ buildId()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::buildId ( array  $source_parts,
?string  $locale_source 
)
private
Parameters
list<string>$source_parts see sourceParts()
string | null$locale_sourcenull: no locale is shipped, see NativeGettext::usesSystemLocale()

Definition at line 685 of file ShippedTranslationsBuild.php.

685 : string
686 {
687 $parts = [
688 (string) self::FORMAT_VERSION,
689 $this->codeFingerprint(),
690 $locale_source === null ? 'system-locale|' . PHP_OS_FAMILY : $this->directoryFingerprint($locale_source),
691 ...$source_parts,
692 ];
693
694 return 'b-' . substr(hash('sha256', implode("\n", $parts)), 0, 32);
695 }
codeFingerprint()
Over the code that determines the build, see FORMAT_VERSION.
directoryFingerprint(string $directory)
Over the content of every file below $directory - a locale copied from another version of the C libra...

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked().

+ Here is the caller graph for this function:

◆ catalogCompatibility()

static ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::catalogCompatibility ( )
staticprivate

Fingerprint of the compiling and cleaning code: formerCatalog() only takes over catalogs with the same one.

Definition at line 498 of file ShippedTranslationsBuild.php.

498 : string
499 {
500 return hash('sha256', self::FORMAT_VERSION . '|' . self::classesFingerprint(self::CATALOG_CLASSES));
501 }

◆ classesFingerprint()

static ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::classesFingerprint ( array  $classes)
staticprivate
Parameters
list<class-string>$classes

Definition at line 734 of file ShippedTranslationsBuild.php.

734 : string
735 {
736 $parts = [];
737 foreach ($classes as $class) {
738 $file = (new \ReflectionClass($class))->getFileName();
739 $parts[] = is_string($file) ? (string) @hash_file('sha256', $file) : '';
740 }
741
742 return hash('sha256', implode('|', $parts));
743 }

◆ codeFingerprint()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::codeFingerprint ( )
private

Over the code that determines the build, see FORMAT_VERSION.

Definition at line 721 of file ShippedTranslationsBuild.php.

721 : string
722 {
724 self::class,
725 MigratedLanguageFilePaths::class,
726 NativeGettext::class,
727 ...self::CATALOG_CLASSES,
728 ]);
729 }

◆ copyLocale()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::copyLocale ( string  $source,
string  $target,
string  $build_directory,
array &  $files 
)
private
Parameters
array<string,array{0int, 1: string}> $files the manifest, extended by the copied files

Definition at line 803 of file ShippedTranslationsBuild.php.

803 : void
804 {
805 foreach ($this->listFiles($source) as $relative => $file) {
806 $content = @file_get_contents($file);
807 if ($content === false) {
808 throw new RuntimeException(sprintf('Could not read "%s".', $file));
809 }
810 $this->ensureDirectory(dirname($target . '/' . $relative));
811 $this->writeFile($target . '/' . $relative, $content);
812 $files[substr($target . '/' . $relative, strlen($build_directory) + 1)] = [strlen($content), hash('sha256', $content)];
813 }
814 }

References $files, and ILIAS\Scripts\PHPStan\$target.

◆ directoryFingerprint()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::directoryFingerprint ( string  $directory)
private

Over the content of every file below $directory - a locale copied from another version of the C library may not be loadable by the one in use.

Definition at line 749 of file ShippedTranslationsBuild.php.

749 : string
750 {
751 $parts = [];
752 foreach ($this->listFiles($directory) as $relative => $file) {
753 $parts[] = $relative . '|' . (string) @hash_file('sha256', $file);
754 }
755
756 return hash('sha256', implode("\n", $parts));
757 }

◆ ensureDirectory()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::ensureDirectory ( string  $directory)
private

Definition at line 989 of file ShippedTranslationsBuild.php.

989 : void
990 {
991 if (!is_dir($directory) && !@mkdir($directory, 0755, true) && !is_dir($directory)) {
992 throw new RuntimeException(sprintf('Could not create the directory "%s".', $directory));
993 }
994 }

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\run().

+ Here is the caller graph for this function:

◆ findCollisions()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::findCollisions ( array  $values_per_language)
private

The identifiers with different values in several modules, per language.

Parameters
array<string,array<string,array<string,string>>>$values_per_language lang => module => identifier => value
Returns
array{0: list<string>, 1: list<string>} one line per identifier (for the manifest), and one per set of modules naming its identifiers (for the output)

Definition at line 533 of file ShippedTranslationsBuild.php.

533 : array
534 {
535 $collisions = [];
536 foreach ($values_per_language as $lang_key => $modules) {
537 $values_of = [];
538 foreach ($modules as $module => $values) {
539 foreach ($values as $identifier => $value) {
540 $values_of[(string) $identifier][$module] = $value;
541 }
542 }
543 foreach ($values_of as $identifier => $values) {
544 if (count(array_unique($values)) > 1) {
545 ksort($values, SORT_STRING);
546 $collisions[$identifier . "\0" . implode(', ', array_keys($values))][] = (string) $lang_key;
547 }
548 }
549 }
550 ksort($collisions, SORT_STRING);
551
552 $list = [];
553 $identifiers_of = [];
554 foreach ($collisions as $key => $lang_keys) {
555 [$identifier, $modules] = explode("\0", (string) $key, 2);
556 sort($lang_keys, SORT_STRING);
557 $list[] = sprintf('Key "%s" has different values in the modules %s (languages: %s) - txt() serves the one of the module loaded last.', $identifier, $modules, implode(', ', $lang_keys));
558 $identifiers_of[$modules][] = $identifier;
559 }
560 ksort($identifiers_of, SORT_STRING);
561 $summary = [];
562 foreach ($identifiers_of as $modules => $identifiers) {
563 $summary[] = sprintf('%d key(s) with different values in the modules %s - txt() serves the one of the module loaded last: %s', count($identifiers), $modules, implode(', ', $identifiers));
564 }
565
566 return [$list, $summary];
567 }
if(!isset($arguments[0])) if(count( $arguments) > 3) if(! $remove_from_lang &&isset($arguments[2]) &&!is_dir($arguments[2])) $module

References $module, and ILIAS\UI\examples\Symbol\Glyph\Sort\sort().

+ Here is the call graph for this function:

◆ findLocaleSource()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::findLocaleSource ( )
private
Exceptions
RuntimeExceptionif no locale to ship is found

Definition at line 786 of file ShippedTranslationsBuild.php.

786 : string
787 {
788 foreach ($this->locale_source !== null ? [$this->locale_source] : self::LOCALE_SOURCES as $candidate) {
789 if (is_file($candidate . '/LC_MESSAGES/SYS_LC_MESSAGES') || is_file($candidate . '/LC_CTYPE')) {
790 return $candidate;
791 }
792 }
793
794 throw new RuntimeException(sprintf(
795 'The locale "C.utf8" of the C library was not found (looked for %s) - it is needed to serve the language files with native gettext.',
796 implode(', ', $this->locale_source !== null ? [$this->locale_source] : self::LOCALE_SOURCES)
797 ));
798 }

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked().

+ Here is the caller graph for this function:

◆ findShippedFiles()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::findShippedFiles ( callable  $inform)
private

Every shipped .po of every contributed directory with a module prefix (the main lang/ directory has none and holds no .po), per module.

Parameters
callable(string)void $inform
Returns
array{0: array<string, array{directory: LanguageFileDirectory, languages: array<string, string>, template: ?string}>, 1: list<string>} the modules with .po files, sorted by module (languages: lang => shipped .po, sorted by language), and every contributed module, sorted

Definition at line 622 of file ShippedTranslationsBuild.php.

622 : array
623 {
624 $modules = [];
625 $contributed = [];
626 $matching_files = [];
627 $all_shipped_files = [];
628 foreach ($this->language_file_directory_manager->getDirectories() as $directory) {
629 $module = $directory->getPrefix();
630 if ($module === '') {
631 continue;
632 }
634 $inform(sprintf('WARNING: The language files of module "%s" are not compiled: the module name cannot be part of a file name.', $module));
635 continue;
636 }
637 if (isset($modules[$module])) {
638 $inform(sprintf('WARNING: Module "%s" is contributed twice - only "%s" is compiled.', $module, $modules[$module]['directory']->getPath()));
639 continue;
640 }
641 // also with an invalid file name pattern
642 $contributed[$module] = $module;
643 try {
645 $shipped_files = MigratedLanguageFilePaths::findShippedPoFiles($this->ilias_absolute_path, $directory);
646 $template = MigratedLanguageFilePaths::shippedTemplatePath($this->ilias_absolute_path, $directory);
647 } catch (\InvalidArgumentException $e) {
648 $inform(sprintf('WARNING: The language files of module "%s" are not compiled: %s', $module, $e->getMessage()));
649 continue;
650 }
651 if (!MigratedLanguageFilePaths::isFoundWithoutGraph($this->ilias_absolute_path, $directory)) {
652 $inform(sprintf(
653 'WARNING: Module "%s" has no template "components/<Vendor>/<Component>/lang/%s.pot" - without a build Setup and CLI do not recognise it as shipped.',
654 $module,
655 $module
656 ));
657 }
658 $base = rtrim($this->ilias_absolute_path, '/') . '/' . ltrim($directory->getPath(), '/');
659 $matching_files = [...$matching_files, ...(glob($base . sprintf($pattern, '*') . '.po') ?: [])];
660 $all_shipped_files = [...$all_shipped_files, ...array_values($shipped_files)];
661 if ($shipped_files === []) {
662 continue;
663 }
664 $modules[$module] = [
665 'directory' => $directory,
666 'languages' => $shipped_files,
667 'template' => is_file($template) ? $template : null,
668 ];
669 }
670 // files matching a pattern that yield no language key, e.g. "tos_pt_BR.po"; "chatroom_*.po" also
671 // matches "chatroom_adm_de.po" of another module in the same directory
672 foreach (array_unique(array_diff($matching_files, $all_shipped_files)) as $unknown) {
673 $inform(sprintf('WARNING: "%s" is not compiled: its name contains no valid language key.', $this->relativePath($unknown)));
674 }
675 ksort($modules, SORT_STRING);
676 sort($contributed, SORT_STRING);
677
678 return [$modules, $contributed];
679 }
static findShippedPoFiles(string $ilias_absolute_path, LanguageFileDirectory $directory)
The shipped .po files of $directory by language key, found via its file name pattern (see shippedFile...
static isFoundWithoutGraph(string $ilias_absolute_path, LanguageFileDirectory $directory)
Whether findShippedTemplates() finds $directory's module.
static shippedFileNamePattern(LanguageFileDirectory $directory)
The name of $directory's shipped .po without ".po", with "%s" for the language key: its own pattern (...
static shippedTemplatePath(string $ilias_absolute_path, LanguageFileDirectory $directory)
The template (.pot) next to the shipped .po files, see templateFileName().

References Vendor\Package\$e, $module, $pattern, and ILIAS\UI\examples\Symbol\Glyph\Sort\sort().

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ formerCatalog()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::formerCatalog ( string  $artifact_directory,
array  $former_builds,
string  $module,
string  $lang_key 
)
private

The catalog of $module/$lang_key in the first former build that has it intact and compiled the same way (see catalogCompatibility()).

The bytes are taken over, not recompiled: reading a .mo back would shift plural forms (see TranslationCatalog::fromMoString()). Values are checked against the markup policy again.

Parameters
list<string>$former_builds build ids
Returns
array{mo: string, values: array<string, string>, build: string, hash: string}|null values: see ShippedTranslations::compileCatalog(); hash: of the .po it was compiled from

Definition at line 445 of file ShippedTranslationsBuild.php.

445 : ?array
446 {
447 $policy = new TranslationMarkupPolicy();
448 foreach ($former_builds as $former_build) {
449 $build_directory = MigratedLanguageFilePaths::buildDirectory($artifact_directory, $former_build);
450 $manifest = self::readManifest($build_directory);
451 if ($manifest === null || $manifest['compatibility'] !== self::catalogCompatibility()) {
452 continue;
453 }
454 $file = MigratedLanguageFilePaths::buildMoFile($build_directory, $module, $lang_key);
455 [$size, $hash] = $manifest['files'][substr($file, strlen($build_directory) + 1)] ?? [null, null];
456 $resolved_file = realpath($file);
457 $resolved_directory = realpath($build_directory);
458 $mo = $size === null
459 || $resolved_file === false
460 || $resolved_directory === false
461 || !str_starts_with($resolved_file, $resolved_directory . '/')
462 ? false
463 : @file_get_contents($resolved_file);
464 if ($mo === false || strlen($mo) !== $size || hash('sha256', $mo) !== $hash) {
465 continue;
466 }
467 try {
468 $catalog = TranslationCatalog::fromMoString($mo);
469 } catch (RuntimeException) {
470 continue;
471 }
472 $values = [];
473 foreach ($catalog->getEntries() as $entry) {
474 foreach ([$entry->getTranslation(), ...$entry->getPluralTranslations()] as $value) {
475 if (!$policy->isAllowed($value)) {
476 continue 3;
477 }
478 }
479 if ($entry->getContext() === null && !$entry->isPlural() && $entry->getTranslation() !== '') {
480 $values[$entry->getId()] = $entry->getTranslation();
481 }
482 }
483
484 return [
485 'mo' => $mo,
486 'values' => $values,
487 'build' => $former_build,
488 'hash' => $manifest['hashes'][$module][$lang_key] ?? '',
489 ];
490 }
491
492 return null;
493 }
static fromMoString(string $content)
Reads a .mo of toMoString() for reading only: the library drops empty plural forms and forms "0",...
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 buildMoFile(string $build_directory, string $module, string $lang_key)
The compiled catalog of $module for $lang_key: <build>/messages/LC_MESSAGES/<module>.

References $module, and $policy.

◆ hashes()

static ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::hashes ( mixed  $value)
staticprivate
Returns
array<string, array<string, string>> module => language => hash

Definition at line 1006 of file ShippedTranslationsBuild.php.

1006 : array
1007 {
1008 $hashes = [];
1009 foreach (is_array($value) ? $value : [] as $module => $languages) {
1010 foreach (is_array($languages) ? $languages : [] as $lang_key => $hash) {
1011 if (is_string($module) && is_string($lang_key) && is_string($hash)) {
1012 $hashes[$module][$lang_key] = $hash;
1013 }
1014 }
1015 }
1016
1017 return $hashes;
1018 }

References $module.

◆ holdsOnlyCatalogs()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::holdsOnlyCatalogs ( string  $directory)
private

Whether $directory (of the former layout <lang>/<module>.mo) holds nothing but catalogs and temporary files of them.

Definition at line 947 of file ShippedTranslationsBuild.php.

947 : bool
948 {
949 foreach (scandir($directory) ?: [] as $name) {
950 if ($name !== '.' && $name !== '..' && (!str_contains($name, '.mo') || !is_file($directory . '/' . $name))) {
951 return false;
952 }
953 }
954
955 return true;
956 }

◆ isIntact()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::isIntact ( string  $build_directory,
array  $manifest 
)
private

Whether every file of $manifest is in $build_directory as it was written - without fsync(), a crash can leave empty or truncated files behind.

Parameters
array{filesarray<string, array{0: int, 1: string}>} $manifest

Definition at line 874 of file ShippedTranslationsBuild.php.

874 : bool
875 {
876 foreach ($manifest['files'] as $relative => [$size, $hash]) {
877 $file = $build_directory . '/' . $relative;
878 clearstatcache(true, $file);
879 if (
880 str_contains($relative, '..')
881 || is_link($file)
882 || !is_file($file)
883 || @filesize($file) !== $size
884 || @hash_file('sha256', $file) !== $hash
885 ) {
886 return false;
887 }
888 }
889
890 return $manifest['files'] !== [];
891 }

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked().

+ Here is the caller graph for this function:

◆ listFiles()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::listFiles ( string  $directory)
private
Returns
array<string, string> path relative to $directory => absolute path, sorted

Definition at line 762 of file ShippedTranslationsBuild.php.

762 : array
763 {
764 $files = [];
765 foreach (scandir($directory) ?: [] as $name) {
766 if ($name === '.' || $name === '..') {
767 continue;
768 }
769 $path = $directory . '/' . $name;
770 if (is_dir($path)) {
771 foreach ($this->listFiles($path) as $relative => $file) {
772 $files[$name . '/' . $relative] = $file;
773 }
774 } elseif (is_file($path)) {
775 $files[$name] = $path;
776 }
777 }
778 ksort($files, SORT_STRING);
779
780 return $files;
781 }

References $files.

◆ platformProblem()

static ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::platformProblem ( string  $build_directory)
static

Why a build made with a system locale (macOS/BSD) cannot be activated by a PHP needing the shipped one.

Definition at line 1045 of file ShippedTranslationsBuild.php.

1045 : ?string
1046 {
1047 if (!array_key_exists($build_directory, self::$system_locale_platforms)) {
1048 $manifest = self::$manifests[$build_directory] ?? self::readManifest($build_directory);
1049 self::$system_locale_platforms[$build_directory] = $manifest !== null && $manifest['locale'] === 'system'
1050 ? ($manifest['platform'] === '' ? 'another platform' : $manifest['platform'])
1051 : null;
1052 }
1053 $platform = self::$system_locale_platforms[$build_directory];
1054 if ($platform === null || NativeGettext::usesSystemLocale()) {
1055 return null;
1056 }
1057
1058 return sprintf(
1059 'the build was made on %s with a system locale, run "php cli/setup.php build" on the platform of the web server',
1060 $platform
1061 );
1062 }
static usesSystemLocale()
macOS/BSD: activate() uses a SYSTEM_LOCALES entry, their C library ignores LOCPATH.

Referenced by ILIAS\Language\ComponentTranslation\TranslationGettextRepository\state(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuildTest\testPlatformProblemIsNullForAShippedLocaleBuildAndForAManifestOfAFormerVersion(), and ILIAS\Language\ComponentTranslation\ShippedTranslationsBuildTest\testPlatformProblemTellsASystemLocaleBuildOnlyWhereTheShippedLocaleIsNeeded().

+ Here is the caller graph for this function:

◆ readManifest()

static ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::readManifest ( string  $build_directory)
staticprivate
Returns
Manifest|null

Definition at line 819 of file ShippedTranslationsBuild.php.

819 : ?array
820 {
821 $content = is_link($build_directory) ? false : @file_get_contents($build_directory . '/' . self::MANIFEST_FILE);
822 $data = $content === false ? null : json_decode($content, true);
823 $sample = $data['sample'] ?? null;
824 if (
825 !is_array($data)
826 || !is_array($data['files'] ?? null)
827 || !is_array($sample)
828 || !is_string($sample['module'] ?? null)
829 || !is_string($sample['lang'] ?? null)
830 || !is_string($sample['key'] ?? null)
831 || !is_string($sample['value'] ?? null)
834 ) {
835 return null;
836 }
837 $files = [];
838 foreach ($data['files'] as $file => $details) {
839 if (!is_string($file) || !is_array($details) || !is_int($details[0] ?? null) || !is_string($details[1] ?? null)) {
840 return null;
841 }
842 $files[$file] = [$details[0], $details[1]];
843 }
844 $languages = [];
845 foreach (is_array($data['languages'] ?? null) ? $data['languages'] : [] as $module => $lang_keys) {
846 if (!is_string($module) || !MigratedLanguageFilePaths::isValidModule($module) || !is_array($lang_keys)) {
847 return null;
848 }
849 $languages[$module] = array_values(array_filter(
850 $lang_keys,
851 static fn($lang_key): bool => is_string($lang_key) && MigratedLanguageFilePaths::isValidLanguageKey($lang_key)
852 ));
853 }
854
855 return [
856 'files' => $files,
857 'sample' => $sample,
858 'languages' => $languages,
859 'warnings' => self::stringList($data['warnings'] ?? []),
860 'hashes' => self::hashes($data['hashes'] ?? []),
861 'collisions' => self::stringList($data['collisions'] ?? []),
862 'compatibility' => is_string($data['compatibility'] ?? null) ? $data['compatibility'] : '',
863 'locale' => ($data['locale'] ?? null) === 'system' ? 'system' : 'shipped',
864 'platform' => is_string($data['platform'] ?? null) ? $data['platform'] : '',
865 ];
866 }
$data
Definition: storeScorm.php:45

References $data, $files, $module, and Vendor\Package\$sample.

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked().

+ Here is the caller graph for this function:

◆ readPointer()

static ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::readPointer ( string  $artifact_directory)
static

The pointer of this PHP's locale kind in $artifact_directory, null if it is missing or not valid.

Returns
Pointer|null modules: module => the languages it is built for; shipped: null in a pointer written before it was recorded

Definition at line 254 of file ShippedTranslationsBuild.php.

254 : ?array
255 {
256 return self::readPointerOf($artifact_directory, NativeGettext::usesSystemLocale());
257 }
static readPointerOf(string $artifact_directory, bool $system_locale)

References ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\readPointerOf(), and ILIAS\Language\ComponentTranslation\NativeGettext\usesSystemLocale().

Referenced by ILIAS\Language\ComponentTranslation\MigratedTranslations\pointer(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuildTest\testEveryContributedModuleIsRecordedAlsoWithoutAnyPoAndWithoutABuild(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuildTest\testOnlyTheCurrentAndThePreviousBuildStayAndUnrelatedFilesAreLeftAlone(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuildTest\testReadPointerTakesOnlyValidModulesOfTheShippedList(), and ILIAS\Language\ComponentTranslation\ShippedTranslationsBuildTest\testRebuildingAnEarlierStateSwitchesBackWithoutLosingThePreviousBuild().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ readPointerOf()

static ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::readPointerOf ( string  $artifact_directory,
bool  $system_locale 
)
static
Returns
Pointer|null sources: fingerprint of the shipped files built, null in a pointer written before it was recorded or without a build

Definition at line 263 of file ShippedTranslationsBuild.php.

263 : ?array
264 {
265 $content = @file_get_contents(MigratedLanguageFilePaths::buildPointerFileOf($artifact_directory, $system_locale));
266 $data = $content === false ? null : json_decode($content, true);
267 if (!is_array($data) || !is_array($data['modules'] ?? null)) {
268 return null;
269 }
270 $build = $data['build'] ?? null;
271 $previous = $data['previous'] ?? null;
272 if (
273 ($build !== null && (!is_string($build) || !MigratedLanguageFilePaths::isBuildId($build)))
274 || ($previous !== null && (!is_string($previous) || !MigratedLanguageFilePaths::isBuildId($previous)))
275 ) {
276 return null;
277 }
278
279 $modules = [];
280 foreach ($data['modules'] as $module => $languages) {
281 if (!is_string($module) || !MigratedLanguageFilePaths::isValidModule($module) || !is_array($languages)) {
282 return null;
283 }
284 $modules[$module] = array_values(array_filter(
285 $languages,
286 static fn($lang_key): bool => is_string($lang_key) && MigratedLanguageFilePaths::isValidLanguageKey($lang_key)
287 ));
288 }
289
290 // an invalid list only loses itself: the runtime falls back to the templates
291 $shipped = is_array($data['shipped'] ?? null) && array_is_list($data['shipped']) ? $data['shipped'] : null;
292 if ($shipped !== null) {
293 $shipped = array_values(array_filter(
294 $shipped,
295 static fn($module): bool => is_string($module) && MigratedLanguageFilePaths::isValidModule($module)
296 ));
297 }
298
299 return [
300 'build' => $build,
301 'previous' => $previous,
302 'modules' => $modules,
303 'shipped' => $shipped,
304 'sources' => is_string($data['sources'] ?? null) ? $data['sources'] : null,
305 ];
306 }
static buildPointerFileOf(string $artifact_directory, bool $system_locale)
The pointer of the builds made with ($system_locale) or without a system locale.

References $data, $module, ILIAS\Language\ComponentTranslation\MigratedLanguageFilePaths\buildPointerFileOf(), ILIAS\Language\ComponentTranslation\MigratedLanguageFilePaths\isBuildId(), ILIAS\Language\ComponentTranslation\MigratedLanguageFilePaths\isValidLanguageKey(), and ILIAS\Language\ComponentTranslation\MigratedLanguageFilePaths\isValidModule().

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\readPointer(), ILIAS\Language\ComponentTranslation\MigratedTranslations\reportNewerOtherBuild(), and ILIAS\Language\ComponentTranslation\ShippedTranslationsBuildTest\testBuildsOfTheOtherLocaleKindAndTheirPointerSurviveTheCleanup().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ relativePath()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::relativePath ( string  $file)
private

Definition at line 996 of file ShippedTranslationsBuild.php.

996 : string
997 {
998 $root = rtrim($this->ilias_absolute_path, '/') . '/';
999
1000 return str_starts_with($file, $root) ? substr($file, strlen($root)) : $file;
1001 }

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked().

+ Here is the caller graph for this function:

◆ removeObsolete()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::removeObsolete ( string  $artifact_directory,
array  $keep 
)
private

Removes everything of a former build of this locale kind except the builds $keep: other builds, temporary directories and files of aborted builds, and the former layout (<lang>/<module>.mo, index.json).

Anything else stays, also everything of the other kind (whose build may run at the same time where the lock does not work, e.g. across a Docker Desktop mount).

Parameters
list<string>$keep build ids
Returns
int the number of removed entries

Definition at line 917 of file ShippedTranslationsBuild.php.

917 : int
918 {
919 $removed = 0;
920 $system_locale = NativeGettext::usesSystemLocale();
921 $pointer_name = basename(MigratedLanguageFilePaths::buildPointerFile($artifact_directory));
922 foreach (scandir($artifact_directory) ?: [] as $name) {
923 $path = $artifact_directory . '/' . $name;
924 $obsolete = match (true) {
925 $name === '.' || $name === '..' || in_array($name, $keep, true) => false,
927 preg_match($system_locale ? '/\A\.b-[0-9a-f]+\.system\.[0-9a-f]+\z/' : '/\A\.b-[0-9a-f]+\.[0-9a-f]+\z/', $name) === 1,
928 str_starts_with($name, '.' . $pointer_name . '.'),
929 $name === 'index.json',
930 str_starts_with($name, '.index.json.') => true,
931 preg_match('/\A[a-z]{2}\z/', $name) === 1 => is_dir($path) && !is_link($path) && $this->holdsOnlyCatalogs($path),
932 default => false,
933 };
934 if ($obsolete) {
935 $this->removeTree($artifact_directory, $path);
936 $removed++;
937 }
938 }
939
940 return $removed;
941 }
static buildPointerFile(string $artifact_directory)
artifacts/language/current.json (current-system.json where NativeGettext::usesSystemLocale()): names ...
removeTree(string $artifact_directory, string $path)
Removes $path recursively - only below $artifact_directory, and without following a symbolic link (a ...
holdsOnlyCatalogs(string $directory)
Whether $directory (of the former layout <lang>/<module>.mo) holds nothing but catalogs and temporary...

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked().

+ Here is the caller graph for this function:

◆ removeTree()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::removeTree ( string  $artifact_directory,
string  $path 
)
private

Removes $path recursively - only below $artifact_directory, and without following a symbolic link (a link itself is removed).

Definition at line 962 of file ShippedTranslationsBuild.php.

962 : void
963 {
964 if (!str_starts_with($path, rtrim($artifact_directory, '/') . '/') || str_contains($path, '/../')) {
965 return;
966 }
967 if (is_link($path) || is_file($path)) {
968 @unlink($path);
969 return;
970 }
971 if (!is_dir($path)) {
972 return;
973 }
974 foreach (scandir($path) ?: [] as $name) {
975 if ($name !== '.' && $name !== '..') {
976 $this->removeTree($artifact_directory, $path . '/' . $name);
977 }
978 }
979 @rmdir($path);
980 }

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked().

+ Here is the caller graph for this function:

◆ run()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::run ( callable  $inform)
Parameters
callable(string$message): void $inform progress and warnings
Returns
array{build: ?string, compiled: int, unchanged: bool, rebuilt: bool, removed: int, warnings: list<string>, collisions: list<string>, collision_summary: list<string>, manifest: ?string, markup_warnings: int, other_warnings: list<string>} build: the id now served (null without any migrated module); compiled: .po files compiled (0 for an unchanged build); rebuilt: the build was written now, not reused; removed: files and directories of former builds; collision_summary: the collisions grouped by modules, only if rebuilt; manifest: the manifest of the served build, relative to the ILIAS directory; markup_warnings: how many of the warnings are about disallowed markup; other_warnings: the rest
Exceptions
RuntimeExceptionif the build cannot be written, the locale is missing, or native gettext does not serve it - the pointer is left unchanged then

Definition at line 145 of file ShippedTranslationsBuild.php.

145 : array
146 {
147 $artifact_directory = MigratedLanguageFilePaths::shippedArtifactDirectory($this->ilias_absolute_path);
148 $this->ensureDirectory($artifact_directory);
149
150 // one build at a time (e.g. two Setup runs)
151 $lock = @fopen($artifact_directory . '/' . self::LOCK_FILE, 'c');
152 if ($lock === false || !flock($lock, LOCK_EX)) {
153 throw new RuntimeException(sprintf('Could not lock "%s".', $artifact_directory . '/' . self::LOCK_FILE));
154 }
155 try {
156 return $this->runLocked($artifact_directory, $inform);
157 } finally {
158 flock($lock, LOCK_UN);
159 fclose($lock);
160 }
161 }
static shippedArtifactDirectory(string $ilias_absolute_path)
The directory Setup's build writes into (see ShippedTranslationsBuild), in the untracked artifacts/.

References ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\ensureDirectory(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked(), and ILIAS\Language\ComponentTranslation\MigratedLanguageFilePaths\shippedArtifactDirectory().

+ Here is the call graph for this function:

◆ runLocked()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::runLocked ( string  $artifact_directory,
callable  $inform 
)
private
Parameters
callable(string$message): void $inform
Returns
array{build: ?string, compiled: int, unchanged: bool, rebuilt: bool, removed: int, warnings: list<string>, collisions: list<string>, collision_summary: list<string>, manifest: ?string, markup_warnings: int, other_warnings: list<string>}

Definition at line 167 of file ShippedTranslationsBuild.php.

167 : array
168 {
169 $pointer = self::readPointer($artifact_directory);
170 [$modules, $shipped] = $this->findShippedFiles($inform);
171
172 if ($modules === []) {
173 $this->writePointer($artifact_directory, ['build' => null, 'previous' => null, 'modules' => [], 'shipped' => $shipped, 'sources' => null]);
174 return [
175 'build' => null,
176 'compiled' => 0,
177 'unchanged' => ($pointer['build'] ?? null) === null,
178 'rebuilt' => false,
179 'removed' => $this->removeObsolete($artifact_directory, []),
180 'warnings' => [],
181 'collisions' => [],
182 'collision_summary' => [],
183 'manifest' => null,
184 'markup_warnings' => 0,
185 'other_warnings' => [],
186 ];
187 }
188
189 $locale_source = NativeGettext::usesSystemLocale() ? null : $this->findLocaleSource();
190 $source_parts = $this->sourceParts($modules);
191 $build_id = $this->buildId($source_parts, $locale_source);
192 $build_directory = MigratedLanguageFilePaths::buildDirectory($artifact_directory, $build_id);
193 $manifest = self::readManifest($build_directory);
194 $compiled = 0;
195 $collision_summary = null;
196 if ($manifest === null || !$this->isIntact($build_directory, $manifest)) {
197 $former_builds = array_values(array_diff(
198 array_filter([$pointer['build'] ?? null, $pointer['previous'] ?? null], 'is_string'),
199 [$build_id]
200 ));
201 [$manifest, $compiled, $collision_summary] = $this->write($artifact_directory, $build_directory, $build_id, $modules, $locale_source, $former_builds);
202 }
203
204 try {
205 $this->assertNativeGettextServes($build_directory, $manifest['sample']);
206 } catch (RuntimeException $e) {
207 if (!in_array($build_id, [$pointer['build'] ?? null, $pointer['previous'] ?? null], true)) {
208 $this->removeTree($artifact_directory, $build_directory);
209 }
210 throw $e;
211 }
212
213 $unchanged = ($pointer['build'] ?? null) === $build_id;
214 $previous = ($pointer['build'] ?? null) !== null && !$unchanged
215 ? $pointer['build']
216 : ($pointer['previous'] ?? null);
217 $new_pointer = [
218 'build' => $build_id,
219 'previous' => $previous,
220 'modules' => $manifest['languages'],
221 'shipped' => $shipped,
222 'sources' => hash('sha256', implode("\n", $source_parts)),
223 ];
224 if ($new_pointer !== $pointer) {
225 $this->writePointer($artifact_directory, $new_pointer);
226 }
227
228 $other_warnings = array_values(array_filter(
229 $manifest['warnings'],
230 static fn(string $warning): bool => !str_starts_with($warning, self::MARKUP_WARNING)
231 ));
232
233 return [
234 'build' => $build_id,
235 'compiled' => $compiled,
236 'unchanged' => $unchanged && $compiled === 0,
237 'rebuilt' => $collision_summary !== null,
238 'removed' => $this->removeObsolete($artifact_directory, array_values(array_filter([$build_id, $previous]))),
239 'warnings' => $manifest['warnings'],
240 'collisions' => $manifest['collisions'],
241 'collision_summary' => $collision_summary ?? [],
242 'manifest' => $this->relativePath($build_directory . '/' . self::MANIFEST_FILE),
243 'markup_warnings' => count($manifest['warnings']) - count($other_warnings),
244 'other_warnings' => $other_warnings,
245 ];
246 }
isIntact(string $build_directory, array $manifest)
Whether every file of $manifest is in $build_directory as it was written - without fsync(),...
removeObsolete(string $artifact_directory, array $keep)
Removes everything of a former build of this locale kind except the builds $keep: other builds,...
write(string $artifact_directory, string $build_directory, string $build_id, array $modules, ?string $locale_source, array $former_builds)
Writes the build $build_id into a temporary directory and renames that to $build_directory.
static readPointer(string $artifact_directory)
The pointer of this PHP's locale kind in $artifact_directory, null if it is missing or not valid.
findShippedFiles(callable $inform)
Every shipped .po of every contributed directory with a module prefix (the main lang/ directory has n...
assertNativeGettextServes(string $build_directory, array $sample)
Looks $sample up through native gettext in the build $build_directory.

References Vendor\Package\$e, ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\assertNativeGettextServes(), ILIAS\Language\ComponentTranslation\MigratedLanguageFilePaths\buildDirectory(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\buildId(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\findLocaleSource(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\findShippedFiles(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\isIntact(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\readManifest(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\readPointer(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\relativePath(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\removeObsolete(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\removeTree(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\sourceParts(), ILIAS\Language\ComponentTranslation\NativeGettext\usesSystemLocale(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\write(), and ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\writePointer().

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\run().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ servesShippedPo()

static ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::servesShippedPo ( string  $artifact_directory,
string  $module,
string  $lang_key,
string  $shipped_po 
)
static

Whether the build of $artifact_directory serves exactly the shipped .po $shipped_po of $module for $lang_key (its content hash, see the manifest) - false without a build or after a change of the .po since (e.g.

by "merge", see ShippedPoMerger).

Definition at line 1025 of file ShippedTranslationsBuild.php.

1025 : bool
1026 {
1027 $build = self::readPointer($artifact_directory)['build'] ?? null;
1028 if ($build === null) {
1029 return false;
1030 }
1031 $build_directory = MigratedLanguageFilePaths::buildDirectory($artifact_directory, $build);
1032 if (!isset(self::$manifests[$build_directory])) {
1033 $manifest = self::readManifest($build_directory);
1034 if ($manifest === null) {
1035 return false;
1036 }
1037 self::$manifests[$build_directory] = $manifest;
1038 }
1039 $hash = self::$manifests[$build_directory]['hashes'][$module][$lang_key] ?? null;
1040
1041 return $hash !== null && $hash === @hash_file('sha256', $shipped_po);
1042 }

References $module.

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuildTest\testServesShippedPoIsFalseWhenTheManifestOfTheCurrentBuildHasNoHashes(), ILIAS\Language\ComponentTranslation\ShippedTranslationsBuildTest\testServesShippedPoIsFalseWhenTheManifestOfTheCurrentBuildIsGone(), and ILIAS\Language\ComponentTranslation\ShippedTranslationsBuildTest\testServesShippedPoIsTrueOnlyForThePoTheBuildWasMadeFrom().

+ Here is the caller graph for this function:

◆ sourceParts()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::sourceParts ( array  $modules)
private
Parameters
array<string,array{directoryLanguageFileDirectory, languages: array<string, string>, template: ?string}> $modules
Returns
list<string>

Definition at line 701 of file ShippedTranslationsBuild.php.

701 : array
702 {
703 $parts = [];
704 foreach ($modules as $module => $shipped) {
705 $files = $shipped['languages'];
706 if ($shipped['template'] !== null) {
707 $files['pot'] = $shipped['template'];
708 }
709 foreach ($files as $lang_key => $file) {
710 $hash = @hash_file('sha256', $file);
711 $parts[] = $module . '|' . $lang_key . '|' . $this->relativePath($file) . '|' . ($hash === false ? 'unreadable' : $hash);
712 }
713 }
714
715 return $parts;
716 }

References $files, and $module.

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked().

+ Here is the caller graph for this function:

◆ stringList()

static ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::stringList ( mixed  $value)
staticprivate
Returns
list<string>

Definition at line 1067 of file ShippedTranslationsBuild.php.

1067 : array
1068 {
1069 return is_array($value) ? array_values(array_filter($value, 'is_string')) : [];
1070 }

◆ templateKeys()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::templateKeys ( ?string  $template,
string  $module,
array &  $warnings 
)
private

The identifiers of $module in its template (.pot), [] if there is none or it cannot be read (reported).

Parameters
list<string>$warnings
Returns
array<string, true>

Definition at line 510 of file ShippedTranslationsBuild.php.

510 : array
511 {
512 if ($template === null) {
513 return [];
514 }
515 try {
516 return array_fill_keys(
517 array_map('strval', array_keys(MigratedLanguageFileSync::moduleEntries(TranslationCatalog::fromPoFile($template), $module))),
518 true
519 );
520 } catch (RuntimeException $e) {
521 $warnings[] = sprintf('The template "%s" cannot be read: %s', $this->relativePath($template), $e->getMessage());
522 return [];
523 }
524 }
static moduleEntries(TranslationCatalog $catalog, string $module)
The entries of $catalog that belong to $module: those with the module as context (msgctxt) and those ...

References Vendor\Package\$e, and $module.

◆ write()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::write ( string  $artifact_directory,
string  $build_directory,
string  $build_id,
array  $modules,
?string  $locale_source,
array  $former_builds 
)
private

Writes the build $build_id into a temporary directory and renames that to $build_directory.

Parameters
array<string,array{directoryLanguageFileDirectory, languages: array<string, string>, template: ?string}> $modules
list<string>$former_builds where the catalog of a .po that cannot be compiled is taken from
Returns
array{0: Manifest, 1: int, 2: list<string>} the manifest of the build, the number of compiled .po files and the collisions grouped by modules (see findCollisions())

Definition at line 316 of file ShippedTranslationsBuild.php.

323 : array {
324 $temporary = $artifact_directory . '/.' . $build_id . ($locale_source === null ? '.system.' : '.') . bin2hex(random_bytes(6));
325 $this->ensureDirectory($temporary . '/' . NativeGettext::CATALOG_DIRECTORY);
326 $this->ensureDirectory($temporary . '/keys');
327 try {
328 $files = [];
329 $warnings = [];
330 $languages = [];
331 $values_per_language = [];
332 $sample = null;
333 $hashes = [];
334 $compiled = 0;
335 foreach ($modules as $module => $shipped) {
336 $keys = $this->templateKeys($shipped['template'], $module, $warnings);
337 foreach ($shipped['languages'] as $lang_key => $shipped_po) {
338 $source = $this->relativePath($shipped_po);
339 $po_hash = (string) @hash_file('sha256', $shipped_po);
340 try {
341 $catalog = $this->shipped_translations->compile(
342 $shipped_po,
343 $module,
344 static function (string $identifier, array $violations) use (&$warnings, $source, $module): void {
345 $warnings[] = sprintf('%s, %s (module "%s", key "%s"): removed %s', self::MARKUP_WARNING, $source, $module, $identifier, implode(', ', $violations));
346 },
347 static function (string $message) use (&$warnings, $source, $module): void {
348 $warnings[] = sprintf('%s (module "%s"): %s', $source, $module, $message);
349 }
350 );
351 $compiled++;
352 } catch (\Throwable $t) {
353 $former = $this->formerCatalog($artifact_directory, $former_builds, $module, $lang_key);
354 if ($former === null) {
355 $warnings[] = sprintf(
356 'Could not compile "%s", module "%s" is not served for "%s" until the file is fixed (no former build has it): %s',
357 $source,
358 $module,
359 $lang_key,
360 $t->getMessage()
361 );
362 continue;
363 }
364 $warnings[] = sprintf(
365 'Could not compile "%s", module "%s" is served for "%s" as in build %s until the file is fixed: %s',
366 $source,
367 $module,
368 $lang_key,
369 $former['build'],
370 $t->getMessage()
371 );
372 $catalog = $former;
373 // the build does not serve this .po, see servesShippedPo()
374 $po_hash = $former['hash'];
375 }
376 $file = MigratedLanguageFilePaths::buildMoFile($temporary, $module, $lang_key);
377 $this->writeFile($file, $catalog['mo']);
378 $files[substr($file, strlen($temporary) + 1)] = [strlen($catalog['mo']), hash('sha256', $catalog['mo'])];
379 $languages[$module][] = $lang_key;
380 $hashes[$module][$lang_key] = $po_hash;
381 $keys += array_fill_keys(array_map('strval', array_keys($catalog['values'])), true);
382 $values_per_language[$lang_key][$module] = $catalog['values'];
383 foreach ($catalog['values'] as $identifier => $value) {
384 if ($sample === null && $value !== (string) $identifier) {
385 $sample = ['module' => $module, 'lang' => $lang_key, 'key' => (string) $identifier, 'value' => $value];
386 }
387 }
388 }
389 if (!isset($languages[$module])) {
390 continue;
391 }
392 ksort($keys, SORT_STRING);
393 $content = "<?php\n\n// Generated by the ILIAS build (ShippedTranslationsBuild) - do not edit.\n\nreturn "
394 . var_export(array_fill_keys(array_map('strval', array_keys($keys)), $module), true) . ";\n";
396 $this->writeFile($file, $content);
397 $files[substr($file, strlen($temporary) + 1)] = [strlen($content), hash('sha256', $content)];
398 }
399 if ($sample === null) {
400 throw new RuntimeException('No migrated module has a single translated value - nothing to check native gettext with.');
401 }
402
403 if ($locale_source !== null) {
404 $this->copyLocale($locale_source, MigratedLanguageFilePaths::buildLocaleDirectory($temporary) . '/' . NativeGettext::LOCALE_NAME, $temporary, $files);
405 }
406 ksort($files, SORT_STRING);
407 [$collisions, $collision_summary] = $this->findCollisions($values_per_language);
408 $manifest = [
409 'files' => $files,
410 'sample' => $sample,
411 'languages' => $languages,
412 'hashes' => $hashes,
413 'warnings' => $warnings,
414 'collisions' => $collisions,
415 'compatibility' => self::catalogCompatibility(),
416 'locale' => $locale_source === null ? 'system' : 'shipped',
417 'platform' => PHP_OS_FAMILY,
418 ];
419 $this->writeFile($temporary . '/' . self::MANIFEST_FILE, json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . "\n");
420
421 // a damaged build of the same id (see isIntact()) is replaced
422 $this->removeTree($artifact_directory, $build_directory);
423 if (!@rename($temporary, $build_directory)) {
424 throw new RuntimeException(sprintf('Could not rename "%s" to "%s".', $temporary, $build_directory));
425 }
426 } catch (\Throwable $t) {
427 $this->removeTree($artifact_directory, $temporary);
428 throw $t instanceof RuntimeException ? $t : new RuntimeException($t->getMessage(), 0, $t);
429 }
430
431 return [$manifest, $compiled, $collision_summary];
432 }
static buildKeysFile(string $build_directory, string $module)
The identifiers of $module in the build, the same for every language: <build>/keys/<module>....
findCollisions(array $values_per_language)
The identifiers with different values in several modules, per language.
templateKeys(?string $template, string $module, array &$warnings)
The identifiers of $module in its template (.pot), [] if there is none or it cannot be read (reported...
formerCatalog(string $artifact_directory, array $former_builds, string $module, string $lang_key)
The catalog of $module/$lang_key in the first former build that has it intact and compiled the same w...
copyLocale(string $source, string $target, string $build_directory, array &$files)
static catalogCompatibility()
Fingerprint of the compiling and cleaning code: formerCatalog() only takes over catalogs with the sam...

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked().

+ Here is the caller graph for this function:

◆ writeFile()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::writeFile ( string  $file,
string  $content 
)
private

Definition at line 982 of file ShippedTranslationsBuild.php.

982 : void
983 {
984 if (@file_put_contents($file, $content) !== strlen($content)) {
985 throw new RuntimeException(sprintf('Could not write "%s".', $file));
986 }
987 }

◆ writePointer()

ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::writePointer ( string  $artifact_directory,
array  $pointer 
)
private
Parameters
Pointer$pointer

Definition at line 896 of file ShippedTranslationsBuild.php.

896 : void
897 {
900 json_encode(
901 $pointer,
902 JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR
903 ) . "\n",
904 $artifact_directory
905 );
906 }
static write(string $file, string $content, ?string $confine_to_directory=null, bool $flush_to_disk=true)

Referenced by ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild\runLocked().

+ Here is the caller graph for this function:

Field Documentation

◆ $manifests

array ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::$manifests = []
staticprivate

Definition at line 104 of file ShippedTranslationsBuild.php.

◆ $system_locale_platforms

array ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::$system_locale_platforms = []
staticprivate

Definition at line 111 of file ShippedTranslationsBuild.php.

◆ CATALOG_CLASSES

const array ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::CATALOG_CLASSES
private
Initial value:
= [
ShippedTranslations::class,
TranslationMarkupPolicy::class,
TranslationCatalog::class,
TranslationEntry::class,
PluralForms::class,
PluralFormKey::class,
MigratedLanguageFileSync::class,
]

The classes that determine the bytes of a compiled catalog (MigratedLanguageFileSync: moduleEntries()).

Definition at line 84 of file ShippedTranslationsBuild.php.

◆ FORMAT_VERSION

const int ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::FORMAT_VERSION = 4
private

Raise to rebuild when the output changes for a reason the fingerprint of the involved classes does not capture, e.g.

an update of the gettext library.

Definition at line 79 of file ShippedTranslationsBuild.php.

◆ LOCALE_SOURCES

const array ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::LOCALE_SOURCES
private
Initial value:
= [
'/usr/lib/locale/C.utf8',
'/usr/lib/locale/C.UTF-8',
'/usr/lib64/locale/C.utf8',
'/usr/lib64/locale/C.UTF-8',
]

Where the C library's "C.utf8" locale is looked for (Debian/Ubuntu, Fedora/RHEL, Arch, ...).

Definition at line 116 of file ShippedTranslationsBuild.php.

◆ LOCK_FILE

const string ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::LOCK_FILE = 'build.lock'
private

Definition at line 97 of file ShippedTranslationsBuild.php.

◆ MANIFEST_FILE

const string ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::MANIFEST_FILE = 'manifest.json'
private

Definition at line 94 of file ShippedTranslationsBuild.php.

◆ MARKUP_WARNING

const string ILIAS\Language\ComponentTranslation\ShippedTranslationsBuild::MARKUP_WARNING = 'Markup not allowed in a language value'
private

Definition at line 95 of file ShippedTranslationsBuild.php.


The documentation for this class was generated from the following file: