summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonas Kohl <git@jonaskohl.de>2024-09-16 13:27:51 +0200
committerJonas Kohl <git@jonaskohl.de>2024-09-16 13:27:51 +0200
commitfd3d969cf658475709db6a1e5090069cce51cbbd (patch)
tree2fba18a3eb37803afb37117fc100cc4292ba779d
parent38f03c375eafdb6b95190729479c6fa0d721b400 (diff)
Even more i18n
-rw-r--r--src/application/i18n.php176
-rw-r--r--src/application/messages/de.msg27
-rw-r--r--src/application/views/template_end.php18
-rw-r--r--src/composer.json3
-rw-r--r--src/composer.lock50
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": [],