315 lines
8.2 KiB
JavaScript
315 lines
8.2 KiB
JavaScript
'use strict';
|
|
|
|
const Debug = require('debug');
|
|
const debug = Debug('koa-locales');
|
|
const debugSilly = Debug('koa-locales:silly');
|
|
const ini = require('ini');
|
|
const util = require('util');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const ms = require('humanize-ms');
|
|
const assign = require('object-assign');
|
|
const yaml = require('js-yaml');
|
|
|
|
const DEFAULT_OPTIONS = {
|
|
defaultLocale: 'en-US',
|
|
queryField: 'locale',
|
|
cookieField: 'locale',
|
|
localeAlias: {},
|
|
writeCookie: true,
|
|
cookieMaxAge: '1y',
|
|
dir: undefined,
|
|
dirs: [path.join(process.cwd(), 'locales')],
|
|
functionName: '__',
|
|
};
|
|
|
|
module.exports = function (app, options) {
|
|
options = assign({}, DEFAULT_OPTIONS, options);
|
|
const defaultLocale = formatLocale(options.defaultLocale);
|
|
const queryField = options.queryField;
|
|
const cookieField = options.cookieField;
|
|
const cookieDomain = options.cookieDomain;
|
|
const localeAlias = options.localeAlias;
|
|
const writeCookie = options.writeCookie;
|
|
const cookieMaxAge = ms(options.cookieMaxAge);
|
|
const localeDir = options.dir;
|
|
const localeDirs = options.dirs;
|
|
const functionName = options.functionName;
|
|
const resources = {};
|
|
|
|
/**
|
|
* @Deprecated Use options.dirs instead.
|
|
*/
|
|
if (localeDir && localeDirs.indexOf(localeDir) === -1) {
|
|
localeDirs.push(localeDir);
|
|
}
|
|
|
|
for (let i = 0; i < localeDirs.length; i++) {
|
|
const dir = localeDirs[i];
|
|
|
|
if (!fs.existsSync(dir)) {
|
|
continue;
|
|
}
|
|
|
|
const names = fs.readdirSync(dir);
|
|
for (let j = 0; j < names.length; j++) {
|
|
const name = names[j];
|
|
const filepath = path.join(dir, name);
|
|
// support en_US.js => en-US.js
|
|
const locale = formatLocale(name.split('.')[0]);
|
|
let resource = {};
|
|
|
|
if (name.endsWith('.js') || name.endsWith('.json')) {
|
|
resource = flattening(require(filepath));
|
|
} else if (name.endsWith('.properties')) {
|
|
resource = ini.parse(fs.readFileSync(filepath, 'utf8'));
|
|
} else if (name.endsWith('.yml') || name.endsWith('.yaml')) {
|
|
resource = flattening(yaml.safeLoad(fs.readFileSync(filepath, 'utf8')));
|
|
}
|
|
|
|
resources[locale] = resources[locale] || {};
|
|
assign(resources[locale], resource);
|
|
}
|
|
}
|
|
|
|
debug('Init locales with %j, got %j resources', options, Object.keys(resources));
|
|
|
|
if (typeof app[functionName] !== 'undefined') {
|
|
console.warn('[koa-locales] will override exists "%s" function on app', functionName);
|
|
}
|
|
|
|
function gettext(locale, key, value) {
|
|
if (arguments.length === 0 || arguments.length === 1) {
|
|
// __()
|
|
// --('en')
|
|
return '';
|
|
}
|
|
|
|
const resource = resources[locale] || {};
|
|
|
|
let text = resource[key];
|
|
if (text === undefined) {
|
|
text = key;
|
|
}
|
|
|
|
debugSilly('%s: %j => %j', locale, key, text);
|
|
if (!text) {
|
|
return '';
|
|
}
|
|
|
|
if (arguments.length === 2) {
|
|
// __(locale, key)
|
|
return text;
|
|
}
|
|
if (arguments.length === 3) {
|
|
if (isObject(value)) {
|
|
// __(locale, key, object)
|
|
// __('zh', '{a} {b} {b} {a}', {a: 'foo', b: 'bar'})
|
|
// =>
|
|
// foo bar bar foo
|
|
return formatWithObject(text, value);
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
// __(locale, key, array)
|
|
// __('zh', '{0} {1} {1} {0}', ['foo', 'bar'])
|
|
// =>
|
|
// foo bar bar foo
|
|
return formatWithArray(text, value);
|
|
}
|
|
|
|
// __(locale, key, value)
|
|
return util.format(text, value);
|
|
}
|
|
|
|
// __(locale, key, value1, ...)
|
|
const args = new Array(arguments.length - 1);
|
|
args[0] = text;
|
|
for (let i = 2; i < arguments.length; i++) {
|
|
args[i - 1] = arguments[i];
|
|
}
|
|
return util.format.apply(util, args);
|
|
}
|
|
|
|
app[functionName] = gettext;
|
|
|
|
app.context[functionName] = function (key, value) {
|
|
if (arguments.length === 0) {
|
|
// __()
|
|
return '';
|
|
}
|
|
|
|
const locale = this.__getLocale();
|
|
if (arguments.length === 1) {
|
|
return gettext(locale, key);
|
|
}
|
|
if (arguments.length === 2) {
|
|
return gettext(locale, key, value);
|
|
}
|
|
const args = new Array(arguments.length + 1);
|
|
args[0] = locale;
|
|
for (let i = 0; i < arguments.length; i++) {
|
|
args[i + 1] = arguments[i];
|
|
}
|
|
return gettext.apply(this, args);
|
|
};
|
|
|
|
// 1. query: /?locale=en-US
|
|
// 2. cookie: locale=zh-TW
|
|
// 3. header: Accept-Language: zh-CN,zh;q=0.5
|
|
app.context.__getLocale = function () {
|
|
if (this.__locale) {
|
|
return this.__locale;
|
|
}
|
|
|
|
const cookieLocale = this.cookies.get(cookieField, { signed: false });
|
|
|
|
// 1. Query
|
|
let locale = this.query[queryField];
|
|
let localeOrigin = 'query';
|
|
|
|
// 2. Cookie
|
|
if (!locale) {
|
|
locale = cookieLocale;
|
|
localeOrigin = 'cookie';
|
|
}
|
|
|
|
// 3. Header
|
|
if (!locale) {
|
|
// Accept-Language: zh-CN,zh;q=0.5
|
|
// Accept-Language: zh-CN
|
|
let languages = this.acceptsLanguages();
|
|
if (languages) {
|
|
if (Array.isArray(languages)) {
|
|
if (languages[0] === '*') {
|
|
languages = languages.slice(1);
|
|
}
|
|
if (languages.length > 0) {
|
|
for (let i = 0; i < languages.length; i++) {
|
|
const lang = formatLocale(languages[i]);
|
|
if (resources[lang] || localeAlias[lang]) {
|
|
locale = lang;
|
|
localeOrigin = 'header';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
locale = languages;
|
|
localeOrigin = 'header';
|
|
}
|
|
}
|
|
|
|
// all missing, set it to defaultLocale
|
|
if (!locale) {
|
|
locale = defaultLocale;
|
|
localeOrigin = 'default';
|
|
}
|
|
}
|
|
|
|
// cookie alias
|
|
if (locale in localeAlias) {
|
|
const originalLocale = locale;
|
|
locale = localeAlias[locale];
|
|
debugSilly('Used alias, received %s but using %s', originalLocale, locale);
|
|
}
|
|
|
|
locale = formatLocale(locale);
|
|
|
|
// validate locale
|
|
if (!resources[locale]) {
|
|
debugSilly('Locale %s is not supported. Using default (%s)', locale, defaultLocale);
|
|
locale = defaultLocale;
|
|
}
|
|
|
|
// if header not send, set the locale cookie
|
|
if (writeCookie && cookieLocale !== locale && !this.headerSent) {
|
|
updateCookie(this, locale);
|
|
}
|
|
debug('Locale: %s from %s', locale, localeOrigin);
|
|
debugSilly('Locale: %s from %s', locale, localeOrigin);
|
|
this.__locale = locale;
|
|
this.__localeOrigin = localeOrigin;
|
|
return locale;
|
|
};
|
|
|
|
app.context.__getLocaleOrigin = function () {
|
|
if (this.__localeOrigin) return this.__localeOrigin;
|
|
this.__getLocale();
|
|
return this.__localeOrigin;
|
|
};
|
|
|
|
app.context.__setLocale = function (locale) {
|
|
this.__locale = locale;
|
|
this.__localeOrigin = 'set';
|
|
updateCookie(this, locale);
|
|
};
|
|
|
|
function updateCookie(ctx, locale) {
|
|
const cookieOptions = {
|
|
// make sure brower javascript can read the cookie
|
|
httpOnly: false,
|
|
maxAge: cookieMaxAge,
|
|
signed: false,
|
|
domain: cookieDomain,
|
|
overwrite: true,
|
|
};
|
|
ctx.cookies.set(cookieField, locale, cookieOptions);
|
|
debugSilly('Saved cookie with locale %s', locale);
|
|
}
|
|
};
|
|
|
|
function isObject(obj) {
|
|
return Object.prototype.toString.call(obj) === '[object Object]';
|
|
}
|
|
|
|
const ARRAY_INDEX_RE = /\{(\d+)\}/g;
|
|
function formatWithArray(text, values) {
|
|
return text.replace(ARRAY_INDEX_RE, function (orignal, matched) {
|
|
const index = parseInt(matched);
|
|
if (index < values.length) {
|
|
return values[index];
|
|
}
|
|
// not match index, return orignal text
|
|
return orignal;
|
|
});
|
|
}
|
|
|
|
const Object_INDEX_RE = /\{(.+?)\}/g;
|
|
function formatWithObject(text, values) {
|
|
return text.replace(Object_INDEX_RE, function (orignal, matched) {
|
|
const value = values[matched];
|
|
if (value) {
|
|
return value;
|
|
}
|
|
// not match index, return orignal text
|
|
return orignal;
|
|
});
|
|
}
|
|
|
|
function formatLocale(locale) {
|
|
// support zh_CN, en_US => zh-CN, en-US
|
|
return locale.replace('_', '-').toLowerCase();
|
|
}
|
|
|
|
function flattening(data) {
|
|
|
|
const result = {};
|
|
|
|
function deepFlat(data, keys) {
|
|
Object.keys(data).forEach(function (key) {
|
|
const value = data[key];
|
|
const k = keys ? keys + '.' + key : key;
|
|
if (isObject(value)) {
|
|
deepFlat(value, k);
|
|
} else {
|
|
result[k] = String(value);
|
|
}
|
|
});
|
|
}
|
|
|
|
deepFlat(data, '');
|
|
|
|
return result;
|
|
}
|