Skip to content

Latest commit

 

History

History
507 lines (357 loc) · 15.5 KB

File metadata and controls

507 lines (357 loc) · 15.5 KB

JsTranslationBundle

A pretty nice way to expose your Symfony translation messages to your client applications.

Important: This documentation has been written for version 2.0.0 and above of this bundle. For version 1.x, please read: https://github.com/willdurand/BazingaJsTranslationBundle/blob/1.2.1/Resources/doc/index.md. Also, you might be interested in this UPGRADE guide.

Installation

Require via Composer

Install the bundle:

composer require "willdurand/js-translation-bundle"

Register the bundle in app/AppKernel.php:

<?php
// app/AppKernel.php
public function registerBundles()
{
    return array(
        // ...
        new Bazinga\Bundle\JsTranslationBundle\BazingaJsTranslationBundle(),
    );
}

Register the routing in app/config/routing.yml (optional: Because the dump command does not depend on the router component):

# app/config/routing.yml
_bazinga_jstranslation:
    resource: "@BazingaJsTranslationBundle/Resources/config/routing/routing.yml"

Publish assets:

php bin/console assets:install --symlink

Require via NPM (optional)

Install the package:

npm install bazinga-translator --save

This step is optional because the files exposed by the npm package are also part of the composer bundle. Normally you would do this if you prefer to keep all your front-end dependencies in one place, or if you wish to include the Translator object as a module dependency in your JS files.

Important: it is strongly recommended that you use the same version of the composer bundle and the npm package.

Usage

To use the Translator object in your JS files you can either load it globally or require / import it as a module.

  • To load it globally add the following line to your template:
<script src="{{ asset('bundles/bazingajstranslation/js/translator.min.js') }}"></script>
  • To load it as a module you must be using a module bundler, like webpack and it is recommended that you install the translator via npm. Then in your JS files you can do:
// ES2015
import Translator from 'bazinga-translator';
// ES5
var Translator = require('bazinga-translator');

Then add the current application's locale into your layout, by adding a lang attribute to the html tag:

<html lang="{{ app.request.locale|split('_')[0] }}">

Now, you are done with the basic setup, and you can specify the translation files you want to load.

Load Translations

Loading translations is a matter of adding a new script tag as follows:

<script src="{{ url('bazinga_jstranslation_js') }}"></script>

This will use the current locale and will return the translated messages found in each messages.CURRENT_LOCALE.* files of your project.

In case you do not want to expose an entire translation domain to your frontend, you can manually add translations to the translator collections. This simulates the way how the above script would add translations, but allows you to use any other renderer (like Twig or php) to make translations accessible

<script>
/**
 * Adds a translation entry.
 *
 * @param {String} id         The message id
 * @param {String} message    The message to register for the given id
 * @param {String} [domain]   The domain for the message or null to use the default
 * @param {String} [locale]   The locale or null to use the default
 * @return {Object}           Translator
 * @api public
 */
Translator.add(
    'translation_key',
    '{{ 'translation_key'|trans }}',
    'messages',
    'en'
);
</script>

Manually adding single translations allows your translators to change the translation or placeholder ordering without the need of a separate translation domain, or without having to change the Twig or js view.

Domains

You can add translations that are bound to a given domain:

<script src="{{ url('bazinga_jstranslation_js', { 'domain': 'DOMAIN_NAME' }) }}"></script>

This will use the current locale and will return the translated messages found in each DOMAIN_NAME.CURRENT_LOCALE.* file of your project.

Locales

You can use the locales query parameter to get translations in a specific language, or to load translation messages in several languages at once:

<script src="{{ url('bazinga_jstranslation_js', { 'domain': 'DOMAIN_NAME', 'locales': 'MY_LOCALE' }) }}"></script>

This will return the translated messages found in each DOMAIN_NAME.MY_LOCALE.* files of your project.

<script src="{{ url('bazinga_jstranslation_js', { 'domain': 'DOMAIN_NAME', 'locales': 'fr,en' }) }}"></script>

This will return the translated messages found in each DOMAIN_NAME.(fr|en).* file of your project.

Loading via JSON

Alternatively, you can load your translated messages via JSON (e.g. using the fetch API, jQuery's ajax() or RequireJS's text plugin). Just amend the above mentioned URLs to also contain the '_format': 'json' parameter like so:

{{ url('bazinga_jstranslation_js', { '_format': 'json' }) }}

Then, feed the translator via Translator.fromJSON(myRetrievedJSONString).

The dump Command

This bundle provides a command to dump the translation files:

php bin/console bazinga:js-translation:dump [target] [--format=js|json] [--pattern=/translations/{domain}.{_format}] [--merge-domains]

The optional target argument allows you to override the target directory to dump JS translation files in. By default, it generates files in the public/js/ directory.

The --format option allows you to specify which formats must be included in the output. If you only need JSON files in your project you can do --format=json.

The --pattern option allows you to specify the url pattern that will be generated when generating the file with the routes (E.g: /translations/{domain}.{_format}). There is no dependency with the router component.

The --merge-domains option when set will generate only one file per locale with all the domains in it. For cases where you prefer to load all language strings at once.

You have to load a config.js file, which contains the configuration for the JS Translator, then you can load all translation files that have been dumped. Note that dumped files don't contain any configuration, they only add messages to the JS Translator.

Assetic

The command below is useful if you use Assetic:

{% javascripts
    'bundles/bazingajstranslation/js/translator.min.js'
    'js/translations/config.js'
    'js/translations/*/*.js' %}
    <script src="{{ asset_url }}"></script>
{% endjavascripts %}

In the example above, all translation files from your entire project will be loaded. Of course you can load specific domains: js/translations/admin/*.js.

The default translation URLs let a controller dump the translations. If you make use of the Assetic, you need to manually dump the translations each time a translation changes because the Assetic links will point to a static file.

The JS Translator

The Translator object implements the Symfony2 TranslatorInterface and provides the same trans() and transChoice() methods:

Translator.trans('key', {}, 'DOMAIN_NAME');
// the translated message or undefined

Translator.transChoice('key', 1, {}, 'DOMAIN_NAME');
// the translated message or undefined

Note: The JavaScript is AMD ready.

Message Placeholders / Parameters

The trans() method accepts a second argument that takes an array of parameters:

Translator.trans('key', { "foo" : "bar" }, 'DOMAIN_NAME');
// will replace each "%foo%" in the message by "bar".

You can override the placeholder delimiters by setting the placeHolderSuffix and placeHolderPrefix attributes.

The transChoice() method accepts this array of parameters as third argument:

Translator.transChoice('key', 123, { "foo" : "bar" }, 'DOMAIN_NAME');
// will replace each "%foo%" in the message by "bar".

Read the official documentation about Symfony2 message placeholders.

Pluralization

Probably the best feature provided by this bundle! It allows you to use pluralization exactly like you would do using the Symfony Translator component.

# app/Resources/messages.en.yml
apples: "{0} There is no apples|{1} There is one apple|]1,19] There are %count% apples|[20,Inf] There are many apples"
Translator.locale = 'en';

Translator.transChoice('apples', 0, {"count" : 0});
// will return "There is no apples"

Translator.transChoice('apples', 1, {"count" : 1});
// will return "There is one apple"

Translator.transChoice('apples', 2, {"count" : 2});
// will return "There are 2 apples"

Translator.transChoice('apples', 10, {"count" : 10});
// will return "There are 10 apples"

Translator.transChoice('apples', 19, {"count" : 19});
// will return "There are 19 apples"

Translator.transChoice('apples', 20, {"count" : 20});
// will return "There are many apples"

Translator.transChoice('apples', 100, {"count" : 100});
// will return "There are many apples"

For more information, read the official documentation about pluralization.

Using ICU MessageFormat

Like Symfony, the bundle supports ICU MessageFormat. It's a more advanced syntax that allows you to handle placeholders, singular/plural, number, date, time, conditions, etc... see some examples.

Installation

The bundle requires on an external library intl-messageformat.

You can either load it globally, e.g. from a CDN:

<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-messageformat/9.0.2/intl-messageformat.min.js" integrity="sha512-uGIOqaLIi8I30qAnPLfrEnecDDi08AcCrg7gzGp/XrDafLJl/NIilHwAm1Wl2FLiTSf10D5vM70108k3oMjK5Q==" crossorigin="anonymous"></script>
<script src="{{ url('bazinga_jstranslation_js') }}"></script>

Or use NPM if you use an module bundler:

npm install intl-messageformat --save

intl-messageformat depends of Intl. If you targets old browser you will need to use a polyfill, for example Andy Earnshaw's Intl polyfill.

Usage

You must define your translations key in domain suffixed by +intl-icu, e.g.: messages.en.yaml becomes messages+intl-icu.en.yaml.

Given the following translations file:

# translations/messages+intl-icu.en.yaml
hello_name: Hello {name}!
name_has_x_projects: {name} has {projectCount, plural, =0 {no projects} one {# project} other {# projects}}

Then you can use the Translator like this:

Translator.trans('hello_name', { name: 'John' }, 'messages');
// will return "Hello John!"


// equivalent to Translator.transChoice() with the "classic" translations format
Translator.trans('name_has_x_projects', { name: 'John', projectCount: 0 }, 'messages');
// will return "John has no projects."

Translator.trans('name_has_x_projects', { name: 'John', projectCount: 1 }, 'messages');
// will return "John has 1 project."

Translator.trans('name_has_x_projects', { name: 'John', projectCount: 4 }, 'messages');
// will return "John has 4 projects."

For more information, read the Symfony documentation about ICU MessageFormat or the ICU User Guide.

Get The Locale

You can get the current locale by accessing the locale attribute:

Translator.locale;
// will return the current locale.

By default, the locale is set to the value defined in the lang attribute of the html tag.

Examples

Consider the following translation files:

# app/Resources/translations/Hello.fr.yml
foo: "Bar"
ba:
    bar: "Hello world"

placeholder: "Hello %username%!"
# app/Resources/translations/messages.fr.yml
placeholder: "Hello %username%, how are you?"

You can do:

Translator.trans('foo');
// will return 'Bar'

Translator.trans('foo', {}, 'Hello');
// will return 'Bar'

Translator.trans('ba.bar');
// will return 'Hello world'

Translator.trans('ba.bar', {}, 'Hello');
// will return 'Hello world'

Translator.trans('placeholder', {} , 'messages');
// will return 'Hello %username%, how are you?'

Translator.trans('placeholder', {} , 'Hello');
// will return 'Hello %username%!'

Translator.trans('placeholder', { "username" : "will" }, 'messages');
// will return 'Hello will, how are you?'

Translator.trans('placeholder', { "username" : "will" }, 'Hello');
// will return 'Hello will!'

Translator.trans('placeholder', { "username" : "will" });
// will return 'Hello will!' as the `Hello` messages have been loaded before the `messages` ones

More configuration

Locale Fallback

If some of your translations are not complete you can enable a fallback for untranslated messages:

bazinga_js_translation:
    locale_fallback: en  # It is recommended to set the same value used for the
                         # translator fallback.

Default Domain

You can define the default domain used when translation messages are added without any given translation domain:

bazinga_js_translation:
    default_domain:       messages

Active locales

By default, all locales are dumped. You can define an array of active locales:

bazinga_js_translation:
    active_locales:
        - fr
        - en

Active Domains

By default, all domains are dumped. You can define an array of active domains:

bazinga_js_translation:
    active_domains:
        - messages

Reference Configuration

# app/config/config*.yml
bazinga_js_translation:
    locale_fallback:      en
    default_domain:       messages

Testing

PHP

Setup the test suite using Composer:

$ composer install --dev

Run it using PHPUnit:

$ phpunit

JavaScript

You can run the JavaScript test suite using PhantomJS:

$ phantomjs Resources/js/run-qunit.js file://`pwd`/Resources/js/index.html