19declare(strict_types=1);
87 string $ilias_absolute_path,
89 ?
string $client_data_dir,
90 string $backup_directory,
91 ?\Closure $after_module =
null
96 'invalid_markup' => [],
98 'unwritten_database' => [],
99 'unmarked_overlay' => [],
101 if ($client_data_dir ===
null) {
108 ksort($shipped_files, SORT_STRING);
109 foreach ($shipped_files as
$module => $shipped_po) {
114 if ($backup_names[
$module] ===
null) {
115 $result[
'skipped'][
$module] = sprintf(
'The shipped file name "%s" is not unique among the modules, it cannot be backed up.', basename($shipped_po));
119 $language_file_directory_manager,
123 static function () use ($language_file_directory_manager, $ilias_absolute_path, $lang_key,
$module, $shipped_po, $client_data_dir, $backup_directory, $backup_names, $after_module): array {
127 $language_file_directory_manager,
130 static fn(): array => self::mergeModule(
131 $language_file_directory_manager,
132 $ilias_absolute_path,
137 rtrim($backup_directory,
'/') .
'/' . $backup_names[
$module]
140 }
catch (RuntimeException|\InvalidArgumentException
$e) {
141 return [
'skipped' =>
$e->getMessage()];
143 if ($outcome[
'written'] && $after_module !==
null) {
146 $after_module(
$module, $outcome[
'merged'], $outcome[
'merged_remarks']);
147 }
catch (\Throwable $t) {
148 $outcome[
'database_error'] = $t->getMessage();
156 if (isset($outcome[
'skipped'])) {
157 $result[
'skipped'][
$module] = $outcome[
'skipped'];
160 if ($outcome[
'invalid_markup'] !== []) {
161 $result[
'invalid_markup'][
$module] = $outcome[
'invalid_markup'];
163 if ($outcome[
'not_merged'] !== []) {
164 $result[
'not_merged'][
$module] = $outcome[
'not_merged'];
166 if ($outcome[
'written']) {
167 $result[
'written'][
$module] = $shipped_po;
169 if (isset($outcome[
'database_error'])) {
170 $result[
'unwritten_database'][
$module] = $outcome[
'database_error'];
172 if ($outcome[
'unmarked'] !==
null) {
173 $result[
'unmarked_overlay'][
$module] = $outcome[
'unmarked'];
192 string $ilias_absolute_path,
195 string $confine_to_directory
197 $shipped_files =
MigratedLanguageFileSync::findShippedModuleFiles($language_file_directory_manager, $ilias_absolute_path, $lang_key);
199 foreach (self::backupFileNames($shipped_files) as
$module => $name) {
201 if ($name ===
null) {
202 throw new RuntimeException(
'file name not unique');
204 self::backup($shipped_files[
$module], rtrim($directory,
'/') .
'/' . $name, $confine_to_directory);
205 }
catch (RuntimeException) {
224 string $ilias_absolute_path,
228 $shipped_files =
MigratedLanguageFileSync::findShippedModuleFiles($language_file_directory_manager, $ilias_absolute_path, $lang_key);
229 $result = [
'changes' => [],
'without_backup' => []];
230 foreach (self::backupFileNames($shipped_files) as
$module => $name) {
232 $backup = $name ===
null ? null : rtrim($directory,
'/') .
'/' . $name;
234 if ($backup ===
null || !is_file($backup) || !is_readable($backup)) {
235 throw new RuntimeException(
'no backup');
237 $former = self::valuesOf(MigratedLanguageFileSync::loadShippedModuleEntries($backup,
$module));
238 $current = self::valuesOf(MigratedLanguageFileSync::loadShippedModuleEntries($shipped_files[
$module],
$module));
239 }
catch (RuntimeException) {
240 $result[
'without_backup'][] =
$module;
243 $changes = array_diff_assoc($current, $former);
244 if ($changes !== []) {
245 $result[
'changes'][
$module] = $changes;
261 string $ilias_absolute_path,
265 string $client_data_dir,
271 'merged_remarks' => [],
272 'invalid_markup' => [],
277 $overlay = MigratedLanguageFileSync::readOverlayCatalog($language_file_directory_manager, $lang_key,
$module, $client_data_dir, $ilias_absolute_path);
278 $directory = MigratedLanguageFileSync::findDirectory($language_file_directory_manager,
$module);
279 if ($overlay ===
null || $directory ===
null) {
282 $shipped = TranslationCatalog::fromPoFile($shipped_po);
284 $overlay = MigratedLanguageFileSync::withPluralRuleOf($overlay, $shipped,
$module);
285 $overlay_entries = MigratedLanguageFileSync::moduleEntries($overlay,
$module);
288 self::assertWritableFile($shipped_po, $ilias_absolute_path);
289 self::assertWritableFile($backup_file, $ilias_absolute_path);
290 $template_file = MigratedLanguageFilePaths::shippedTemplatePath($ilias_absolute_path, $directory);
292 foreach ($overlay_entries as $identifier => $entry) {
294 !MigratedLanguageFileSync::isRemarkOnly($entry)
295 && PluralFormKey::parse((
string) $identifier) ===
null
296 && MigratedLanguageFileSync::findModuleEntry($shipped,
$module, (
string) $identifier) ===
null
298 if (!is_file($template_file)) {
299 throw new RuntimeException(sprintf(
'The template "%s" for the entries added locally is missing.', $template_file));
301 self::assertWritableFile($template_file, $ilias_absolute_path);
302 $template = TranslationCatalog::fromPoFile($template_file);
307 $plural_forms = MigratedLanguageFileSync::pluralFormsOf($shipped);
308 $policy =
new TranslationMarkupPolicy();
309 $template_changed =
false;
310 foreach ($overlay_entries as $identifier => $overlay_entry) {
311 $identifier = (string) $identifier;
312 $shipped_entry = MigratedLanguageFileSync::findModuleEntry($shipped,
$module, $identifier);
313 $remark = LocalChangeComments::getRemark($overlay_entry);
314 $has_value = !MigratedLanguageFileSync::isRemarkOnly($overlay_entry);
318 $message = $shipped_entry ===
null && PluralFormKey::parse($identifier) !==
null
320 : self::mergedMessage($overlay_entry, $shipped_entry, $template,
$module, $plural_forms);
322 $outcome[
'not_merged'][] = $identifier;
326 $shipped_values = $shipped_entry ===
null ? [] : MigratedLanguageFileSync::flatValuesOf($shipped_entry, $plural_forms);
327 $invalid =
$policy->findInvalidChangedValues(self::flatValues($identifier,
$message), $shipped_values, $shipped_values);
328 if ($invalid !== []) {
329 $outcome[
'invalid_markup'] += $invalid;
333 $target = $shipped_entry ??
new TranslationEntry(
null, $identifier);
334 if (
$message[
'plural_id'] ===
null) {
340 if ($shipped_entry ===
null) {
341 $shipped->addInIdOrder(
$target);
342 $template_changed = self::addToTemplate($template,
$module, $identifier,
$message[
'plural_id']) || $template_changed;
344 $outcome[
'merged'][] = $identifier;
348 if ($remark !==
null && $remark !==
'') {
349 if ($shipped_entry ===
null) {
354 $notes = array_map(
'trim', MigratedLanguageFileSync::shippedNotesOf($shipped_entry));
355 if (!in_array(trim(TranslationEntry::plainComment($remark)), $notes,
true)) {
356 $shipped_entry->addTranslatorNote($remark);
358 $outcome[
'merged_remarks'][] = $identifier;
362 if ($outcome[
'merged'] === [] && $outcome[
'merged_remarks'] === []) {
366 $po_content = self::checkedPoContent($shipped,
true);
367 $template_content = $template_changed && $template !==
null ? self::checkedPoContent($template,
false) : null;
368 $previous_content = @file_get_contents($shipped_po);
369 if ($previous_content ===
false) {
370 throw new RuntimeException(sprintf(
'Could not read "%s".', $shipped_po));
372 if ($po_content === $previous_content && $template_content ===
null) {
375 $outcome[
'unmarked'] = self::markMerged($language_file_directory_manager, $lang_key,
$module, $outcome[
'merged'], $client_data_dir, $ilias_absolute_path);
376 $outcome[
'merged'] = [];
377 $outcome[
'merged_remarks'] = [];
381 self::backup($shipped_po, $backup_file, $ilias_absolute_path, $previous_content);
382 if ($template_content !==
null) {
384 AtomicFileWriter::write($template_file, $template_content, $ilias_absolute_path);
386 AtomicFileWriter::write($shipped_po, $po_content, $ilias_absolute_path);
387 $outcome[
'written'] =
true;
389 $outcome[
'unmarked'] = self::markMerged($language_file_directory_manager, $lang_key,
$module, $outcome[
'merged'], $client_data_dir, $ilias_absolute_path);
406 string $client_data_dir,
407 string $ilias_absolute_path
411 $language_file_directory_manager,
418 }
catch (RuntimeException|\InvalidArgumentException
$e) {
419 return $e->getMessage();
440 if (!$overlay_entry->isPlural() && !$shipped_entry?->isPlural()) {
441 return [
'plural_id' =>
null,
'forms' => [$overlay_entry->
getTranslation()]];
443 if ($overlay_entry->isPlural() && $shipped_entry !==
null && !$shipped_entry->isPlural()) {
447 if ($overlay_entry->isPlural()) {
448 $forms = array_values(MigratedLanguageFileSync::flatValuesOf($overlay_entry, $plural_forms));
450 $forms = array_values(MigratedLanguageFileSync::flatValuesOf($shipped_entry, $plural_forms));
451 $forms[$plural_forms->defaultFormIndex()] = $overlay_entry->getTranslation();
453 if ($forms[0] ===
'' && implode(
'', $forms) !==
'') {
456 $template_entry = $template ===
null ? null : MigratedLanguageFileSync::findModuleEntry($template,
$module, $overlay_entry->getId());
459 'plural_id' => $shipped_entry?->getPluralId()
460 ?? $template_entry?->getPluralId()
461 ?? $overlay_entry->getId() .
'_plural',
474 if ($template ===
null || MigratedLanguageFileSync::findModuleEntry($template,
$module, $identifier) !==
null) {
478 if ($plural_id !==
null) {
479 $entry->setPlural($plural_id, [
'',
'']);
492 if (
$message[
'plural_id'] ===
null) {
493 return [$identifier =>
$message[
'forms'][0]];
496 foreach (
$message[
'forms'] as $form => $value) {
497 $values[PluralFormKey::of($identifier, $form)] = $value;
512 if (!mb_check_encoding($content,
'UTF-8')) {
513 throw new RuntimeException(
'The merged file would not be valid UTF-8.');
515 TranslationCatalog::fromPoString($content);
529 private static function backup(
string $shipped_po,
string $backup_file,
string $confine_to_directory, ?
string $content =
null): void
531 $content ??= @file_get_contents($shipped_po);
532 if ($content ===
false) {
533 throw new RuntimeException(sprintf(
'Could not read "%s".', $shipped_po));
535 if (!is_dir(dirname($backup_file))) {
536 throw new RuntimeException(sprintf(
'The backup directory "%s" does not exist.', dirname($backup_file)));
538 AtomicFileWriter::write($backup_file, $content, $confine_to_directory);
549 $root = realpath($confine_to_directory);
550 $directory = realpath(dirname($file));
553 || $directory ===
false
554 || ($directory !== $root && !str_starts_with($directory, rtrim($root,
'/') .
'/'))
555 || !is_dir(dirname($file))
556 || !is_writable(dirname($file))
558 || (file_exists($file) && (!is_file($file) || !is_writable($file)))
560 throw new RuntimeException(sprintf(
'"%s" is not writable or does not resolve to a path below "%s".', $file, $confine_to_directory));
573 $counts = array_count_values(array_map(
'basename', $shipped_files));
575 foreach ($shipped_files as
$module => $file) {
576 $names[(string)
$module] = $counts[basename($file)] === 1 ? basename($file) :
null;
586 private static function valuesOf(array $entries): array
588 return array_map(
static fn(array $entry):
string => $entry[
'value'], $entries);
An in-memory gettext catalog (header block plus messages) and the only place of the component that re...
addInIdOrder(TranslationEntry $entry)
add() at the position of $entry's id: before the first entry whose id sorts after it (strcmp(),...
toMoString()
Deterministic: the same catalog always compiles to the same bytes, so an existing ....
One message of a TranslationCatalog (a msgctxt/msgid/msgstr block of a .po file).
Reads the shipped .po of a migrated module and maintains its per-installation overlay: the ....
static withTemplateLock(LanguageFileDirectoryManager $language_file_directory_manager, string $module, ?string $client_data_dir, \Closure $callback)
Runs $callback while holding the lock of $module's template (.pot, see MigratedLanguageFilePaths::tem...
static findShippedModuleFiles(LanguageFileDirectoryManager $language_file_directory_manager, string $ilias_absolute_path, string $lang_key)
Every module migrated for $lang_key (its shipped .po exists, parseable or not) with the path of that ...
static hasOverlay(LanguageFileDirectoryManager $language_file_directory_manager, string $lang_key, string $module, ?string $client_data_dir)
Whether $module/$lang_key has an overlay (anything at the place of its .po or current),...
static withOverlayLock(?LanguageFileDirectoryManager $language_file_directory_manager, string $lang_key, string $module, ?string $client_data_dir, \Closure $callback, ?string $ilias_absolute_path=null)
Runs $callback while holding an exclusive flock() on the lock file of the overlay of $module/$lang_ke...
The "merge" maintenance action of developer mode (LANGMODE) for PO-maintained modules,...
static assertWritableFile(string $file, string $confine_to_directory)
static findShippedChangesSinceBackup(LanguageFileDirectoryManager $language_file_directory_manager, string $ilias_absolute_path, string $lang_key, string $directory)
What the shipped .po of every migrated module changed since its backup in $directory (see backupShipp...
static valuesOf(array $entries)
static markMerged(LanguageFileDirectoryManager $language_file_directory_manager, string $lang_key, string $module, array $identifiers, string $client_data_dir, string $ilias_absolute_path)
MigratedLanguageFileSync::markMergedIntoShipped() for the files just written (or written by an earlie...
static merge(LanguageFileDirectoryManager $language_file_directory_manager, string $ilias_absolute_path, string $lang_key, ?string $client_data_dir, string $backup_directory, ?\Closure $after_module=null)
static backupShippedPoFiles(LanguageFileDirectoryManager $language_file_directory_manager, string $ilias_absolute_path, string $lang_key, string $directory, string $confine_to_directory)
Copies the shipped .po of every module migrated for $lang_key into $directory under its shipped file ...
static checkedPoContent(TranslationCatalog $catalog, bool $compile)
$catalog as .po content, after making sure it can be read back and, for a language's ....
static flatValues(string $identifier, array $message)
static backup(string $shipped_po, string $backup_file, string $confine_to_directory, ?string $content=null)
Copies $shipped_po (or $content) to $backup_file atomically, confined to $confine_to_directory (see A...
static mergeModule(LanguageFileDirectoryManager $language_file_directory_manager, string $ilias_absolute_path, string $lang_key, string $module, string $shipped_po, string $client_data_dir, string $backup_file)
merge() for one module, under its overlay lock and the template lock of the module.
static addToTemplate(?TranslationCatalog $template, string $module, string $identifier, ?string $plural_id)
Adds the locally added entry $identifier to the template without msgstr (a plural message with two em...
static mergedMessage(TranslationEntry $overlay_entry, ?TranslationEntry $shipped_entry, ?TranslationCatalog $template, string $module, PluralForms $plural_forms)
The message $overlay_entry makes of its shipped entry: a singular value, or every form of a plural me...
static backupFileNames(array $shipped_files)
The backup name of each shipped .po of $shipped_files (module => path): its file name,...
if(!isset($arguments[0])) if(count( $arguments) > 3) if(! $remove_from_lang &&isset($arguments[2]) &&!is_dir($arguments[2])) $module
if(!file_exists('../ilias.ini.php'))