ember-intl
Version:
A internationalization toolbox for ambitious applications.
275 lines (219 loc) • 7.15 kB
JavaScript
/**
* Copyright 2015, Yahoo! Inc.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
import { getOwner } from '@ember/application';
import { computed, get, set } from '@ember/object';
import { readOnly } from '@ember/object/computed';
import Evented from '@ember/object/evented';
import { assert } from '@ember/debug';
import { makeArray } from '@ember/array';
import Service from '@ember/service';
import { next, cancel } from '@ember/runloop';
import { FormatDate, FormatMessage, FormatNumber, FormatRelative, FormatTime } from '../-private/formatters';
import isArrayEqual from '../-private/utils/is-array-equal';
import normalizeLocale from '../-private/utils/normalize-locale';
import getDOM from '../-private/utils/get-dom';
import hydrate from '../-private/utils/hydrate';
import TranslationContainer from '../-private/store/container';
export default Service.extend(Evented, {
/** @public **/
formats: null,
/**
* Returns an array of registered locale names
*
* @property locales
* @public
*/
locales: readOnly('_translationContainer.locales'),
/** @public **/
locale: computed('_locale', {
set(_, localeName) {
const proposed = makeArray(localeName).map(normalizeLocale);
if (!isArrayEqual(proposed, this._locale)) {
set(this, '_locale', proposed);
cancel(this._timer);
this._timer = next(() => {
this.trigger('localeChanged');
this._updateDocumentLanguage(this._locale);
});
}
return this._locale;
},
get() {
return get(this, '_locale');
},
}),
/**
* Returns the first locale of the currently active locales
*
* @property primaryLocale
* @public
*/
primaryLocale: readOnly('locale.0'),
/** @public **/
formatRelative: createFormatterProxy('relative'),
/** @public **/
formatMessage: createFormatterProxy('message'),
/** @public **/
formatNumber: createFormatterProxy('number'),
/** @public **/
formatTime: createFormatterProxy('time'),
/** @public **/
formatDate: createFormatterProxy('date'),
/** @private **/
_translationContainer: null,
/** @private **/
_locale: null,
/** @private **/
_timer: null,
/** @private **/
_formatters: null,
/** @public **/
init() {
this._super(...arguments);
const initialLocale = get(this, 'locale') || ['en-us'];
this.setLocale(initialLocale);
this._owner = getOwner(this);
// Below issue can be ignored, as this is during the `init()` constructor.
// eslint-disable-next-line ember/no-assignment-of-untracked-properties-used-in-tracking-contexts
this._translationContainer = TranslationContainer.create();
this._formatters = this._createFormatters();
if (!this.formats) {
this.formats = this._owner.resolveRegistration('formats:main') || {};
}
hydrate(this);
},
willDestroy() {
this._super(...arguments);
cancel(this._timer);
},
/** @private **/
onError({ /* kind, */ error }) {
throw error;
},
/** @public **/
lookup(key, localeName) {
const localeNames = this._localeWithDefault(localeName);
let translation;
for (let i = 0; i < localeNames.length; i++) {
translation = this._translationContainer.lookup(localeNames[i], key);
if (translation !== undefined) {
break;
}
}
return translation;
},
/** @private **/
lookupAst(key, localeName, options = {}) {
const localeNames = this._localeWithDefault(localeName);
let translation;
for (let i = 0; i < localeNames.length; i++) {
translation = this._translationContainer.lookupAst(localeNames[i], key);
if (translation !== undefined) {
break;
}
}
if (translation === undefined && options.resilient !== true) {
const missingMessage = this._owner.resolveRegistration('util:intl/missing-message');
return missingMessage.call(this, key, localeNames, options);
}
return translation;
},
validateKeys(keys) {
return keys.forEach((key) => {
assert(
`[ember-intl] expected translation key "${key}" to be of type String but received: "${typeof key}"`,
typeof key === 'string'
);
});
},
/** @public **/
t(key, options = {}) {
let keys = [key];
if (options.default) {
if (Array.isArray(options.default)) {
keys = [...keys, ...options.default];
} else if (typeof options.default === 'string') {
keys = [...keys, options.default];
}
}
this.validateKeys(keys);
for (let index = 0; index < keys.length; index++) {
const key = keys[index];
const ast = this.lookupAst(key, options.locale, {
...options,
// Note: last iteration will throw with the last key that was missing
// in the future maybe the thrown error should include all the keys to help debugging
resilient: keys.length - 1 !== index,
});
if (ast) {
return this.formatMessage(ast, options);
}
}
},
/** @public **/
exists(key, localeName) {
const localeNames = this._localeWithDefault(localeName);
assert(`[ember-intl] locale is unset, cannot lookup '${key}'`, Array.isArray(localeNames) && localeNames.length);
return localeNames.some((localeName) => this._translationContainer.has(localeName, key));
},
/** @public */
setLocale(locale) {
set(this, 'locale', locale);
},
/** @public **/
addTranslations(localeName, payload) {
this._translationContainer.push(normalizeLocale(localeName), payload);
},
/** @public **/
translationsFor(localeName) {
return this._translationContainer.findTranslationModel(normalizeLocale(localeName), false);
},
/** @private **/
_localeWithDefault(localeName) {
if (!localeName) {
return get(this, '_locale') || [];
}
if (typeof localeName === 'string') {
return makeArray(localeName).map(normalizeLocale);
}
if (Array.isArray(localeName)) {
return localeName.map(normalizeLocale);
}
},
/** @private **/
_updateDocumentLanguage(locales) {
const dom = getDOM(this);
if (dom) {
const [primaryLocale] = locales;
const html = dom.documentElement;
html.setAttribute('lang', primaryLocale);
}
},
/** @private */
_createFormatters() {
const formatterConfig = {
onError: this.onError.bind(this),
readFormatConfig: () => this.formats,
};
return {
message: new FormatMessage(formatterConfig),
relative: new FormatRelative(formatterConfig),
number: new FormatNumber(formatterConfig),
time: new FormatTime(formatterConfig),
date: new FormatDate(formatterConfig),
};
},
});
function createFormatterProxy(name) {
return function serviceFormatterProxy(value, formatOptions) {
let locale;
if (formatOptions && formatOptions.locale) {
locale = this._localeWithDefault(formatOptions.locale);
} else {
locale = get(this, 'locale');
}
return this._formatters[name].format(locale, value, formatOptions);
};
}