ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
TranslationGettextRepository.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
44{
50 private static array $build_keys = [];
51
57 private static array $states = [];
58
62 private const int MAX_REVISION_FILE_SIZE = 128;
63
64 public function getKeys(string $module, string $lang_key, ?string $client_data_dir): ?array
65 {
66 return self::state($module, $lang_key, $client_data_dir)['keys'] ?? null;
67 }
68
69 public function getText(string $module, string $lang_key, string $key, ?string $client_data_dir): ?string
70 {
71 $state = self::servedState($module, $lang_key, $key, $client_data_dir);
72 if ($state === null) {
73 return null;
74 }
75 if ($state['overlay_domain'] !== null && isset($state['overlay_keys'][$key])) {
76 // listed in the overlay: a value equal to the identifier is a value as well
77 $value = self::lookup($state['overlay_domain'], $key);
78 return $value === false ? null : $value ?? $key;
79 }
80
81 $value = self::lookup($state['domain'], $key);
82 if ($value !== null) {
83 return $value === false ? null : $value;
84 }
85
86 return self::lookup($state['domain'], ShippedTranslations::IDENTITY_CONTEXT . "\x04" . $key) === '1' ? $key : null;
87 }
88
92 public function getPluralText(string $module, string $lang_key, string $key, int $n, ?string $client_data_dir): ?string
93 {
94 $state = self::servedState($module, $lang_key, $key, $client_data_dir);
95 if ($state === null) {
96 return null;
97 }
98 $domain = $state['overlay_domain'] !== null && isset($state['overlay_keys'][$key])
99 ? $state['overlay_domain']
100 : $state['domain'];
101 $message_id = ShippedTranslations::PLURAL_CONTEXT . "\x04" . $key;
102 $value = NativeGettext::translatePlural($domain, $message_id, $n);
103 if ($value === $message_id && !NativeGettext::isActive()) {
106 return null;
107 }
108 $value = NativeGettext::translatePlural($domain, $message_id, $n);
109 }
110
111 return $value === $message_id ? null : $value;
112 }
113
117 public function invalidateModule(string $module, string $lang_key): void
118 {
119 unset(self::$states[$module . '|' . $lang_key]);
120 }
121
125 public function invalidateLanguage(string $lang_key): void
126 {
127 foreach (array_keys(self::$states) as $cache_key) {
128 if (str_ends_with((string) $cache_key, '|' . $lang_key)) {
129 unset(self::$states[$cache_key]);
130 }
131 }
132 }
133
137 public function invalidateAll(): void
138 {
139 self::$build_keys = [];
140 self::$states = [];
141 }
142
148 private static function servedState(string $module, string $lang_key, string $key, ?string $client_data_dir): ?array
149 {
150 $state = self::state($module, $lang_key, $client_data_dir);
151
152 return $state !== null && $state['ok'] && isset($state['keys'][$key]) ? $state : null;
153 }
154
158 private static function state(string $module, string $lang_key, ?string $client_data_dir): ?array
159 {
160 $cache_key = $module . '|' . $lang_key;
161 if (array_key_exists($cache_key, self::$states)) {
162 $state = self::$states[$cache_key];
163 // the client data directory became known later in the request (e.g. Setup)
164 if ($state !== null && $state['ok'] && !$state['overlay_checked'] && $client_data_dir !== null) {
165 $state = self::$states[$cache_key] = self::withOverlay($state, $client_data_dir, $module, $lang_key);
166 }
167 return $state;
168 }
170 if (
171 $pointer === null
172 || $pointer['build'] === null
175 || !in_array($lang_key, $pointer['modules'][$module] ?? [], true)
176 ) {
177 return self::$states[$cache_key] = null;
178 }
179
182 $keys = self::buildKeys($build_directory, $module);
183 $state = ['keys' => $keys ?? [], 'ok' => false, 'domain' => $domain, 'overlay_domain' => null, 'overlay_keys' => [], 'overlay_checked' => false];
184 if ($keys === null) {
186 'The identifiers of module "%s" cannot be read from "%s" - run "php cli/setup.php build".',
187 $module,
189 ));
193 ?? (string) NativeGettext::getProblem()
194 );
195 } elseif (!NativeGettext::bind($domain, $build_directory) || !NativeGettext::isLoaded($domain)) {
197 'Native gettext cannot read "%s" - run "php cli/setup.php build".',
198 MigratedLanguageFilePaths::buildMoFile($build_directory, $module, $lang_key)
199 ), true);
200 } else {
201 $state['ok'] = true;
202 if ($client_data_dir !== null) {
203 $state = self::withOverlay($state, $client_data_dir, $module, $lang_key);
204 }
205 }
206
207 return self::$states[$cache_key] = $state;
208 }
209
216 private static function withOverlay(array $state, string $client_data_dir, string $module, string $lang_key): array
217 {
218 $state['overlay_checked'] = true;
219 $overlay_directory = MigratedLanguageFilePaths::overlayDirectory($client_data_dir, $module, $lang_key);
220 $current_file = MigratedLanguageFilePaths::overlayCurrentFile($overlay_directory);
221 if (!file_exists($current_file) && !is_link($current_file)) {
222 return $state;
223 }
224 // like the write path: no symbolic link below the overlay root is followed
225 if (self::hasSymbolicLink([dirname($overlay_directory), $overlay_directory, $current_file])) {
226 MigratedTranslations::reportProblem(sprintf('The overlay "%s" contains a symbolic link - its local changes are not served.', $overlay_directory));
227 return $state;
228 }
229 $revision = self::readStart($current_file, self::MAX_REVISION_FILE_SIZE);
230 $revision = $revision === null ? '' : trim($revision);
232 MigratedTranslations::reportProblem(sprintf('The overlay "%s" names no valid revision - its local changes are not served.', $current_file));
233 return $state;
234 }
235 $revision_directory = MigratedLanguageFilePaths::overlayRevisionDirectory($overlay_directory, $revision);
236 $keys_file = MigratedLanguageFilePaths::overlayKeysFile($revision_directory);
237 $mo_file = MigratedLanguageFilePaths::overlayMoFile($revision_directory, $module, $lang_key);
238 if (self::hasSymbolicLink([$revision_directory, dirname($mo_file, 2), dirname($mo_file), $mo_file, $keys_file])) {
239 MigratedTranslations::reportProblem(sprintf('The overlay revision "%s" contains a symbolic link - its local changes are not served.', $revision_directory));
240 return $state;
241 }
242 $content = @file_get_contents($keys_file);
243 $keys = $content === false ? null : json_decode($content, true);
244 if (!is_array($keys) || !array_is_list($keys) || array_filter($keys, 'is_string') !== $keys) {
245 MigratedTranslations::reportProblem(sprintf('The overlay revision "%s" has no valid "keys.json" - its local changes are not served.', $revision_directory));
246 return $state;
247 }
249 if (!NativeGettext::bind($domain, $revision_directory) || !NativeGettext::isLoaded($domain)) {
251 'Native gettext cannot read "%s" - its local changes are not served.',
252 MigratedLanguageFilePaths::overlayMoFile($revision_directory, $module, $lang_key)
253 ));
254 return $state;
255 }
256
257 $state['overlay_domain'] = $domain;
258 $state['overlay_keys'] = array_fill_keys($keys, true);
259 $state['keys'] += array_fill_keys($keys, $module);
260
261 return $state;
262 }
263
269 private static function lookup(string $domain, string $message_id): string|false|null
270 {
271 $value = NativeGettext::translate($domain, $message_id);
272 if ($value !== $message_id) {
273 return $value;
274 }
278 return false;
279 }
280 $value = NativeGettext::translate($domain, $message_id);
281 }
282
283 return $value === $message_id ? null : $value;
284 }
285
286 private static function reportLostActivation(): void
287 {
288 MigratedTranslations::reportActivationProblem(NativeGettext::getProblem() ?? 'LC_MESSAGES or LANGUAGE were changed and it cannot be activated again');
289 }
290
294 private static function hasSymbolicLink(array $paths): bool
295 {
296 foreach ($paths as $path) {
297 if (is_link($path)) {
298 return true;
299 }
300 }
301
302 return false;
303 }
304
308 private static function readStart(string $file, int $length): ?string
309 {
310 $handle = is_file($file) ? @fopen($file, 'rb') : false;
311 if ($handle === false) {
312 return null;
313 }
314 try {
315 $content = fread($handle, $length);
316 return $content === false ? null : $content;
317 } finally {
318 fclose($handle);
319 }
320 }
321
325 private static function buildKeys(string $build_directory, string $module): ?array
326 {
327 if (!array_key_exists($module, self::$build_keys)) {
328 $file = MigratedLanguageFilePaths::buildKeysFile($build_directory, $module);
329 $keys = is_file($file) ? (static fn(string $file): mixed => include $file)($file) : null;
330 self::$build_keys[$module] = is_array($keys) ? $keys : null;
331 }
332
333 return self::$build_keys[$module];
334 }
335}
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 overlayRevisionDirectory(string $overlay_directory, string $revision)
One compiled revision of the overlay in $overlay_directory: r-<hash>/, bound with bindtextdomain() as...
static buildLocaleDirectory(string $build_directory)
The directory with the locale NativeGettext activates (NativeGettext::LOCALE_NAME): <build>/locale,...
static buildMoFile(string $build_directory, string $module, string $lang_key)
The compiled catalog of $module for $lang_key: <build>/messages/LC_MESSAGES/<module>.
static overlayMoFile(string $revision_directory, string $module, string $lang_key)
The compiled overlay of $module for $lang_key in $revision_directory: r-<hash>/messages/LC_MESSAGES/<...
static overlayDomain(string $module, string $lang_key)
The gettext domain of the overlay of $module for $lang_key: "<module>.<lang>.overlay".
static overlayDirectory(string $client_data_dir, string $module, string $lang_key)
The overlay of $module for $lang_key lives in <client data dir>/lang/<module>/<lang>/: the ....
static overlayKeysFile(string $revision_directory)
The identifiers the overlay revision $revision_directory has a value for: r-<hash>/keys....
static buildKeysFile(string $build_directory, string $module)
The identifiers of $module in the build, the same for every language: <build>/keys/<module>....
static shippedDomain(string $module, string $lang_key)
The gettext domain of the shipped state of $module for $lang_key: "<module>.<lang>".
static overlayCurrentFile(string $overlay_directory)
The file naming the revision (see overlayRevisionDirectory()) of the overlay in $overlay_directory th...
static reportProblem(string $message, bool $fatal=false)
Records a problem (logged once per request, see getProblem()).
static reportActivationProblem(string $reason)
Native gettext cannot be activated (first use or later): one message per request, whatever the reason...
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 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 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...
static isLoaded(string $domain)
Whether the catalog of the bound $domain is readable: its header (the translation of the empty messag...
static platformProblem(string $build_directory)
Why a build made with a system locale (macOS/BSD) cannot be activated by a PHP needing the shipped on...
TranslationRepository over the build (ShippedTranslationsBuild) with the overlay of local changes (Mi...
static state(string $module, string $lang_key, ?string $client_data_dir)
invalidateModule(string $module, string $lang_key)
Forgets what was read for $module/$lang_key, so a local change just written is served.
static readStart(string $file, int $length)
At most the first $length bytes of $file, null if it cannot be read.
static lookup(string $domain, string $message_id)
The translation of $message_id in.
getPluralText(string $module, string $lang_key, string $key, int $n, ?string $client_data_dir)
Also null if the overlay holds a singular value for $key or where getText() returns null.
static withOverlay(array $state, string $client_data_dir, string $module, string $lang_key)
$state with the overlay revision current of the overlay of $module/$lang_key names,...
invalidateAll()
Forgets everything read so far, see MigratedTranslations::reset().
const int MAX_REVISION_FILE_SIZE
current only holds the name of a revision - more is never read.
getKeys(string $module, string $lang_key, ?string $client_data_dir)
Identifier => $module, null if $module is not served for $lang_key.
invalidateLanguage(string $lang_key)
Forgets what was read for $lang_key (e.g.
getText(string $module, string $lang_key, string $key, ?string $client_data_dir)
null if there is none (not served, not translated, or a problem, see MigratedTranslations::getProblem...
static servedState(string $module, string $lang_key, string $key, ?string $client_data_dir)
The state of $module/$lang_key if it is readable and lists the identifier $key, null otherwise.
if(!isset($arguments[0])) if(count( $arguments) > 3) if(! $remove_from_lang &&isset($arguments[2]) &&!is_dir($arguments[2])) $module
The values the ILIAS modules serve at runtime: the shipped ones with the local changes on top (see il...