123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- '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 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'));
- }
- 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;
- }
|