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.
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
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.
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 vianpm
. 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.
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.
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.
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.
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)
.
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.
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 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.
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.
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.
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.
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.
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.
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.
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
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.
You can define the default domain used when translation messages are added without any given translation domain:
bazinga_js_translation:
default_domain: messages
By default, all locales are dumped. You can define an array of active locales:
bazinga_js_translation:
active_locales:
- fr
- en
By default, all domains are dumped. You can define an array of active domains:
bazinga_js_translation:
active_domains:
- messages
# app/config/config*.yml
bazinga_js_translation:
locale_fallback: en
default_domain: messages
Setup the test suite using Composer:
$ composer install --dev
Run it using PHPUnit:
$ phpunit
You can run the JavaScript test suite using PhantomJS:
$ phantomjs Resources/js/run-qunit.js file://`pwd`/Resources/js/index.html