index.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. 'use strict';
  2. const Debug = require('debug');
  3. const debug = Debug('koa-locales');
  4. const debugSilly = Debug('koa-locales:silly');
  5. const ini = require('ini');
  6. const util = require('util');
  7. const fs = require('fs');
  8. const path = require('path');
  9. const ms = require('humanize-ms');
  10. const assign = require('object-assign');
  11. const DEFAULT_OPTIONS = {
  12. defaultLocale: 'en-US',
  13. queryField: 'locale',
  14. cookieField: 'locale',
  15. localeAlias: {},
  16. writeCookie: true,
  17. cookieMaxAge: '1y',
  18. dir: undefined,
  19. dirs: [ path.join(process.cwd(), 'locales') ],
  20. functionName: '__',
  21. };
  22. module.exports = function (app, options) {
  23. options = assign({}, DEFAULT_OPTIONS, options);
  24. const defaultLocale = formatLocale(options.defaultLocale);
  25. const queryField = options.queryField;
  26. const cookieField = options.cookieField;
  27. const cookieDomain = options.cookieDomain;
  28. const localeAlias = options.localeAlias;
  29. const writeCookie = options.writeCookie;
  30. const cookieMaxAge = ms(options.cookieMaxAge);
  31. const localeDir = options.dir;
  32. const localeDirs = options.dirs;
  33. const functionName = options.functionName;
  34. const resources = {};
  35. /**
  36. * @Deprecated Use options.dirs instead.
  37. */
  38. if (localeDir && localeDirs.indexOf(localeDir) === -1) {
  39. localeDirs.push(localeDir);
  40. }
  41. for (let i = 0; i < localeDirs.length; i++) {
  42. const dir = localeDirs[i];
  43. if (!fs.existsSync(dir)) {
  44. continue;
  45. }
  46. const names = fs.readdirSync(dir);
  47. for (let j = 0; j < names.length; j++) {
  48. const name = names[j];
  49. const filepath = path.join(dir, name);
  50. // support en_US.js => en-US.js
  51. const locale = formatLocale(name.split('.')[0]);
  52. let resource = {};
  53. if (name.endsWith('.js') || name.endsWith('.json')) {
  54. resource = flattening(require(filepath));
  55. } else if (name.endsWith('.properties')) {
  56. resource = ini.parse(fs.readFileSync(filepath, 'utf8'));
  57. }
  58. resources[locale] = resources[locale] || {};
  59. assign(resources[locale], resource);
  60. }
  61. }
  62. debug('Init locales with %j, got %j resources', options, Object.keys(resources));
  63. if (typeof app[functionName] !== 'undefined') {
  64. console.warn('[koa-locales] will override exists "%s" function on app', functionName);
  65. }
  66. function gettext(locale, key, value) {
  67. if (arguments.length === 0 || arguments.length === 1) {
  68. // __()
  69. // --('en')
  70. return '';
  71. }
  72. const resource = resources[locale] || {};
  73. let text = resource[key];
  74. if (text === undefined) {
  75. text = key;
  76. }
  77. debugSilly('%s: %j => %j', locale, key, text);
  78. if (!text) {
  79. return '';
  80. }
  81. if (arguments.length === 2) {
  82. // __(locale, key)
  83. return text;
  84. }
  85. if (arguments.length === 3) {
  86. if (isObject(value)) {
  87. // __(locale, key, object)
  88. // __('zh', '{a} {b} {b} {a}', {a: 'foo', b: 'bar'})
  89. // =>
  90. // foo bar bar foo
  91. return formatWithObject(text, value);
  92. }
  93. if (Array.isArray(value)) {
  94. // __(locale, key, array)
  95. // __('zh', '{0} {1} {1} {0}', ['foo', 'bar'])
  96. // =>
  97. // foo bar bar foo
  98. return formatWithArray(text, value);
  99. }
  100. // __(locale, key, value)
  101. return util.format(text, value);
  102. }
  103. // __(locale, key, value1, ...)
  104. const args = new Array(arguments.length - 1);
  105. args[0] = text;
  106. for(let i = 2; i < arguments.length; i++) {
  107. args[i - 1] = arguments[i];
  108. }
  109. return util.format.apply(util, args);
  110. }
  111. app[functionName] = gettext;
  112. app.context[functionName] = function (key, value) {
  113. if (arguments.length === 0) {
  114. // __()
  115. return '';
  116. }
  117. const locale = this.__getLocale();
  118. if (arguments.length === 1) {
  119. return gettext(locale, key);
  120. }
  121. if (arguments.length === 2) {
  122. return gettext(locale, key, value);
  123. }
  124. const args = new Array(arguments.length + 1);
  125. args[0] = locale;
  126. for(let i = 0; i < arguments.length; i++) {
  127. args[i + 1] = arguments[i];
  128. }
  129. return gettext.apply(this, args);
  130. };
  131. // 1. query: /?locale=en-US
  132. // 2. cookie: locale=zh-TW
  133. // 3. header: Accept-Language: zh-CN,zh;q=0.5
  134. app.context.__getLocale = function () {
  135. if (this.__locale) {
  136. return this.__locale;
  137. }
  138. const cookieLocale = this.cookies.get(cookieField, { signed: false });
  139. // 1. Query
  140. let locale = this.query[queryField];
  141. let localeOrigin = 'query';
  142. // 2. Cookie
  143. if (!locale) {
  144. locale = cookieLocale;
  145. localeOrigin = 'cookie';
  146. }
  147. // 3. Header
  148. if (!locale) {
  149. // Accept-Language: zh-CN,zh;q=0.5
  150. // Accept-Language: zh-CN
  151. let languages = this.acceptsLanguages();
  152. if (languages) {
  153. if (Array.isArray(languages)) {
  154. if (languages[0] === '*') {
  155. languages = languages.slice(1);
  156. }
  157. if (languages.length > 0) {
  158. for (let i = 0; i < languages.length; i++) {
  159. const lang = formatLocale(languages[i]);
  160. if (resources[lang] || localeAlias[lang]) {
  161. locale = lang;
  162. localeOrigin = 'header';
  163. break;
  164. }
  165. }
  166. }
  167. } else {
  168. locale = languages;
  169. localeOrigin = 'header';
  170. }
  171. }
  172. // all missing, set it to defaultLocale
  173. if (!locale) {
  174. locale = defaultLocale;
  175. localeOrigin = 'default';
  176. }
  177. }
  178. // cookie alias
  179. if (locale in localeAlias) {
  180. const originalLocale = locale;
  181. locale = localeAlias[locale];
  182. debugSilly('Used alias, received %s but using %s', originalLocale, locale);
  183. }
  184. locale = formatLocale(locale);
  185. // validate locale
  186. if (!resources[locale]) {
  187. debugSilly('Locale %s is not supported. Using default (%s)', locale, defaultLocale);
  188. locale = defaultLocale;
  189. }
  190. // if header not send, set the locale cookie
  191. if (writeCookie && cookieLocale !== locale && !this.headerSent) {
  192. updateCookie(this, locale);
  193. }
  194. debug('Locale: %s from %s', locale, localeOrigin);
  195. debugSilly('Locale: %s from %s', locale, localeOrigin);
  196. this.__locale = locale;
  197. this.__localeOrigin = localeOrigin;
  198. return locale;
  199. };
  200. app.context.__getLocaleOrigin = function () {
  201. if (this.__localeOrigin) return this.__localeOrigin;
  202. this.__getLocale();
  203. return this.__localeOrigin;
  204. };
  205. app.context.__setLocale = function (locale) {
  206. this.__locale = locale;
  207. this.__localeOrigin = 'set';
  208. updateCookie(this, locale);
  209. };
  210. function updateCookie(ctx, locale) {
  211. const cookieOptions = {
  212. // make sure brower javascript can read the cookie
  213. httpOnly: false,
  214. maxAge: cookieMaxAge,
  215. signed: false,
  216. domain: cookieDomain,
  217. overwrite: true,
  218. };
  219. ctx.cookies.set(cookieField, locale, cookieOptions);
  220. debugSilly('Saved cookie with locale %s', locale);
  221. }
  222. };
  223. function isObject(obj) {
  224. return Object.prototype.toString.call(obj) === '[object Object]';
  225. }
  226. const ARRAY_INDEX_RE = /\{(\d+)\}/g;
  227. function formatWithArray(text, values) {
  228. return text.replace(ARRAY_INDEX_RE, function (orignal, matched) {
  229. const index = parseInt(matched);
  230. if (index < values.length) {
  231. return values[index];
  232. }
  233. // not match index, return orignal text
  234. return orignal;
  235. });
  236. }
  237. const Object_INDEX_RE = /\{(.+?)\}/g;
  238. function formatWithObject(text, values) {
  239. return text.replace(Object_INDEX_RE, function (orignal, matched) {
  240. const value = values[matched];
  241. if (value) {
  242. return value;
  243. }
  244. // not match index, return orignal text
  245. return orignal;
  246. });
  247. }
  248. function formatLocale(locale) {
  249. // support zh_CN, en_US => zh-CN, en-US
  250. return locale.replace('_', '-').toLowerCase();
  251. }
  252. function flattening(data) {
  253. const result = {};
  254. function deepFlat (data, keys) {
  255. Object.keys(data).forEach(function(key) {
  256. const value = data[key];
  257. const k = keys ? keys + '.' + key : key;
  258. if (isObject(value)) {
  259. deepFlat(value, k);
  260. } else {
  261. result[k] = String(value);
  262. }
  263. });
  264. }
  265. deepFlat(data, '');
  266. return result;
  267. }