diff options
author | Jonas Kohl | 2024-09-16 13:27:51 +0200 |
---|---|---|
committer | Jonas Kohl | 2024-09-16 13:27:51 +0200 |
commit | fd3d969cf658475709db6a1e5090069cce51cbbd (patch) | |
tree | 2fba18a3eb37803afb37117fc100cc4292ba779d /src | |
parent | 38f03c375eafdb6b95190729479c6fa0d721b400 (diff) |
Even more i18n
Diffstat (limited to 'src')
-rw-r--r-- | src/application/i18n.php | 176 | ||||
-rw-r--r-- | src/application/messages/de.msg | 27 | ||||
-rw-r--r-- | src/application/views/template_end.php | 18 | ||||
-rw-r--r-- | src/composer.json | 3 | ||||
-rw-r--r-- | src/composer.lock | 50 |
5 files changed, 238 insertions, 36 deletions
diff --git a/src/application/i18n.php b/src/application/i18n.php index 2fcadab..f5b755d 100644 --- a/src/application/i18n.php +++ b/src/application/i18n.php @@ -1,72 +1,163 @@ <?php +use RR\Shunt\Context; +use RR\Shunt\Parser; +use RR\Shunt\Scanner; + const MESSAGE_DIR = __DIR__ . "/messages"; $__i18n_msg_store = []; +$__i18n_msg_store_plural = []; +$__i18n_msg_metadata = []; $__i18n_current_locale = null; +const _I18N_MSGID = 0; +const _I18N_MSGSTR = 1; +const _I18N_MSGIDP = 2; +const _I18N_MSGSTRP = 3; +const _I18N_META = 10; + function i18n_parse(string $contents, ?string $filename = null): array { $syntax_error = fn(string $msg, int $line): never => throw new Exception("i18n syntax error: $msg (in " . ($filename ?? "unknown") . ":" . $line . ")"); $other_error = fn(string $msg, int $line): never => throw new Exception("i18n error: $msg (in " . ($filename ?? "unknown") . ":" . $line . ")"); + $metadata = []; + $msgs = []; + $pmsgs = []; $lines = explode("\n", $contents); $currentId = ""; $currentContext = ""; $currentMessage = ""; - $isInMessage = false; + $currentIdP = []; + $currentMessageP = []; + $currentMetadataBuffer = []; + $state = _I18N_MSGID; + + $processedLines = 0; foreach ($lines as $i => $ln) { $lnNum = $i + 1; if (trim($ln) === "") continue; + if ($ln === "metadata({") { + if ($processedLines > 0) + $syntax_error("metadata must come first in file", $lnNum); + $currentMetadataBuffer []= "{"; + $state = _I18N_META; + ++$processedLines; + continue; + } + if ($state == _I18N_META) { + if ($ln === "})") { + $currentMetadataBuffer []= "}"; + $state = _I18N_MSGSTR; + $currentMetadataBuffer = implode("\n", $currentMetadataBuffer); + $metadata = json_decode($currentMetadataBuffer, true); + } else { + $currentMetadataBuffer []= $ln; + } + ++$processedLines; + continue; + } + switch ($ln[0]) { case "#": continue 2; case ":": - if ($currentId !== "") { - if ($currentContext !== "") - $currentId = $currentContext . "\004" . $currentId; - if (isset($msgs[$currentId])) - $other_error("duplicate message id '$currentId'", $lnNum); - $msgs[$currentId] = $currentMessage; + if ($state === _I18N_MSGSTRP) { + // plural + if (count($currentIdP) > 0) { + if (isset($msgs[$currentId])) + $other_error("duplicate message id '$currentId'", $lnNum); + $pmsgs[$currentIdP[0]] = $currentMessageP; + } + } else { + // singular + if ($currentId !== "") { + if ($currentContext !== "") + $currentId = $currentContext . "\x7F" . $currentId; + if (isset($msgs[$currentId])) + $other_error("duplicate message id '$currentId'", $lnNum); + $msgs[$currentId] = $currentMessage; + } } - $currentId = json_decode(substr($ln, 2)); + $currentIdP = []; + $currentMessageP = []; + $currentId = ""; $currentContext = ""; $currentMessage = ""; - $isInMessage = false; + if ($ln === ":...") { + // plural + $state = _I18N_MSGIDP; + } else { + // singular + $currentId = json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum); + $state = _I18N_MSGID; + } + break; + case "-": + if ($state === _I18N_MSGIDP) { + $_id = json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum); + if ($currentContext !== "") + $_id = $currentContext . "\x7F" . $_id; + $currentIdP []= $_id; + } elseif ($state === _I18N_MSGSTRP) { + $currentMessageP []= json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum); + } else + $syntax_error("cannot define multiple ids/messages in singular mode", $lnNum); break; case "=": - $isInMessage = true; - $currentMessage = json_decode(substr($ln, 2)); + if ($ln === "=...") { + // plural + if ($state !== _I18N_MSGIDP) + $syntax_error("cannot start plural message in singular mode", $lnNum); + $state = _I18N_MSGSTRP; + } else { + // singular + $state = _I18N_MSGSTR; + $currentMessage = json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum); + } break; case "?": - if ($isInMessage) + if ($state !== _I18N_MSGID) $syntax_error("context must be defined before start of message", $lnNum); - $currentContext = json_decode(substr($ln, 2)); + $currentContext = json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum); break; case " ": - if ($isInMessage) - $currentMessage .= json_decode(substr($ln, 2)); + if ($state === _I18N_MSGSTR) + $currentMessage .= json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum); else - $currentId .= json_decode(substr($ln, 2)); + $currentId .= json_decode(substr($ln, 2)) ?? $syntax_error("malformed string", $lnNum); break; default: $syntax_error("invalid start of line '" . $ln[0] . "' (0x" . str_pad(strtoupper(dechex(ord($ln[0]))), 2, "0", STR_PAD_LEFT) . ")", $lnNum); break; } + + ++$processedLines; } - if ($currentId !== "") { - if ($currentContext !== "") - $currentId = $currentContext . "\x7F" . $currentId; - if (isset($msgs[$currentId])) - $other_error("duplicate message id '$currentId'", count($lines)); - $msgs[$currentId] = $currentMessage; + if ($state === _I18N_MSGSTRP) { + // plural + if (count($currentIdP) > 0) { + if (isset($msgs[$currentId])) + $other_error("duplicate message id '$currentId'", $lnNum); + $pmsgs[$currentIdP[0]] = $currentMessageP; + } + } else { + // singular + if ($currentId !== "") { + if ($currentContext !== "") + $currentId = $currentContext . "\x7F" . $currentId; + if (isset($msgs[$currentId])) + $other_error("duplicate message id '$currentId'", $lnNum); + $msgs[$currentId] = $currentMessage; + } } - return $msgs; + return [$metadata, $msgs, $pmsgs]; } function i18n_locale(string $locale) { @@ -91,14 +182,51 @@ function i18n_get(string $msgid, array $params = [], ?string $context = null): s ); } +function i18n_get_plural(string $msgid_singular, string $msgid_plural, int $count, array $params = [], ?string $context = null): string { + global $__i18n_current_locale, $__i18n_msg_metadata, $__i18n_msg_store_plural; + + $key = $msgid_singular; + if ($context !== null) + $key = $context . "\x7F" . $key; + + $msgs = ($__i18n_msg_store_plural[$__i18n_current_locale] ?? [])[$msgid_singular] ?? null; + $msg = ""; + if ($msgs === null) { + $msg = $count === 1 ? $msgid_singular : $msgid_plural; + } else { + $expression = (($__i18n_msg_metadata[$__i18n_current_locale] ?? [])["plural"] ?? [])["indexMapping"] ?? "n == 1 ? 0 : 1"; + $ctx = new Context(); + $ctx->def("ifelse", function(bool $c, $a, $b) { return $c ? $a : $b; }); + $ctx->def("n", $count); + $parser = new Parser(new Scanner($expression)); + $index = $parser->reduce($ctx); + $index = max(0, min(count($msgs), $index)); + $msg = $msgs[$index]; + } + + uksort($params, fn(string $a, string $b): int => strlen($b) <=> strlen($a)); + return str_replace( + array_map(fn(string $k): string => "%$k%", array_keys($params)), + array_values($params), + $msg + ); +} + function __(string $msgid, array $params = [], ?string $context = null): string { return i18n_get($msgid, $params, $context); } +function ___(string $msgid_singular, string $msgid_plural, int $count, array $params = [], ?string $context = null): string { + return i18n_get_plural($msgid_singular, $msgid_plural, $count, $params, $context); +} + foreach (scandir(MESSAGE_DIR) as $ent) { $path = MESSAGE_DIR . "/" . $ent; if ($ent[0] === "." || !is_file($path) || strcasecmp(pathinfo($ent, PATHINFO_EXTENSION), "msg") !== 0) continue; $lang = pathinfo($ent, PATHINFO_FILENAME); - $__i18n_msg_store[$lang] = i18n_parse(file_get_contents($path), $ent); + [$meta, $msgs, $pmsgs] = i18n_parse(file_get_contents($path), $ent); + $__i18n_msg_store[$lang] = $msgs; + $__i18n_msg_store_plural[$lang] = $pmsgs; + $__i18n_msg_metadata[$lang] = $meta; } diff --git a/src/application/messages/de.msg b/src/application/messages/de.msg index 692e559..c797241 100644 --- a/src/application/messages/de.msg +++ b/src/application/messages/de.msg @@ -1,3 +1,9 @@ +metadata({ + "plural": { + "indexMapping": "ifelse(n = 0, 2, ifelse(n = 1, 0, 1))" + } +}) + : "Log in" = "Anmelden" @@ -254,9 +260,18 @@ : "Upload new profile picture" = "Neues Profilbild hochladen" - -#:: -#- "Select file" -#- "Select files" -#= "Datei auswählen" -#- "Dateien auswählen" +:... +- "Select file" +- "Select files" +=... +- "Datei auswählen" +- "Dateien auswählen" + +:... +- "%n% file selected" +- "%n% files selected" +- "No files selected" +=... +- "%n% Datei ausgewählt" +- "%n% Dateien ausgewählt" +- "Keine Datei ausgewählt" diff --git a/src/application/views/template_end.php b/src/application/views/template_end.php index 7757780..c30330e 100644 --- a/src/application/views/template_end.php +++ b/src/application/views/template_end.php @@ -12,6 +12,18 @@ <script> $(function() { + var _messages = <?= json_encode([ + "selectFiles" => [ + ___("Select file", "Select files", 1), + ___("Select file", "Select files", 2), + ], + "filesSelected" => [ + ___("%n% file selected", "No files selected", 0), + ___("%n% file selected", "%n% files selected", 1), + ___("%n% file selected", "%n% files selected", 2), + ], + ]) ?> + $("._time").each(function(i, e) { var date = new Date($(e).text()); $(e).text(date.toLocaleString()); @@ -27,11 +39,11 @@ $(function() { $("input[type=file]").each(function(i, e) { var isMultiple = !!$(e).prop("multiple"); - var $input = $('<input type="text" readonly class="form-control" />').attr("placeholder", "No file" + (isMultiple ? "s" : "") + " selected"); + var $input = $('<input type="text" readonly class="form-control" />').attr("placeholder", _messages.filesSelected[0]); $(e).after($('<div class="input-group file-input-group"></div>').append( $input, $('<span class="input-group-btn"></span>').append( - $('<button class="btn btn-default" type="button"></button>').text("Select file" + (isMultiple ? "s" : "") + "...").click(function() { + $('<button class="btn btn-default" type="button"></button>').text(_messages.selectFiles[isMultiple ? 1 : 0]).click(function() { $(e).click(); }) ) @@ -43,7 +55,7 @@ $(function() { else if (files.length === 1) $input.val(files[0].name); else - $input.val(files.length + " files selected"); + $input.val(_messages.filesSelected[2].replace("%n%", files.length)); }); }) }); diff --git a/src/composer.json b/src/composer.json index 82afc5b..badc2eb 100644 --- a/src/composer.json +++ b/src/composer.json @@ -6,6 +6,7 @@ }, "require": { "symfony/mailer": "^7.1", - "gregwar/captcha": "^1.2" + "gregwar/captcha": "^1.2", + "andig/php-shunting-yard": "dev-master" } } diff --git a/src/composer.lock b/src/composer.lock index 048b760..16b5256 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -4,9 +4,53 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6a9be35a60a5746ba957ae334c8f378a", + "content-hash": "d16e6b613e1bf8bf4fe62234fa37325f", "packages": [ { + "name": "andig/php-shunting-yard", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/andig/php-shunting-yard.git", + "reference": "18109b60ce04338abdac0cf7d72ef07d4414d21c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/andig/php-shunting-yard/zipball/18109b60ce04338abdac0cf7d72ef07d4414d21c", + "reference": "18109b60ce04338abdac0cf7d72ef07d4414d21c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "^5.0|^6.0|^7.0|^8.0" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-0": { + "RR\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Refactored repack of https://github.com/droptable/php-shunting-yard", + "homepage": "https://github.com/andig/php-shunting-yard", + "keywords": [ + "equation", + "math", + "shunting yard" + ], + "support": { + "issues": "https://github.com/andig/php-shunting-yard/issues", + "source": "https://github.com/andig/php-shunting-yard/tree/master" + }, + "time": "2019-03-12T16:50:30+00:00" + }, + { "name": "doctrine/lexer", "version": "3.0.1", "source": { @@ -1142,7 +1186,9 @@ "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "andig/php-shunting-yard": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": [], |