Open thekid opened 3 years ago
$h= new Handlebars($path, (new I18n())
->dates('d.m.Y')
->times('H:i')
->separator(',')
->thousands('.')
->prices('%.2f EUR')
);
⚠️ Incomplete - date and time handling needs more than this, see https://www.php.net/manual/de/intldateformatter.create.php
Translations idea:
(new Handlebars($path))->using(new Translations('texts.csv'));
Language would need to be determined from the request:
In both scenarios, we could pass the determined language in request values and then react on that inside this template engine.
Telling the template engine where to select the language from could be done by passing a function(web.Request): string
to it.
⚠️ Incomplete - date and time handling needs more than this, see https://www.php.net/manual/de/intldateformatter.create.php
This has been taken care of in #3
Example implementation of translations extension:
<?php namespace org\example\skills;
use io\Path;
use io\streams\{TextReader, FileInputStream};
use text\csv\CsvMapReader;
use web\frontend\helpers\Extension;
class Translations extends Extension {
private $original;
private $texts= [];
/** Creates new translations from a CSV file */
public function __construct(string|Path $file) {
$reader= new CsvMapReader(new TextReader(new FileInputStream($file), 'utf-8'));
$translations= $reader->getHeaders();
$this->original= $translations[0];
foreach ($reader->withKeys($translations)->lines() as $record) {
$this->texts[$record[$this->original]]= $record;
}
}
/** Returns "t" helper */
public function helpers(): iterable {
yield 't' => function($in, $context, $options) {
$lang= $context->lookup('request', helpers: false)->value('user')['language'] ?? $this->original;
$text= array_shift($options);
return vsprintf($this->texts[$text][$lang] ?? $text, $options);
};
}
}
{{t "Welcome %s!" self.name}}
{{t "Search users and skills..."}}
en;de
"Welcome %s!";"Willkommen %s!"
"Search users and skills...";"Nutzer und Skills durchsuchen..."
I chose to use CSV because there are enough GUIs to edit these files, even by not-too-technical folks.
$lang= $context->lookup('request', helpers: false)->value('user')['language'] ?? $this->original;
⚠️ This seems a) like a "heavy" operation and b) could not serve as a general-purpose implementation since it has knowledge of the user object.
This seems a) like a "heavy" operation
One idea would be to pass an individual context so that this could be rewritten to $context->request->...
, for example. However, contexts spawn child contexts, which are new instances, e.g. a HashContext
, which handles e.g. lookup of @key
.
The other idea would be to have like a "scoped" engine, and then have: $context->engine->scope->...
or so. This is most probably easier, because instead of calling $engine->write($template, $context, $out)
(which internally passes $this
along with the context) we could invoke $template->write($c->withEngine(new Scoped($engine, $request)), $out);
The other idea would be to have like a "scoped" engine
...which means the helper would be dependant on a certain engine implementation, which is OK as we control the engine and the helpers inside this library.
Here's what we can do on the other hand: First, we need to construct templates with a function:
$translation= new Translation(
$texts,
fn($context) => $context->lookup('request', helpers: false)->value('user')['language'] ?? null
);
Second, we can cache the results of this function inside the context:
yield 't' => function($in, $context, $options) {
$lang= $context->variables['lang'] ??= ($this->language)($context) ?? $this->original;
// ...
}
The problem with the date and number helpers are that they do not take user preference into mind. Maybe this would work:
// Always uses "d.m.Y" as date format
$engine= new Handlebars($templates, new Dates(formats: ['short' => 'd.m.Y']));
// Use locale from user object, would try [lang]_[region], [lang], then fall back to null
$engine= new Handlebars($templates, new ByLocale(
fn($context) => $context->lookup('request', helpers: false)->value('user')['locale'],
[
'en_US' => [new Numbers('.', ','), new Dates(formats: ['short' => 'm/d/Y'])],
'de' => [new Numbers(',', '.'), new Dates(formats: ['short' => 'd.m.Y'])],
null => [new Numbers(), new Dates(formats: ['short' => 'd.m.Y'])],
]
));
// Using the "intl" extension (https://www.php.net/manual/de/book.intl.php)
$engine= new Handlebars($templates, new ByLocale(
fn($context) => $context->lookup('request', helpers: false)->value('user')['locale'],
fn($locale) => [Numbers::using(new NumberFormatter($locale)), Dates::using(new IntlDateFormatter($locale))],
));
The locale could also be initially detected by looking at Accept-Language
(and then have the user refine it, see https://www.w3.org/International/questions/qa-accept-lang-locales.en)
See https://preview.npmjs.com/package/handlebars-i18n as a good example, though we might abstract the text file into a
Translations
interface and implementations for YAML, CSV etcetera.