Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

L10N & I18N #1

Open
thekid opened this issue Feb 13, 2021 · 10 comments
Open

L10N & I18N #1

thekid opened this issue Feb 13, 2021 · 10 comments
Labels
help wanted Extra attention is needed

Comments

@thekid
Copy link
Member

thekid commented Feb 13, 2021

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.

@thekid thekid added the help wanted Extra attention is needed label Feb 13, 2021
@thekid
Copy link
Member Author

thekid commented Feb 13, 2021

$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

@thekid
Copy link
Member Author

thekid commented Feb 13, 2021

Translations idea:

(new Handlebars($path))->using(new Translations('texts.csv'));

@thekid
Copy link
Member Author

thekid commented Feb 13, 2021

Language would need to be determined from the request:

  • for logged in users, we could read this from a database
  • for unauthenticated requests, we can use HTTP accept headers

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.

@thekid thekid changed the title I18N L10N & I18N Feb 14, 2021
@thekid
Copy link
Member Author

thekid commented Feb 14, 2021

⚠️ 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

@thekid
Copy link
Member Author

thekid commented Feb 14, 2021

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.

@thekid
Copy link
Member Author

thekid commented Feb 15, 2021

I chose to use CSV because there are enough GUIs to edit these files, even by not-to-technical folks.

If we refactored this to accept a Texts source, we could also easily support other formats like e.g. JSON, YAML, PO files, XLIFF and others.

@thekid
Copy link
Member Author

thekid commented Feb 15, 2021

$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.

@thekid
Copy link
Member Author

thekid commented Feb 17, 2021

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);

@thekid
Copy link
Member Author

thekid commented Feb 20, 2021

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;
  // ...
}

@thekid
Copy link
Member Author

thekid commented Feb 20, 2021

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)

@thekid thekid reopened this Feb 20, 2021
thekid added a commit to xp-forge/mustache that referenced this issue Feb 27, 2021
This opens possibilities to render contexts in different scopes, e.g. a HTTP
request scope, which then pass language and internationalization preferences

See xp-forge/handlebars-templates#1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant