ember-intl
Version:
Ember toolbox for internationalization.
347 lines (268 loc) • 12.6 kB
Markdown
# ember-intl
[![npm Version][npm-badge]][npm]
[![Build Status][travis-badge]][travis]
[](http://emberobserver.com/addons/ember-intl)
[![Ember badge][ember-badge]][embadge]
This library provides Ember Handlebar helpers and a localization service. The service, and helpers, provide a way to format dates, numbers, strings messages, including pluralization.
## Notable Features
* Addon support (addons can provide translations up to the project application)
* Built on top of native standards ([Intl API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) and ICU message syntax)
* Translations are able to be lazily loaded (not bundled with the app code)
* Formatting for relative time, datetime, and numbers
* Handlebar helpers and service API
## Installation
* `ember install ember-intl`
* Polyfill the Intl.js API (required for Safari/PhantomJS, and legacy browser)
* Instructions on [loading from assets directory][]
* Instructions on [conditionally loading the polyfill based on User-Agent][]
* Instructions on [disabling polyfill][]
[loading from assets directory]: https://github.com/jasonmit/ember-intl/wiki/Intl.js-Polyfill#loading-files-from-assets
[conditionally loading the polyfill based on User-Agent]: https://github.com/jasonmit/ember-intl/wiki/Intl.js-Polyfill#polyfillio
[disabling polyfill]: https://github.com/jasonmit/ember-intl/wiki/Intl.js-Polyfill#disabling
## Translations
Translations are defined in `/translations`, *outside of `app`* in either JSON or YAML format. Example of `/translations/en-us.yaml`:
```yaml
# en-us
product:
info: '{product} will cost {price, number, USD} if ordered by {deadline, date, time}'
title: 'Hello world!'
html:
info: '<strong>{product}</strong> will cost <em>{price, number, USD}</em> if ordered by {deadline, date, time}'
```
If you wish, you can organize your translations into subdirectories such as `/translations/login-page/en-us.yaml` and `translations/purchase-page/en-us.yaml`.
Translation keys containing periods, i.e., `foo.bar`, conflicts with internal accessors -- which assumes nested objects and therefor triggers errors like: `Error: Property set failed: object in path "foo" could not be found or was destroyed`. However, this is supported as of 2.5.x and to enable run `ember g ember-intl-dot-notation`.
### Translation Compilation
At build time, ember-intl walks all of the translations within the project and attempts to locate missing translations keys. This is done through the `baseLocale` config property. If translations keys are found on the base but not on other locales, a warning is written to the console and ember-intl will automatically use the value from the base locale as a filler.
```js
// config/environment.js
return {
intl: {
baseLocale: 'en-us' // default build-time locale
}
};
```
## Setting runtime locale
Open, or create, `app/routes/application.js` and within `beforeModel` invoke `intl.setLocale`. Example:
```js
// app/routes/application.js
export default Ember.Route.extend({
intl: Ember.inject.service(),
beforeModel() {
// define the app's runtime locale
// For example, here you would maybe do an API lookup to resolver
// which locale the user should be targeted and perhaps lazily
// load translations using XHR and calling intl's `addTranslation`/`addTranslations`
// method with the results of the XHR request
return this.get('intl').setLocale('en-us');
// OR for those that sideload, an array is accepted to handle fallback lookups
// en-ca is the primary locale, en-us is the fallback.
// this is optional, and likely unnecessary if you define baseLocale (see below)
// The primary usecase is if you side load all translations
//
// return this.get('intl').setLocale(['en-ca', 'en-us']);
}
});
```
## Helper Examples
### Format Number
Formats numbers using [`Intl.NumberFormat`][Intl-NF], and returns the formatted string value.
```hbs
{{format-number num}}
{{format-number num format='EUR'}}
{{format-number num style='currency' currency='USD'}}
{{format-number undefined fallback='nothing'}}
```
Or programmatically convert a number within any Ember Object.
```js
export default Ember.Component.extend({
intl: Ember.inject.service(),
computedNumber: Ember.computed('cost', function() {
return this.get('intl').formatNumber(this.get('cost')/*, optional options hash */);
})
});
```
#### Format Number Options
[List of supported format number options](https://github.com/jasonmit/ember-intl/wiki/Format-Number-Options)
### Format Date
Formats dates using [`Intl.DateTimeFormat`][Intl-DTF], and returns the formatted string value.
```hbs
{{format-date now weekday='long' timeZone='UTC'}}
{{format-date now hour='numeric' minute='numeric' hour12=false}}
{{format-date undefined fallback='nothing'}
```
Or programmatically convert a date within any Ember Object.
```js
export default Ember.Component.extend({
intl: Ember.inject.service(),
computedNow: Ember.computed(function() {
return this.get('intl').formatDate(new Date()/*, optional options hash */);
})
});
```
#### Format Date Options
[List of supported format date options](https://github.com/jasonmit/ember-intl/wiki/Format-Date-&-Time-Options)
### Format Time
This is just like the `{{format-date}}` helper, except it will reference any string-named `format` from [`formats.time`](#dataintlformats).
```hbs
{{format-time now format='hhmmss'}}
{{format-time now hour='numeric' minute='numeric' hour12=false}}
{{format-time undefined fallback='nothing'}}
```
Or programmatically convert a time within any Ember Object.
```js
// example
export default Ember.Component.extend({
intl: Ember.inject.service(),
computedNow: Ember.computed(function() {
return this.get('intl').formatTime(new Date()/*, optional options hash */);
})
});
```
#### Format Time Options
[List of supported format date options](https://github.com/jasonmit/ember-intl/wiki/Format-Date-&-Time-Options)
### Format Relative
Formats dates relative to "now" using [`IntlRelativeFormat`][Intl-RF], and returns the formatted string value.
```js
export default Ember.Component.extend({
timestamp: Ember.computed(function() {
var date = new Date();
date.setDate(date.getDate() - 3);
return date;
})
});
```
```hbs
{{format-relative timestamp}} -> 3 days ago
{{format-relative undefined fallback='unknown'}} -> unknown
```
Or programmatically convert a relative time within any Ember Object.
```js
export default Ember.Component.extend({
intl: Ember.inject.service(),
yesterday: Ember.computed(function() {
var date = new Date();
return this.get('intl').formatRelative(date.setDate(date.getDate() - 1)/*, optional options hash */);
})
});
```
#### Live Relative Timestamp
Recompute the relative timestamp on an interval by passing an `interval` argument (in milliseconds).
```hbs
{{format-relative now interval=1000}} -> now, 1 second ago, 2 seconds ago, etc. (will recompute every 1s)
```
#### Format Relative Options
[List of supported format date options](https://github.com/jasonmit/ember-intl/wiki/Format-Relative-Options)
### Format Message
Formats [ICU Message][ICU] strings with the given values supplied as the hash arguments. A short-hand form of the `{{format-message}}` is `{{t}}`.
```
You have {numPhotos, plural,
=0 {no photos.}
=1 {one photo.}
other {# photos.}}
```
```hbs
{{t 'product.info'
product='Apple watch'
price=200
deadline=yesterday}}
{{t boundProperty
name='Jason'
numPhotos=num
takenDate=yesterday}}
```
Or programmatically convert a message within any Ember Object.
```js
export default Ember.Component.extend({
intl: Ember.inject.service(),
yesterday: Ember.computed(function() {
return this.get('intl').formatMessage('Hello {name}', { name: 'Jason' });
})
});
```
#### Passing a string literal to Format Message
This is done by using the `{{l}}` (lowercase L) helper as a subexpression. This is useful for computed properties where you are programmatically constructing a translation string.
```hbs
{{t (l '{name} took {numPhotos, plural,\n =0 {no photos}\n =1 {one photo}\n other {# photos}\n} on {takenDate, date, long}')
name='Jason'
numPhotos=num
takenDate=yesterday}}
```
### Format HTML Message
This delegates to the `{{t}}` helper, but will first HTML-escape all of the hash argument values. This allows the `message` string to contain HTML and it will be considered safe since it's part of the template and not user-supplied data.
```hbs
{{format-html-message 'product.html.info'
product='Apple watch'
price=200
deadline=yesterday}}
{{format-html-message (l '<strong>{numPhotos}</strong>')
numPhotos=(format-number num)}}
```
## Named Formats
Specifying format options (e.g.: style="currency" currency="USD") in every use of format helper can become a problem in large code bases, and isn't DRY. Instead, you can provide named formats through the use of exporting a POJO from `app/formats`. All helpers accept a `format` property which accepts a key that maps to the format option under its respected type (time, date, number, relative).
For example:
```js
// app/formats.js
export default {
date: {
hhmmss: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}
}
};
```
```hbs
{{format-date 'Thu Jan 23 2014 13:00:44' format='hhmmss'}}
```
```js
this.get('intl').formatDate('Thu Jan 23 2014 13:00:44', {
format: 'hhmmss'
})
```
Output of both the helper and the programmatic example:
> 1:00:44 PM
## Polyfill
There are two options on how to load the Intl.js polyfill, either through the polyfill which ships with ember-intl or through polyfill.io. Both of which are documented: https://github.com/jasonmit/ember-intl/wiki/Intl.js-Polyfill
## Helper Options
* All helpers accept optional arguments:
* `locale` argument to explicitly pass/override the application locale
* `format` argument which you pass in a key corresponding to a format configuration in `app/formats.js`
## Writing Unit Tests
Phantom does support the Intl API, so in order for for you ember-intl to work in a browser which does not support the Intl API, it needs to be polyfilled.
To resolve this, add the following above all script tags in `tests/index.html`:
```html
<script src="assets/intl/intl.complete.js"></script>
```
## Asynchronously loading translations
Asynchronously loading translations instead of bundling translations within `app.js` are fully-supported as of 2.x.
https://github.com/jasonmit/ember-intl/wiki/Asynchronously-loading-translations
### Testing with ember-intl
* [Unit testing](https://github.com/jasonmit/ember-intl/wiki/Testing---Unit)
* [Integration testing](https://github.com/jasonmit/ember-intl/wiki/Testing---Integration)
## Common Errors
> `date value is not finite in DateTimeFormat.format()`
Browser vendors implement date/time parsing differently. For example, the following will parse correctly in Chrome but fail in Firefox: `new Intl.DateTimeFormat().format('2015-04-21 20:47:31 GMT');`
The solution is the ensure that the value you are passing in is in a format which is valid for the `Date` constructor. This library currently does not try and normalize date strings outside of what the browser already implements.
## Running
* `ember server`
* Visit your app at http://localhost:4200.
## Running Tests
* `ember test`
* `ember test --server`
[npm]: https://www.npmjs.org/package/ember-intl
[npm-badge]: https://img.shields.io/npm/v/ember-intl.svg?style=flat-square
[travis]: https://travis-ci.org/jasonmit/ember-intl
[travis-badge]: https://img.shields.io/travis/jasonmit/ember-intl/master.svg?style=flat-square
[embadge]: http://embadge.io/
[ember-badge]: http://embadge.io/v1/badge.svg?start=1.13.0
[Intl-RF]: https://github.com/yahoo/intl-relativeformat
[Intl-MF]: https://github.com/yahoo/intl-messageformat
[Intl]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl
[Intl-NF]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat
[Intl-DTF]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
[ICU]: http://userguide.icu-project.org/formatparse/messages
[CLDR]: http://cldr.unicode.org/
[Intl.js]: https://github.com/andyearnshaw/Intl.js
[LICENSE]: https://github.com/yahoo/yahoo-intl/blob/master/LICENSE
[FormatJS]: http://formatjs.io/