252 lines
9.0 KiB
JavaScript
252 lines
9.0 KiB
JavaScript
/*! gettext.js - Guillaume Potier - MIT Licensed */
|
|
var i18n = function (options) {
|
|
options = options || {};
|
|
this && (this.__version = '2.0.0');
|
|
|
|
// default values that could be overriden in i18n() construct
|
|
var defaults = {
|
|
domain: 'messages',
|
|
locale: (typeof document !== 'undefined' ? document.documentElement.getAttribute('lang') : false) || 'en',
|
|
plural_func: function (n) { return { nplurals: 2, plural: (n!=1) ? 1 : 0 }; },
|
|
ctxt_delimiter: String.fromCharCode(4) // \u0004
|
|
};
|
|
|
|
// handy mixins taken from underscode.js
|
|
var _ = {
|
|
isObject: function (obj) {
|
|
var type = typeof obj;
|
|
return type === 'function' || type === 'object' && !!obj;
|
|
},
|
|
isArray: function (obj) {
|
|
return toString.call(obj) === '[object Array]';
|
|
}
|
|
};
|
|
|
|
var
|
|
_plural_funcs = {},
|
|
_locale = options.locale || defaults.locale,
|
|
_domain = options.domain || defaults.domain,
|
|
_dictionary = {},
|
|
_plural_forms = {},
|
|
_ctxt_delimiter = options.ctxt_delimiter || defaults.ctxt_delimiter;
|
|
|
|
if (options.messages) {
|
|
_dictionary[_domain] = {};
|
|
_dictionary[_domain][_locale] = options.messages;
|
|
}
|
|
|
|
if (options.plural_forms) {
|
|
_plural_forms[_locale] = options.plural_forms;
|
|
}
|
|
|
|
// sprintf equivalent, takes a string and some arguments to make a computed string
|
|
// eg: strfmt("%1 dogs are in %2", 7, "the kitchen"); => "7 dogs are in the kitchen"
|
|
// eg: strfmt("I like %1, bananas and %1", "apples"); => "I like apples, bananas and apples"
|
|
// NB: removes msg context if there is one present
|
|
var strfmt = function (fmt) {
|
|
var args = arguments;
|
|
|
|
return fmt
|
|
// put space after double % to prevent placeholder replacement of such matches
|
|
.replace(/%%/g, '%% ')
|
|
// replace placeholders
|
|
.replace(/%(\d+)/g, function (str, p1) {
|
|
return args[p1];
|
|
})
|
|
// replace double % and space with single %
|
|
.replace(/%% /g, '%')
|
|
};
|
|
|
|
var removeContext = function(str) {
|
|
// if there is context, remove it
|
|
if (str.indexOf(_ctxt_delimiter) !== -1) {
|
|
var parts = str.split(_ctxt_delimiter);
|
|
return parts[1];
|
|
}
|
|
|
|
return str;
|
|
};
|
|
|
|
var expand_locale = function(locale) {
|
|
var locales = [locale],
|
|
i = locale.lastIndexOf('-');
|
|
while (i > 0) {
|
|
locale = locale.slice(0, i);
|
|
locales.push(locale);
|
|
i = locale.lastIndexOf('-');
|
|
}
|
|
return locales;
|
|
};
|
|
|
|
var normalizeLocale = function (locale) {
|
|
// Convert locale to BCP 47. If the locale is in POSIX format, locale variant and encoding is discarded.
|
|
locale = locale.replace('_', '-');
|
|
var i = locale.search(/[.@]/);
|
|
if (i != -1) locale = locale.slice(0, i);
|
|
return locale;
|
|
};
|
|
|
|
var getPluralFunc = function (plural_form) {
|
|
// Plural form string regexp
|
|
// taken from https://github.com/Orange-OpenSource/gettext.js/blob/master/lib.gettext.js
|
|
// plural forms list available here http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html
|
|
var pf_re = new RegExp('^\\s*nplurals\\s*=\\s*[0-9]+\\s*;\\s*plural\\s*=\\s*(?:\\s|[-\\?\\|&=!<>+*/%:;n0-9_\(\)])+');
|
|
|
|
var match = plural_form.match(pf_re);
|
|
|
|
if (!match || match[0] !== plural_form)
|
|
throw new Error(strfmt('The plural form "%1" is not valid', plural_form));
|
|
|
|
console.log('>>> Plural form:', plural_form);
|
|
|
|
// Careful here, this is a hidden eval() equivalent..
|
|
// Risk should be reasonable though since we test the plural_form through regex before
|
|
// taken from https://github.com/Orange-OpenSource/gettext.js/blob/master/lib.gettext.js
|
|
// TODO: should test if https://github.com/soney/jsep present and use it if so
|
|
return new Function("n", 'var plural, nplurals; '+ plural_form +' return { nplurals: nplurals, plural: (plural === true ? 1 : (plural ? plural : 0)) };');
|
|
};
|
|
|
|
// Proper translation function that handle plurals and directives
|
|
// Contains juicy parts of https://github.com/Orange-OpenSource/gettext.js/blob/master/lib.gettext.js
|
|
var t = function (messages, n, options /* ,extra */) {
|
|
// Singular is very easy, just pass dictionnary message through strfmt
|
|
if (!options.plural_form)
|
|
return strfmt.apply(this, [removeContext(messages[0])].concat(Array.prototype.slice.call(arguments, 3)));
|
|
|
|
var plural;
|
|
|
|
// if a plural func is given, use that one
|
|
if (options.plural_func) {
|
|
plural = options.plural_func(n);
|
|
|
|
// if plural form never interpreted before, do it now and store it
|
|
} else if (!_plural_funcs[_locale]) {
|
|
_plural_funcs[_locale] = getPluralFunc(_plural_forms[_locale]);
|
|
plural = _plural_funcs[_locale](n);
|
|
|
|
// we have the plural function, compute the plural result
|
|
} else {
|
|
plural = _plural_funcs[_locale](n);
|
|
}
|
|
|
|
// If there is a problem with plurals, fallback to singular one
|
|
if ('undefined' === typeof plural.plural || plural.plural > plural.nplurals || messages.length <= plural.plural)
|
|
plural.plural = 0;
|
|
|
|
return strfmt.apply(this, [removeContext(messages[plural.plural])].concat(Array.prototype.slice.call(arguments, 3)));
|
|
};
|
|
|
|
return {
|
|
strfmt: strfmt, // expose strfmt util
|
|
expand_locale: expand_locale, // expose expand_locale util
|
|
|
|
// Declare shortcuts
|
|
__: function () { return this.gettext.apply(this, arguments); },
|
|
_n: function () { return this.ngettext.apply(this, arguments); },
|
|
_p: function () { return this.pgettext.apply(this, arguments); },
|
|
|
|
setMessages: function (domain, locale, messages, plural_forms) {
|
|
if (!domain || !locale || !messages)
|
|
throw new Error('You must provide a domain, a locale and messages');
|
|
|
|
if ('string' !== typeof domain || 'string' !== typeof locale || !_.isObject(messages))
|
|
throw new Error('Invalid arguments');
|
|
|
|
locale = normalizeLocale(locale);
|
|
|
|
if (plural_forms)
|
|
_plural_forms[locale] = plural_forms;
|
|
|
|
if (!_dictionary[domain])
|
|
_dictionary[domain] = {};
|
|
|
|
_dictionary[domain][locale] = messages;
|
|
|
|
return this;
|
|
},
|
|
loadJSON: function (jsonData, domain) {
|
|
if (!_.isObject(jsonData))
|
|
jsonData = JSON.parse(jsonData);
|
|
|
|
if (!jsonData[''] || !jsonData['']['language'] || !jsonData['']['plural-forms'])
|
|
throw new Error('Wrong JSON, it must have an empty key ("") with "language" and "plural-forms" information');
|
|
|
|
var headers = jsonData[''];
|
|
delete jsonData[''];
|
|
|
|
return this.setMessages(domain || defaults.domain, headers['language'], jsonData, headers['plural-forms']);
|
|
},
|
|
setLocale: function (locale) {
|
|
_locale = normalizeLocale(locale);
|
|
return this;
|
|
},
|
|
getLocale: function () {
|
|
return _locale;
|
|
},
|
|
// getter/setter for domain
|
|
textdomain: function (domain) {
|
|
if (!domain)
|
|
return _domain;
|
|
_domain = domain;
|
|
return this;
|
|
},
|
|
gettext: function (msgid /* , extra */) {
|
|
return this.dcnpgettext.apply(this, [undefined, undefined, msgid, undefined, undefined].concat(Array.prototype.slice.call(arguments, 1)));
|
|
},
|
|
ngettext: function (msgid, msgid_plural, n /* , extra */) {
|
|
return this.dcnpgettext.apply(this, [undefined, undefined, msgid, msgid_plural, n].concat(Array.prototype.slice.call(arguments, 3)));
|
|
},
|
|
pgettext: function (msgctxt, msgid /* , extra */) {
|
|
return this.dcnpgettext.apply(this, [undefined, msgctxt, msgid, undefined, undefined].concat(Array.prototype.slice.call(arguments, 2)));
|
|
},
|
|
dcnpgettext: function (domain, msgctxt, msgid, msgid_plural, n /* , extra */) {
|
|
domain = domain || _domain;
|
|
|
|
if ('string' !== typeof msgid)
|
|
throw new Error(this.strfmt('Msgid "%1" is not a valid translatable string', msgid));
|
|
|
|
var
|
|
translation,
|
|
options = { plural_form: false },
|
|
key = msgctxt ? msgctxt + _ctxt_delimiter + msgid : msgid,
|
|
exist,
|
|
locale,
|
|
locales = expand_locale(_locale);
|
|
|
|
for (var i in locales) {
|
|
locale = locales[i];
|
|
exist = _dictionary[domain] && _dictionary[domain][locale] && _dictionary[domain][locale][key];
|
|
|
|
// because it's not possible to define both a singular and a plural form of the same msgid,
|
|
// we need to check that the stored form is the same as the expected one.
|
|
// if not, we'll just ignore the translation and consider it as not translated.
|
|
if (msgid_plural) {
|
|
exist = exist && "string" !== typeof _dictionary[domain][locale][key];
|
|
} else {
|
|
exist = exist && "string" === typeof _dictionary[domain][locale][key];
|
|
}
|
|
if (exist) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!exist) {
|
|
translation = msgid;
|
|
options.plural_func = defaults.plural_func;
|
|
} else {
|
|
translation = _dictionary[domain][locale][key];
|
|
}
|
|
|
|
// Singular form
|
|
if (!msgid_plural)
|
|
return t.apply(this, [[translation], n, options].concat(Array.prototype.slice.call(arguments, 5)));
|
|
|
|
// Plural one
|
|
options.plural_form = true;
|
|
return t.apply(this, [exist ? translation : [msgid, msgid_plural], n, options].concat(Array.prototype.slice.call(arguments, 5)));
|
|
}
|
|
};
|
|
};
|
|
|
|
export default i18n;
|