tobento/service-translation

Allowing you to support multiple languages within your application.

1.0.3 2023-06-18 10:14 UTC

This package is auto-updated.

Last update: 2024-04-18 12:24:48 UTC


README

With the Translation Service you can translate messages easily.

Table of Contents

Getting started

Add the latest version of the translation service running this command.

composer require tobento/service-translation

Requirements

  • PHP 8.0 or greater

Highlights

  • Framework-agnostic, will work with any project
  • Decoupled design

Simple Example

Here is a simple example of how to use the translation service:

use Tobento\Service\Translation\Translator;
use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;
use Tobento\Service\Translation\Modifiers;
use Tobento\Service\Translation\Modifier\ParameterReplacer;
use Tobento\Service\Translation\Modifier\Pluralization;
use Tobento\Service\Translation\MissingTranslationHandler;

$translator = new Translator(
    new Resources(
        new Resource('*', 'de', [
            'Hello World' => 'Hallo Welt',
        ]),
    ),
    new Modifiers(
        new Pluralization(),
        new ParameterReplacer(),
    ),
    new MissingTranslationHandler(),
    'en',
);

var_dump($translator->trans('Hello World'));
// string(11) "Hello World"

var_dump($translator->trans('Hello World', [], 'de'));
// string(10) "Hallo Welt"

Documentation

Translator

Create Translator

use Tobento\Service\Translation\Translator;
use Tobento\Service\Translation\TranslatorInterface;
use Tobento\Service\Translation\LocaleAware;
use Tobento\Service\Translation\ResourcesAware;
use Tobento\Service\Translation\ModifiersAware;
use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;
use Tobento\Service\Translation\Modifiers;
use Tobento\Service\Translation\Modifier\ParameterReplacer;
use Tobento\Service\Translation\MissingTranslationHandler;

$translator = new Translator(
    resources: new Resources(
        new Resource('*', 'de', [
            'Hello World' => 'Hallo Welt',
        ]),
    ),
    modifiers: new Modifiers(
        new ParameterReplacer(),
    ),
    missingTranslationHandler: new MissingTranslationHandler(),
    locale: 'en',
    localeFallbacks: ['de' => 'en'],
    localeMapping: ['de' => 'de-CH'],
);

var_dump($translator instanceof TranslatorInterface);
// bool(true)

var_dump($translator instanceof LocaleAware);
// bool(true)

var_dump($translator instanceof ResourcesAware);
// bool(true)

var_dump($translator instanceof ModifiersAware);
// bool(true)

Translator Interface

use Tobento\Service\Translation\TranslatorInterface;

$translated = $translator->trans(
    message: 'Hi :name',
    parameters: [':name' => 'John'],
    locale: 'de'
);

Locale Aware

use Tobento\Service\Translation\LocaleAware;

// set the default locale:
$translator->setLocale('de');

// get the default locale:
var_dump($translator->getLocale());
// string(2) "de"

// set the locale fallbacks:
$translator->setLocaleFallbacks(['de' => 'en']);

// get the locale fallbacks:
var_dump($translator->getLocaleFallbacks());
// array(1) { ["de"]=> string(2) "en" }

// set the locale mapping:
$translator->setLocaleMapping(['de' => 'de-CH']);

// get the locale mapping:
var_dump($translator->getLocaleMapping());
// array(1) { ["de"]=> string(5) "de-CH" }

Resources Aware

See also Resources or Files Resources for more details.

use Tobento\Service\Translation\ResourcesAware;
use Tobento\Service\Translation\ResourcesInterface;

// get the resources:
var_dump($translator->resources() instanceof ResourcesInterface);
// bool(true)

// returns the translations of the specified resource:
$translations = $translator->getResource(
    name: '*',
    locale: 'de' // or null to use default
);

var_dump($translations);
// array(1) { ["Hello World"]=> string(10) "Hallo Welt" }

// returns a new instance with the specified resources:
$translator = $translator->withResources(
    resources: $resources // ResourcesInterface
);

Modifiers Aware

See also Modifiers for more details.

use Tobento\Service\Translation\ModifiersAware;
use Tobento\Service\Translation\ModifiersInterface;

// get the modifiers:
var_dump($translator->modifiers() instanceof ModifiersInterface);
// bool(true)

// returns a new instance with the specified modifiers:
$translator = $translator->withModifiers(
    modifiers: $modifiers // ModifiersInterface
);

Translate Message

use Tobento\Service\Translation\Translator;
use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;
use Tobento\Service\Translation\Modifiers;
use Tobento\Service\Translation\Modifier\Pluralization;
use Tobento\Service\Translation\Modifier\ParameterReplacer;
use Tobento\Service\Translation\MissingTranslationHandler;

$translator = new Translator(
    resources: new Resources(
        new Resource('*', 'de', [
            'Hi :name' => 'Hi :name',
            'It takes :minutes minute|It takes :minutes minutes' => 'Es dauert :minutes Minute|Es dauert :minutes Minuten'
        ]),
    ),
    modifiers: new Modifiers(
        new Pluralization(),
        new ParameterReplacer(),
    ),
    missingTranslationHandler: new MissingTranslationHandler(),
    locale: 'en',
    localeFallbacks: ['de' => 'en'],
);

$translated = $translator->trans(
    message: 'Hi :name',
    parameters: [':name' => 'John'],
    locale: 'de'
);

var_dump($translated);
// string(7) "Hi John"

$translated = $translator->trans(
    message: 'It takes :minutes minute|It takes :minutes minutes',
    parameters: [':minutes' => 5, 'count' => 5],
    locale: 'de'
);

var_dump($translated);
// string(19) "Es dauert 5 Minuten"

By default, resources will be sorted by its priority!

Using specific resource

Keep in mind that named resources are only loaded on the first resource request.
Resources with "*" named are always loaded.

use Tobento\Service\Translation\Translator;
use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;
use Tobento\Service\Translation\Modifiers;
use Tobento\Service\Translation\MissingTranslationHandler;

$translator = new Translator(
    resources: new Resources(
        new Resource('shop', 'de', [
            'noProducts' => 'Keine Produkte',
            'No items in your shopping bag.' => 'Keine Artikel sind in deinem Warenkorb.',
        ]),
    ),
    modifiers: new Modifiers(),
    missingTranslationHandler: new MissingTranslationHandler(),
    locale: 'en',
    localeFallbacks: ['de' => 'en'],
);

// with dot notation
$translated = $translator->trans(
    message: 'shop.noProducts',
    locale: 'de'
);

var_dump($translated);
// string(14) "Keine Produkte"

// with src parameter
$translated = $translator->trans(
    message: 'No items in your shopping bag.',
    parameters: ['src' => 'shop'],
    locale: 'de'
);

var_dump($translated);
// string(39) "Keine Artikel sind in deinem Warenkorb."

Resources

Create Resources

use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\ResourcesInterface;
use Tobento\Service\Translation\Resource;

$resources = new Resources(
    new Resource(
        name: '*', 
        locale: 'en', 
        translations: ['Hello World' => 'Hallo Welt'],
        group: 'front',
        priority: 10,
    ),
);

var_dump($resources instanceof ResourcesInterface);
// bool(true)

Add Resources

You may add resources by using the add method:

add resource

use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;

$resources = new Resources();

$resources->add(new Resource('*', 'de', [
    'Hello World' => 'Hallo Welt',
]));

add resources

use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;

$resources = new Resources();

$resources->add(new Resources(
    new Resource('*', 'de', [
        'Hello World' => 'Hallo Welt',
    ]),
));

Filter Resources

You may use the filter methods returning a new instance.

filter

use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;
use Tobento\Service\Translation\ResourceInterface;

$resources = new Resources(
    new Resource(
        name: '*', 
        locale: 'en', 
        translations: ['Hello World' => 'Hallo Welt'],
        group: 'front',
    ),
    new Resource(
        name: '*', 
        locale: 'en', 
        translations: ['Hello World' => 'Hallo Welt'],
        group: 'back',
    ),    
);

// filter by group:
$resources = $resources->filter(
    fn(ResourceInterface $r): bool => $r->group() === 'front'
);

locale

use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;

$resources = new Resources(
    new Resource(
        name: '*', 
        locale: 'en', 
        translations: ['Hello World' => 'Hallo Welt'],
    ),
    new Resource(
        name: '*', 
        locale: 'de', 
        translations: ['Hello World' => 'Hallo Welt'],
    ),    
);

// filter by locale:
$resources = $resources->locale('en');

locales

use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;

$resources = new Resources(
    new Resource(
        name: '*', 
        locale: 'en', 
        translations: ['Hello World' => 'Hallo Welt'],
    ),
    new Resource(
        name: '*', 
        locale: 'de', 
        translations: ['Hello World' => 'Hallo Welt'],
    ),    
);

// filter by locales:
$resources = $resources->locales(['en', 'de']);

name

use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;

$resources = new Resources(
    new Resource(
        name: 'shop', 
        locale: 'en', 
        translations: ['Hello World' => 'Hallo Welt'],
    ),
    new Resource(
        name: 'shop', 
        locale: 'de', 
        translations: ['Hello World' => 'Hallo Welt'],
    ),    
);

// filter by name:
$resources = $resources->name('shop');

Sort Resources

sort by priority

use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;

$resources = new Resources(
    new Resource(
        name: '*', 
        locale: 'en', 
        translations: ['Hello World' => 'Hallo Welt'],
        priority: 10,
    ),
    new Resource(
        name: '*', 
        locale: 'en', 
        translations: ['Hello World' => 'Hallo Welt'],
        priority: 15,
    ),    
);

// sort by priority:
$resources = $resources->sort();

sort by callback

use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;
use Tobento\Service\Translation\ResourceInterface;

$resources = new Resources(
    new Resource(
        name: 'users', 
        locale: 'en', 
        translations: ['Hello World' => 'Hallo Welt'],
        priority: 10,
    ),
    new Resource(
        name: 'shop', 
        locale: 'en', 
        translations: ['Hello World' => 'Hallo Welt'],
        priority: 15,
    ),    
);

// sort by name:
$resources = $resources->sort(
    fn(ResourceInterface $a, ResourceInterface $b): int => $a->name() <=> $b->name()
);

Get Resources / Translations

all

use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;
use Tobento\Service\Translation\ResourceInterface;

$resources = new Resources(
    new Resource(
        name: '*', 
        locale: 'en', 
        translations: ['Hello World' => 'Hallo Welt'],
    ),  
);

foreach($resources->all() as $resource) {
    var_dump($resource instanceof ResourceInterface);
    // bool(true)
    
    var_dump($resource->name());
    // string(1) "*"
    
    var_dump($resource->locale());
    // string(2) "en"
    
    var_dump($resource->group());
    // string(7) "default"
    
    var_dump($resource->priority());
    // int(0)
    
    var_dump($resource->translations());
    // array(1) { ["Hello World"]=> string(10) "Hallo Welt" }
}

translations

use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;

$resources = new Resources(
    new Resource(
        name: '*', 
        locale: 'en', 
        translations: ['Hello World' => 'Hallo Welt'],
        priority: 10,
    ),
);

$translations = $resources->locale('en')->translations();
/*Array (
    [Hello World] => Hallo Welt
)*/

⚠️ You must call locale() or locales() before all() or translations() method if you have added (sub or lazy) resources, otherwise they will not get created.

foreach($resources->locale('en')->all() as $resource) {
    var_dump($resource instanceof ResourceInterface);
    // bool(true)
}

Files Resources

Create Files Resources

use Tobento\Service\Translation\FilesResources;
use Tobento\Service\Dir\Dirs;
use Tobento\Service\Translation\ResourcesInterface;

$resources = new FilesResources(
    (new Dirs())->dir(dir: 'private/trans/', group: 'front', priority: 10)
);

var_dump($resources instanceof ResourcesInterface);
// bool(true)

Directory Structure

Files starting with the locale are stored as * resource name. They are all fetched and merged together on the first translations request.
Files not starting with the locale are only loaded on the first resource request. Furthermore, files named like routes.shop.json and routes.blog.json are merged together as resource name routes.

private/
    trans/
        en/
            en.php
            en.json
            en-shop.json
            shop.json
            routes.shop.json
            routes.blog.json
        de-CH/
            de-CH.json
            de-CH-shop.json
            shop.json
            routes.shop.json

Supported Files

Currently supported files are json and php.

json

{
    "Using Real Message": "Using Real Message",
    "usingKeywordMessage": "Using Keyword Message"
}

php

return [
    'Using Real Message' => 'Using Real Message',
    'usingKeywordMessage' => 'Using Keyword Message',
];

Supporting Other Files

You may support others files by providing your own resource factory:

use Tobento\Service\Translation\FilesResources;
use Tobento\Service\Dir\Dirs;
use Tobento\Service\Translation\ResourceFactory;
use Tobento\Service\Translation\ResourceInterface;
use Tobento\Service\Filesystem\File;

class CustomResourceFactory extends ResourceFactory
{
    /**
     * Create a new Resource from file.
     *
     * @param string|File $file
     * @param string $locale
     * @param string $group
     * @param int $priority
     * @return ResourceInterface
     */    
    public function createResourceFromFile(
        string|File $file,
        string $locale,
        string $group = 'default',
        int $priority = 0,
    ): ResourceInterface {
        
        // Create your custom resource for the specific file extension
        
        // Otherwise use parent
        return parent::createResourceFromFile($file, $locale, $group, $priority);
    }
}

$resources = new FilesResources(
    (new Dirs())->dir(dir: 'private/trans/', group: 'front', priority: 10),
    new CustomResourceFactory()
);

Modifiers

Create Modifiers

use Tobento\Service\Translation\Modifiers;
use Tobento\Service\Translation\ModifiersInterface;
use Tobento\Service\Translation\Modifier\ParameterReplacer;

$modifiers = new Modifiers(
    new ParameterReplacer(),
);

var_dump($modifiers instanceof ModifiersInterface);
// bool(true)

Add Modifier

use Tobento\Service\Translation\Modifiers;
use Tobento\Service\Translation\Modifier\ParameterReplacer;

$modifiers = new Modifiers();
$modifiers->add(new ParameterReplacer());

Get all modifiers

use Tobento\Service\Translation\Modifiers;
use Tobento\Service\Translation\Modifier\ParameterReplacer;
use Tobento\Service\Translation\ModifierInterface;

$modifiers = new Modifiers(new ParameterReplacer());

$allModifiers = $modifiers->all();
// array<int, ModifierInterface>

Modify message

use Tobento\Service\Translation\Modifiers;
use Tobento\Service\Translation\Modifier\ParameterReplacer;

$modifiers = new Modifiers(
    new ParameterReplacer(),
);

[$message, $parameters] = $modifiers->modify(
    message: 'Hi :name',
    parameters: [':name' => 'John'],
);

var_dump($message);
// string(7) "Hi John"

Pluralization

use Tobento\Service\Translation\Modifier\Pluralization;

$modifier = new Pluralization(key: 'count');

[$message, $parameters] = $modifier->modify(
    message: 'There is one apple|There are many apples',
    parameters: ['count' => 5],
);

var_dump($message);
// string(21) "There are many apples"

[$message, $parameters] = $modifier->modify(
    message: 'There is one apple|There are many apples',
    parameters: ['count' => 1],
);

var_dump($message);
// string(18) "There is one apple"

Parameter Replacer

use Tobento\Service\Translation\Modifier\ParameterReplacer;

$modifier = new ParameterReplacer();

[$message, $parameters] = $modifier->modify(
    message: 'Hi :name',
    parameters: [':name' => 'John'],
);

var_dump($message);
// string(7) "Hi John"

Missing Translation Handler

You may add a logger to log missing messages:

use Tobento\Service\Translation\Translator;
use Tobento\Service\Translation\Resources;
use Tobento\Service\Translation\Resource;
use Tobento\Service\Translation\Modifiers;
use Tobento\Service\Translation\MissingTranslationHandler;
use Psr\Log\LoggerInterface;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$logger = new Logger('name');
$logger->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING));

$translator = new Translator(
    new Resources(
        new Resource('*', 'de', [
            'Hello World' => 'Hallo Welt',
        ]),
    ),
    new Modifiers(),
    new MissingTranslationHandler($logger), // any PSR-3 logger
);

var_dump($translator->trans('Hello World'));

Credits