19declare(strict_types=1);
50if (PHP_SAPI !==
'cli') {
55require
$repo_root .
'/vendor/composer/vendor/autoload.php';
65 fwrite(STDERR,
'ERROR: ' .
$message . PHP_EOL);
74 static fn(array $match): string => sprintf(
'\x%02X', ord($match[0])),
92 if ($path ===
'' || str_contains($path,
"\0")) {
95 if (!str_starts_with($path,
'/')) {
96 $path = getcwd() .
'/' . $path;
99 $existing = rtrim($path,
'/');
100 while ($existing !==
'' && !file_exists($existing)) {
101 $missing[] = basename($existing);
102 $existing = dirname($existing);
104 $resolved = realpath($existing ===
'' ?
'/' : $existing);
105 $resolved_root = realpath($root);
106 if ($resolved ===
false || $resolved_root ===
false) {
109 foreach ($missing as $segment) {
110 if ($segment ===
'' || $segment ===
'.' || $segment ===
'..') {
114 $absolute = $resolved . ($missing === [] ?
'' :
'/' . implode(
'/', array_reverse($missing)));
116 return str_starts_with($absolute .
'/', rtrim($resolved_root,
'/') .
'/') && $absolute !== $resolved_root
127 foreach ($identifiers as $identifier) {
128 $base = (string) preg_replace(
'/[^A-Z0-9_]+/',
'_', strtoupper($identifier));
130 $base =
'KEY_' . $base;
132 $bases[$identifier] = $base;
135 foreach ($bases as $identifier => $base) {
136 $groups[$base][] = (string) $identifier;
138 $taken = array_fill_keys(array_keys($groups),
true);
141 ksort($groups, SORT_STRING);
142 foreach ($groups as $base => $group) {
143 usort($group,
static fn(
string $a,
string $b):
int => [
$a !== strtolower(
$a),
$a] <=> [
$b !== strtolower(
$b),
$b]);
144 $names[$group[0]] = (string) $base;
145 $described = [sprintf(
'"%s" -> %s', $group[0], $base)];
147 foreach (array_slice($group, 1) as $identifier) {
148 while (isset($taken[$base .
'_' . $suffix])) {
151 $name = $base .
'_' . $suffix;
152 $taken[$name] =
true;
153 $names[$identifier] = $name;
154 $described[] = sprintf(
'"%s" -> %s', $identifier, $name);
156 if (count($group) > 1) {
157 $collisions[] = sprintf(
'Case name %s: %s', $base, implode(
', ', $described));
162 foreach ($identifiers as $identifier) {
163 $ordered[$identifier] = $names[$identifier];
166 return [
'names' => $ordered,
'collisions' => $collisions];
198 declare(strict_types=1);
201 $code .=
"\nnamespace " . $namespace .
";\n\n";
202 if ($namespace !==
'ILIAS\\Language') {
203 $code .=
"use ILIAS\\Language\\LanguageIdentifier;\n\n";
206 .
" * The language identifiers of the module \"" .
$comment_safe(
$module) .
"\", see LanguageIdentifier.\n"
210 .
" * generate it again after every change of the .pot (CI: --check).\n"
212 .
"enum " . $enum_name .
": string implements LanguageIdentifier\n{\n";
213 foreach ($names as $identifier => $name) {
214 $code .=
' case ' . $name .
' = ' . var_export((
string) $identifier,
true) .
";\n";
219 $code .=
" public function module(): string\n {\n return " . var_export(
$module,
true) .
";\n }\n}\n";
227$literal_module_of =
static function (ReflectionEnum $reflection): ?
string {
228 if (!$reflection->hasMethod(
'module')) {
231 $method = $reflection->getMethod(
'module');
232 $file = $method->getFileName();
233 $lines = $file ===
false ? false : file($file);
234 if ($lines ===
false || $method->getStartLine() ===
false || $method->getEndLine() ===
false) {
237 $source = implode(
'', array_slice($lines, $method->getStartLine() - 1, $method->getEndLine() - $method->getStartLine() + 1));
238 $tokens = array_values(array_filter(
239 token_get_all(
'<?php ' . $source),
240 static fn(mixed
$token):
bool => !is_array(
$token) || !in_array(
$token[0], [T_OPEN_TAG, T_WHITESPACE, T_COMMENT, T_DOC_COMMENT],
true)
242 $start = array_search(
'{', $tokens,
true);
243 $body = $start ===
false ? [] : array_slice($tokens, $start + 1);
246 || !is_array(
$body[0]) ||
$body[0][0] !== T_RETURN
247 || !is_array(
$body[1]) ||
$body[1][0] !== T_CONSTANT_ENCAPSED_STRING || !str_starts_with(
$body[1][1],
"'")
253 return strtr(substr(
$body[1][1], 1, -1), [
'\\\\' =>
'\\',
"\\'" =>
"'"]);
258$options = [
'check' =>
false,
'stdout' =>
false,
'module' =>
null,
'namespace' =>
null];
260foreach (array_slice($argv, 1) as $argument) {
261 if ($argument ===
'--check' || $argument ===
'--stdout') {
262 $options[substr($argument, 2)] =
true;
263 } elseif (str_starts_with($argument,
'--module=')) {
264 $options[
'module'] = substr($argument, strlen(
'--module='));
265 } elseif (str_starts_with($argument,
'--namespace=')) {
266 $options[
'namespace'] = substr($argument, strlen(
'--namespace='));
267 } elseif (str_starts_with($argument,
'-')) {
274 $fail(
'Usage: php generate_language_identifier_enum.php [--check | --stdout] [--module=<module>] --namespace=<Namespace> <pot file> <enum file>');
277 $fail(
'--check and --stdout exclude each other.');
281$namespace = (string)
$options[
'namespace'];
282if (preg_match(
'/^[A-Za-z_][A-Za-z0-9_]*(\\\\[A-Za-z_][A-Za-z0-9_]*)*$/', $namespace) !== 1) {
283 $fail(sprintf(
'Invalid namespace "%s" (e.g. "ILIAS\\Poll", without leading backslash).',
$printable($namespace)));
287if ($pot_file ===
null || !is_file($pot_file) || !str_ends_with($pot_file,
'.pot')) {
291if ($enum_file ===
null || !str_ends_with($enum_file,
'.php')) {
294$enum_name = basename($enum_file,
'.php');
296 $fail(sprintf(
'"%s" is no valid enum name (the file name without ".php").',
$printable($enum_name)));
298if (
$options[
'check'] && !is_file($enum_file)) {
299 $fail(sprintf(
'The enum "%s" does not exist.',
$printable($enum_argument)));
301if (!
$options[
'check'] && !
$options[
'stdout'] && !is_dir(dirname($enum_file))) {
302 $fail(sprintf(
'The directory of "%s" does not exist.',
$printable($enum_argument)));
306 $catalog = TranslationCatalog::fromPoFile($pot_file);
307}
catch (RuntimeException
$e) {
310$module =
$options[
'module'] ?? $catalog->getHeader(
'X-Domain') ?? basename($pot_file,
'.pot');
311if (!MigratedLanguageFilePaths::isValidModule(
$module)) {
316foreach ($catalog->getEntries() as $entry) {
317 $context = $entry->getContext();
318 if ($context !==
null && $context !==
$module) {
319 $fail(sprintf(
'msgid "%s" has the msgctxt "%s" - only none or the module is supported.',
$printable($entry->getId()),
$printable($context)));
321 $identifier = $entry->getId();
322 if ($identifier ===
'' || preg_match(
'/[\x00-\x1F\x7F]/', $identifier) === 1) {
323 $fail(sprintf(
'msgid "%s" cannot be a case (empty or with a control character).',
$printable($identifier)));
325 $identifiers[$identifier] = $identifier;
327[
'names' => $names,
'collisions' => $collisions] =
$case_names(array_values($identifiers));
328foreach ($collisions as $collision) {
329 fwrite(STDERR,
'NOTE: ' .
$printable($collision) . PHP_EOL);
334if (preg_match(
'/\A[A-Za-z0-9_.\/-]+\z/',
$pot_path) !== 1) {
335 $fail(sprintf(
'The path "%s" of the .pot may only contain A-Z, a-z, 0-9, "_", ".", "/" and "-".',
$printable(
$pot_path)));
337$fqcn = $namespace .
'\\' . $enum_name;
342 if (!interface_exists(LanguageIdentifier::class)) {
343 require_once dirname(__DIR__, 2) .
'/src/LanguageIdentifier.php';
345 $declared_before = get_declared_classes();
346 (
static function (
string $file):
void {
349 $declared = array_map(
'strtolower', array_diff(get_declared_classes(), $declared_before));
350 if (!in_array(strtolower($fqcn), $declared,
true) || !enum_exists($fqcn,
false)) {
353 $reflection =
new ReflectionEnum($fqcn);
355 if (!$reflection->implementsInterface(LanguageIdentifier::class)) {
356 $deviations[] = sprintf(
'%s does not implement %s.', $fqcn, LanguageIdentifier::class);
358 if ((
string) $reflection->getBackingType() !==
'string') {
359 $deviations[] = sprintf(
'%s is not backed by string.', $fqcn);
362 foreach ($reflection->getCases() as $case) {
363 $actual[$case->getName()] = $case instanceof ReflectionEnumBackedCase ? $case->getBackingValue() :
null;
365 $expected = array_flip($names);
366 foreach ($expected as $name => $identifier) {
367 if (!array_key_exists($name, $actual)) {
368 $deviations[] = sprintf(
'Missing case %s = %s.', $name, var_export((
string) $identifier,
true));
369 } elseif ($actual[$name] !== (
string) $identifier) {
370 $deviations[] = sprintf(
'Case %s has the value %s instead of %s.', $name, var_export($actual[$name],
true), var_export((
string) $identifier,
true));
373 foreach (array_diff_key($actual, $expected) as $name => $value) {
374 $deviations[] = sprintf(
'Surplus case %s = %s (not in the .pot).', $name, var_export($value,
true));
376 $first_case = $fqcn::cases()[0] ??
null;
379 : $literal_module_of($reflection);
380 if ($actual_module ===
null) {
381 $deviations[] = sprintf(
'module() of %s (without cases) does not just return a single-quoted string literal - cannot check it.', $fqcn);
382 } elseif ($actual_module !==
$module) {
383 $deviations[] = sprintf(
'module() returns %s instead of %s.', var_export($actual_module,
true), var_export(
$module,
true));
385 if ($deviations === []) {
386 fwrite(STDOUT,
$printable(sprintf(
'OK: %s matches %s (%d cases, module "%s").', $fqcn,
$pot_path, count($names),
$module)) . PHP_EOL);
389 foreach ($deviations as $deviation) {
390 fwrite(STDERR,
'DEVIATION: ' .
$printable($deviation) . PHP_EOL);
400 fwrite(STDOUT, $code);
403if (is_file($enum_file) && file_get_contents($enum_file) === $code) {
404 fwrite(STDOUT,
$printable(sprintf(
'Unchanged: %s (%d cases).', $enum_file, count($names))) . PHP_EOL);
408 AtomicFileWriter::write($enum_file, $code,
$repo_root);
409}
catch (RuntimeException
$e) {
412fwrite(STDOUT,
$printable(sprintf(
'Written: %s (%d cases, module "%s").', $enum_file, count($names),
$module)) . PHP_EOL);
Writes a file so that concurrent readers only see the complete old or new content: a temporary file i...
An in-memory gettext catalog (header block plus messages) and the only place of the component that re...
The single place that knows where the files of a module migrated to PO/MO live:
if(!isset($arguments[0])) if(count( $arguments) > 3) if(! $remove_from_lang &&isset($arguments[2]) &&!is_dir($arguments[2])) $module
if(is_file($english_lang_file)) if( $language_names===[]) foreach(array_keys($per_language) as $lang_key) $pot_path
if(PHP_SAPI !=='cli') $repo_root
Generates the enum of the language identifiers of one module (an implementation of ILIAS\Language\Lan...
const LANGUAGE_IDENTIFIER_ENUM_TOOL
$reserved_case_names
Case names PHP rejects (all other keywords are allowed as names of class constants and cases).
$inside_repository
The absolute path of $path if it lies inside $root, null otherwise.
$printable
Makes control characters visible in everything printed that comes from a file or the command line.
A language identifier as an enum case (backing value = identifier), generated per module from its ....
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
exit
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...