avris/localisator

A neat tool to localise messages

v4.0.3 2020-03-06 19:16 UTC

This package is auto-updated.

Last update: 2020-11-06 20:46:41 UTC


README

A neat tool to localise messages.

Localisator uses three elements to translate your messages:

  • LocaleOrder -- decides which language(s) should be used,
  • Providers -- fetches a translation for a specific key from a source (file, database, cache, ...),
  • Transformers -- modifies the translation with more advanced logic (pluralisation, fallback, nesting, ...).

Installation

composer require avris/localisator

Usage

The library offers a simple LocalisatorBuilder, if you just want to use the default configuration. With it, the usage is as simple as this:

$locale = $_SESSION['_locale'] ?? 'en';
$dirs = [
    __DIR__ . '/translations',
];

$localisator = (new LocalisatorBuilder())
    ->registerExtension(new LocalisatorExtension($locale, $dirs))
    ->build(LocalisatorInterface::class);

If the translations dir you can define your translations, for instance:

app.en.yml:

homepage:
    title: My Website
    hello: Hello!
post:
    list: List of posts
    count: <> One post|%count% posts

app.pl.yml:

homepage:
    title: Moja strona
    hello: Witaj!
post:
    list: Lista postów
    count: <polishDeclination> Post|%count% posty|%count% postów

Then whenever you want to translate stuff, just use:

$localisator->get('post.count', ['count' => 3]); // Returns "3 posts" for "en" or "3 posty" for "pl"   

Locales

Locale's identifier is: the ISO 639-1 language code, then an underscore (_), then the ISO 3166-1 alpha-2 country code. Or it might be the language code alone.

Locale with a country is extending/specifying the locale with just the language code. For instance you might have en, en_GB and en_GB. The first file will contain all words and phrases shared by both British and American English, while en_GB could look like this:

color: Colour
dateFormat: d/m/Y
currency: £

and en_US like this:

color: Color
dateFormat: m/d/Y
currency: $

Namespaces

You can group translations into namespaces, app being the default. For instance if you want to keep your form validation messages in one place, put them into validator.*.yml files and access those translations with the $translator->get('validator:minLength', ['min' => 5]) notation.

Helpers

Localisation is a thing that's really widely used across any project and virtually every part of your application might want to translate some strings. If you don't want to worry about injecting Localisator into all those places, you might want to make an exception from pure DI just for the localisation.

It's like using new DateTime('tomorrow') instead of some hypothetical $this->timeManager->createDateTime('tomorrow').

To set it up you just need to call this method once at the beginning of your script:

LocalisedString::setLocalisator($localisator);

And then whenever you need something translated, just create an instance of LocalisedString. When casted to a string, it will get translated:

echo new LocalisedString('post.count', ['count' => 5]); // echoes "5 posts"

Or even shorter with a helper:

echo l('post.count', ['count' => 5]); // echoes "5 posts"

Twig

If you register the Avris\Localisator\LocalisatorTwig extension, you can use the Localisator either as a function or a filter:

{{ 'entity:Post.create.success'|l({title: post.title}) }}

{{ l('entity:Post.create.success', {title: post.title}) }}

Configuration

Providers

TranslationProviders are retrieving data from some source (like file or database) by namespace, key and locale and return a translated string (or null if not found).

  • DirsTranslationProvider reads the translations from all the files in given directories that can be read using implementations of FileReader (so far Yaml and PHP files supported),
  • ModuleDirTranslationProvider finds all the Micrus modules that have a translations directory and loads them,
  • CacheTranslationProvider wraps another provider and caches its translations using any PSR-6 cache pool; it also provides a function to warm up the cache.

Transformers

Transformers take the translated string (or null if not found) and the provided replacements, and do operations on them.

They are executed using Avris Dispatcher (event translationTransform), which takes care of the order of transformations (using listeners priority).

The built-in providers are, in order of execution:

  • FallbackToPatternTransformer (disabled): If the translation is not found, but the key matches a regexp pattern, returns the first match from the pattern. For instance for entity => ['^(.*)\.singular$'], when you want to translate entity:Post.singular but it doesn't exist, it would fall back to "Post".
  • FallbackToWordTransformer (disabled): If the translation is not found, falls back to showing the input key.
  • NestedTransformer (disabled): if the translation for like is "I like [[food.pizza.plural]] and [[food.banana.plural]]", for food.pizza.plural it's "Pizzas" and for food.banana.plural is "Bananas", the result will be "I like Pizzas and Bananas"
  • SelectorsTransformer: select one of the versions of the translation based on the data in the replacements array
    • CountVersion: <> a dog|%count% dogs will select "a dog" if count is equal 1, or "%count% dogs" if it's anything else. You can also specify more complex ranges: <> {0} no dogs|{1} a dog|{2-4} couple dogs|{5-} many dogs.
    • PolishDeclination (disabled): <polishDeclination> pies|%count% psy|%count% psów will select a correct version according to the rules of Polish declination (like "pies", "2 psy", "5 psów").
  • ReplacementsTransformer: Replaces parts of the translated string with values from the replacements array, for instance l('dogs.count', ['count' => 5]) -> %count% dogs -> 5 dogs.

Framework integration

Micrus

Although Localisator can now be used independently from it, it was originally written as a part of the Micrus framework. The integration with it is really simple. In your App\App:registerModules register the Localisator module:

yield new \Avris\Localisator\LocalisatorModule;

and in your config/services.yml register one of the handlers:

Avris\Localisator\Handler\SessionLocaleHandler: []
# or:
Avris\Localisator\Handler\UrlLocaleHandler: []

The first one will store user's locale in the session, the other in the URI (e.g. /de/login).

In config/localisation.yml specify which languages are supported and what's the fallback locale, for instance when a new user (with no chosen locale stored in the session) comes to the website and none of the languages in their Accepted-Language header is supported by you.

supported:
   en_GB: English (GB)
   en_US: English (USA)
   pl: Polski
   de: Deutsch
 fallback: en_GB

Micrus will automatically load the translations from all the modules that have a translations directory.

It will also try to figure out the best locale for a given request - from the URI, session, Accept-Language header and the fallback locale.

In Twig, you can access the current locale

<html lang="{{ currentLocale().language }}">

and generate the links to switch it (SessionLocaleHandler or UrlLocaleHandler will take care of them)

{% for code, name in locales %}
    <a href="{{ route('changeLocale', { locale: code }) }}">
        {{ name }}
    </a>
{% endfor %}

The l() helper is available throughout the application.

Symfony

Symfony has it's own, great tools for localisation. But if you want to use Localisator there anyway (for instance if a library need it, like TimeDiff does), you can use the following container configuration to set it up quickly:

For the LocalisatorBuilder:

Avris\Localisator\LocalisatorBuilder:
    calls:
        - [registerExtension, ['@Avris\Localisator\LocalisatorExtension']]
Avris\Localisator\LocalisatorExtension:
    arguments:
        $locale: '%locale%'
Avris\Localisator\LocalisatorInterface:
    factory: ['@Avris\Localisator\LocalisatorBuilder', build]
    arguments: ['Avris\Localisator\LocalisatorInterface']
Avris\Localisator\LocalisatorTwig: ~

Or for the full config:

Avris\Localisator\LocalisatorInterface: '@Avris\Localisator\Localisator'
Avris\Localisator\Localisator: ~
Avris\Localisator\LocalisatorTwig: ~

Avris\Localisator\Provider\TranslationProviderInterface: '@Avris\Localisator\Provider\CacheTranslationProvider'
Avris\Localisator\Provider\CacheTranslationProvider:
    arguments:
        $provider: '@Avris\Localisator\Provider\DirsTranslationProvider'
Avris\Localisator\Provider\DirsTranslationProvider:
    arguments:
        $dirs:
          - '%kernel.project_dir%/vendor/avris/time-diff/translations'
        $fileReaders:
          - '@Avris\Localisator\Provider\FileReader\YamlFileReader'
          - '@Avris\Localisator\Provider\FileReader\PhpFileReader'
Avris\Localisator\Provider\FileReader\YamlFileReader: ~
Avris\Localisator\Provider\FileReader\PhpFileReader: ~

Avris\Localisator\Order\LocaleOrderProviderInterface: '@Avris\Localisator\Order\SimpleLocaleOrderProvider'
Avris\Localisator\Order\SimpleLocaleOrderProvider:
    arguments:
        $locale: '%locale%'

Avris\Dispatcher\EventDispatcherInterface: '@Avris\Dispatcher\EventDispatcher'
Avris\Dispatcher\EventDispatcher:
    calls:
        - [registerSubscriber, ['@Avris\Localisator\Transformer\ReplacementsTransformer']]
        - [registerSubscriber, ['@Avris\Localisator\Transformer\SelectorsTransformer']]
        # - [registerSubscriber, ['@Avris\Localisator\Transformer\NestedTransformer']]
        # - [registerSubscriber, ['@Avris\Localisator\Transformer\FallbackToWordTransformer']]
        # - [registerSubscriber, ['@Avris\Localisator\Transformer\FallbackToPatternTransformer']]
Avris\Localisator\Transformer\ReplacementsTransformer: ~
Avris\Localisator\Transformer\SelectorsTransformer:
    arguments:
        $translationSelectors:
            - '@Avris\Localisator\Transformer\Selector\CountVersion'
            - '@Avris\Localisator\Transformer\Selector\PolishDeclination'
Avris\Localisator\Transformer\Selector\CountVersion: ~
Avris\Localisator\Transformer\Selector\PolishDeclination: ~

Linked projects

Check out also Stringer for a set of useful helpers and Polonisator for support of the Polish language.

Copyright