i18n.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404
  1. /**
  2. * @author Created by Marcus Spiegel <spiegel@uscreen.de> on 2011-03-25.
  3. * @link https://github.com/mashpie/i18n-node
  4. * @license http://opensource.org/licenses/MIT
  5. */
  6. 'use strict'
  7. // dependencies
  8. const printf = require('fast-printf').printf
  9. const pkgVersion = require('./package.json').version
  10. const fs = require('fs')
  11. const url = require('url')
  12. const path = require('path')
  13. const debug = require('debug')('i18n:debug')
  14. const warn = require('debug')('i18n:warn')
  15. const error = require('debug')('i18n:error')
  16. const Mustache = require('mustache')
  17. const Messageformat = require('@messageformat/core')
  18. const MakePlural = require('make-plural')
  19. const parseInterval = require('math-interval-parser').default
  20. // utils
  21. const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
  22. /**
  23. * create constructor function
  24. */
  25. const i18n = function I18n(_OPTS = false) {
  26. const MessageformatInstanceForLocale = {}
  27. const PluralsForLocale = {}
  28. let locales = {}
  29. const api = {
  30. __: '__',
  31. __n: '__n',
  32. __l: '__l',
  33. __h: '__h',
  34. __mf: '__mf',
  35. getLocale: 'getLocale',
  36. setLocale: 'setLocale',
  37. getCatalog: 'getCatalog',
  38. getLocales: 'getLocales',
  39. addLocale: 'addLocale',
  40. removeLocale: 'removeLocale'
  41. }
  42. const mustacheConfig = {
  43. tags: ['{{', '}}'],
  44. disable: false
  45. }
  46. let mustacheRegex
  47. const pathsep = path.sep // ---> means win support will be available in node 0.8.x and above
  48. let autoReload
  49. let cookiename
  50. let languageHeaderName
  51. let defaultLocale
  52. let retryInDefaultLocale
  53. let directory
  54. let directoryPermissions
  55. let extension
  56. let fallbacks
  57. let indent
  58. let logDebugFn
  59. let logErrorFn
  60. let logWarnFn
  61. let preserveLegacyCase
  62. let objectNotation
  63. let prefix
  64. let queryParameter
  65. let register
  66. let updateFiles
  67. let syncFiles
  68. let missingKeyFn
  69. let parser
  70. // public exports
  71. const i18n = {}
  72. i18n.version = pkgVersion
  73. i18n.configure = function i18nConfigure(opt) {
  74. // reset locales
  75. locales = {}
  76. // Provide custom API method aliases if desired
  77. // This needs to be processed before the first call to applyAPItoObject()
  78. if (opt.api && typeof opt.api === 'object') {
  79. for (const method in opt.api) {
  80. if (Object.prototype.hasOwnProperty.call(opt.api, method)) {
  81. const alias = opt.api[method]
  82. if (typeof api[method] !== 'undefined') {
  83. api[method] = alias
  84. }
  85. }
  86. }
  87. }
  88. // you may register i18n in global scope, up to you
  89. if (typeof opt.register === 'object') {
  90. register = opt.register
  91. // or give an array objects to register to
  92. if (Array.isArray(opt.register)) {
  93. register = opt.register
  94. register.forEach(applyAPItoObject)
  95. } else {
  96. applyAPItoObject(opt.register)
  97. }
  98. }
  99. // sets a custom cookie name to parse locale settings from
  100. cookiename = typeof opt.cookie === 'string' ? opt.cookie : null
  101. // set the custom header name to extract the language locale
  102. languageHeaderName =
  103. typeof opt.header === 'string' ? opt.header : 'accept-language'
  104. // query-string parameter to be watched - @todo: add test & doc
  105. queryParameter =
  106. typeof opt.queryParameter === 'string' ? opt.queryParameter : null
  107. // where to store json files
  108. directory =
  109. typeof opt.directory === 'string'
  110. ? opt.directory
  111. : path.join(__dirname, 'locales')
  112. // permissions when creating new directories
  113. directoryPermissions =
  114. typeof opt.directoryPermissions === 'string'
  115. ? parseInt(opt.directoryPermissions, 8)
  116. : null
  117. // write new locale information to disk
  118. updateFiles = typeof opt.updateFiles === 'boolean' ? opt.updateFiles : true
  119. // sync locale information accros all files
  120. syncFiles = typeof opt.syncFiles === 'boolean' ? opt.syncFiles : false
  121. // what to use as the indentation unit (ex: "\t", " ")
  122. indent = typeof opt.indent === 'string' ? opt.indent : '\t'
  123. // json files prefix
  124. prefix = typeof opt.prefix === 'string' ? opt.prefix : ''
  125. // where to store json files
  126. extension = typeof opt.extension === 'string' ? opt.extension : '.json'
  127. // setting defaultLocale
  128. defaultLocale =
  129. typeof opt.defaultLocale === 'string' ? opt.defaultLocale : 'en'
  130. // allow to retry in default locale, useful for production
  131. retryInDefaultLocale =
  132. typeof opt.retryInDefaultLocale === 'boolean'
  133. ? opt.retryInDefaultLocale
  134. : false
  135. // auto reload locale files when changed
  136. autoReload = typeof opt.autoReload === 'boolean' ? opt.autoReload : false
  137. // enable object notation?
  138. objectNotation =
  139. typeof opt.objectNotation !== 'undefined' ? opt.objectNotation : false
  140. if (objectNotation === true) objectNotation = '.'
  141. // read language fallback map
  142. fallbacks = typeof opt.fallbacks === 'object' ? opt.fallbacks : {}
  143. // setting custom logger functions
  144. logDebugFn = typeof opt.logDebugFn === 'function' ? opt.logDebugFn : debug
  145. logWarnFn = typeof opt.logWarnFn === 'function' ? opt.logWarnFn : warn
  146. logErrorFn = typeof opt.logErrorFn === 'function' ? opt.logErrorFn : error
  147. preserveLegacyCase =
  148. typeof opt.preserveLegacyCase === 'boolean'
  149. ? opt.preserveLegacyCase
  150. : true
  151. // setting custom missing key function
  152. missingKeyFn =
  153. typeof opt.missingKeyFn === 'function' ? opt.missingKeyFn : missingKey
  154. parser =
  155. typeof opt.parser === 'object' &&
  156. typeof opt.parser.parse === 'function' &&
  157. typeof opt.parser.stringify === 'function'
  158. ? opt.parser
  159. : JSON
  160. // when missing locales we try to guess that from directory
  161. opt.locales = opt.staticCatalog
  162. ? Object.keys(opt.staticCatalog)
  163. : opt.locales || guessLocales(directory)
  164. // some options should be disabled when using staticCatalog
  165. if (opt.staticCatalog) {
  166. updateFiles = false
  167. autoReload = false
  168. syncFiles = false
  169. }
  170. // customize mustache parsing
  171. if (opt.mustacheConfig) {
  172. if (Array.isArray(opt.mustacheConfig.tags)) {
  173. mustacheConfig.tags = opt.mustacheConfig.tags
  174. }
  175. if (opt.mustacheConfig.disable === true) {
  176. mustacheConfig.disable = true
  177. }
  178. }
  179. const [start, end] = mustacheConfig.tags
  180. mustacheRegex = new RegExp(escapeRegExp(start) + '.*' + escapeRegExp(end))
  181. // implicitly read all locales
  182. if (Array.isArray(opt.locales)) {
  183. if (opt.staticCatalog) {
  184. locales = opt.staticCatalog
  185. } else {
  186. opt.locales.forEach(read)
  187. }
  188. // auto reload locale files when changed
  189. if (autoReload) {
  190. // watch changes of locale files (it's called twice because fs.watch is still unstable)
  191. fs.watch(directory, (event, filename) => {
  192. const localeFromFile = guessLocaleFromFile(filename)
  193. if (localeFromFile && opt.locales.indexOf(localeFromFile) > -1) {
  194. logDebug('Auto reloading locale file "' + filename + '".')
  195. read(localeFromFile)
  196. }
  197. })
  198. }
  199. }
  200. }
  201. i18n.init = function i18nInit(request, response, next) {
  202. if (typeof request === 'object') {
  203. // guess requested language/locale
  204. guessLanguage(request)
  205. // bind api to req
  206. applyAPItoObject(request)
  207. // looks double but will ensure schema on api refactor
  208. i18n.setLocale(request, request.locale)
  209. } else {
  210. return logError(
  211. 'i18n.init must be called with one parameter minimum, ie. i18n.init(req)'
  212. )
  213. }
  214. if (typeof response === 'object') {
  215. applyAPItoObject(response)
  216. // and set that locale to response too
  217. i18n.setLocale(response, request.locale)
  218. }
  219. // head over to next callback when bound as middleware
  220. if (typeof next === 'function') {
  221. return next()
  222. }
  223. }
  224. i18n.__ = function i18nTranslate(phrase) {
  225. let msg
  226. const argv = parseArgv(arguments)
  227. const namedValues = argv[0]
  228. const args = argv[1]
  229. // called like __({phrase: "Hello", locale: "en"})
  230. if (typeof phrase === 'object') {
  231. if (
  232. typeof phrase.locale === 'string' &&
  233. typeof phrase.phrase === 'string'
  234. ) {
  235. msg = translate(phrase.locale, phrase.phrase)
  236. }
  237. }
  238. // called like __("Hello")
  239. else {
  240. // get translated message with locale from scope (deprecated) or object
  241. msg = translate(getLocaleFromObject(this), phrase)
  242. }
  243. // postprocess to get compatible to plurals
  244. if (typeof msg === 'object' && msg.one) {
  245. msg = msg.one
  246. }
  247. // in case there is no 'one' but an 'other' rule
  248. if (typeof msg === 'object' && msg.other) {
  249. msg = msg.other
  250. }
  251. // head over to postProcessing
  252. return postProcess(msg, namedValues, args)
  253. }
  254. i18n.__mf = function i18nMessageformat(phrase) {
  255. let msg, mf, f
  256. let targetLocale = defaultLocale
  257. const argv = parseArgv(arguments)
  258. const namedValues = argv[0]
  259. const args = argv[1]
  260. // called like __({phrase: "Hello", locale: "en"})
  261. if (typeof phrase === 'object') {
  262. if (
  263. typeof phrase.locale === 'string' &&
  264. typeof phrase.phrase === 'string'
  265. ) {
  266. msg = phrase.phrase
  267. targetLocale = phrase.locale
  268. }
  269. }
  270. // called like __("Hello")
  271. else {
  272. // get translated message with locale from scope (deprecated) or object
  273. msg = phrase
  274. targetLocale = getLocaleFromObject(this)
  275. }
  276. msg = translate(targetLocale, msg)
  277. // --- end get msg
  278. // now head over to Messageformat
  279. // and try to cache instance
  280. if (MessageformatInstanceForLocale[targetLocale]) {
  281. mf = MessageformatInstanceForLocale[targetLocale]
  282. } else {
  283. mf = new Messageformat(targetLocale)
  284. mf.compiledFunctions = {}
  285. MessageformatInstanceForLocale[targetLocale] = mf
  286. }
  287. // let's try to cache that function
  288. if (mf.compiledFunctions[msg]) {
  289. f = mf.compiledFunctions[msg]
  290. } else {
  291. f = mf.compile(msg)
  292. mf.compiledFunctions[msg] = f
  293. }
  294. return postProcess(f(namedValues), namedValues, args)
  295. }
  296. i18n.__l = function i18nTranslationList(phrase) {
  297. const translations = []
  298. Object.keys(locales)
  299. .sort()
  300. .forEach((l) => {
  301. translations.push(i18n.__({ phrase: phrase, locale: l }))
  302. })
  303. return translations
  304. }
  305. i18n.__h = function i18nTranslationHash(phrase) {
  306. const translations = []
  307. Object.keys(locales)
  308. .sort()
  309. .forEach((l) => {
  310. const hash = {}
  311. hash[l] = i18n.__({ phrase: phrase, locale: l })
  312. translations.push(hash)
  313. })
  314. return translations
  315. }
  316. i18n.__n = function i18nTranslatePlural(singular, plural, count) {
  317. let msg
  318. let namedValues
  319. let targetLocale
  320. let args = []
  321. // Accept an object with named values as the last parameter
  322. if (argsEndWithNamedObject(arguments)) {
  323. namedValues = arguments[arguments.length - 1]
  324. args =
  325. arguments.length >= 5
  326. ? Array.prototype.slice.call(arguments, 3, -1)
  327. : []
  328. } else {
  329. namedValues = {}
  330. args =
  331. arguments.length >= 4 ? Array.prototype.slice.call(arguments, 3) : []
  332. }
  333. // called like __n({singular: "%s cat", plural: "%s cats", locale: "en"}, 3)
  334. if (typeof singular === 'object') {
  335. if (
  336. typeof singular.locale === 'string' &&
  337. typeof singular.singular === 'string' &&
  338. typeof singular.plural === 'string'
  339. ) {
  340. targetLocale = singular.locale
  341. msg = translate(singular.locale, singular.singular, singular.plural)
  342. }
  343. args.unshift(count)
  344. // some template engines pass all values as strings -> so we try to convert them to numbers
  345. if (typeof plural === 'number' || Number(plural) + '' === plural) {
  346. count = plural
  347. }
  348. // called like __n({singular: "%s cat", plural: "%s cats", locale: "en", count: 3})
  349. if (
  350. typeof singular.count === 'number' ||
  351. typeof singular.count === 'string'
  352. ) {
  353. count = singular.count
  354. args.unshift(plural)
  355. }
  356. } else {
  357. // called like __n('cat', 3)
  358. if (typeof plural === 'number' || Number(plural) + '' === plural) {
  359. count = plural
  360. // we add same string as default
  361. // which efectivly copies the key to the plural.value
  362. // this is for initialization of new empty translations
  363. plural = singular
  364. args.unshift(count)
  365. args.unshift(plural)
  366. }
  367. // called like __n('%s cat', '%s cats', 3)
  368. // get translated message with locale from scope (deprecated) or object
  369. msg = translate(getLocaleFromObject(this), singular, plural)
  370. targetLocale = getLocaleFromObject(this)
  371. }
  372. if (count === null) count = namedValues.count
  373. // enforce number
  374. count = Number(count)
  375. // find the correct plural rule for given locale
  376. if (typeof msg === 'object') {
  377. let p
  378. // create a new Plural for locale
  379. // and try to cache instance
  380. if (PluralsForLocale[targetLocale]) {
  381. p = PluralsForLocale[targetLocale]
  382. } else {
  383. // split locales with a region code
  384. const lc = targetLocale
  385. .toLowerCase()
  386. .split(/[_-\s]+/)
  387. .filter((el) => true && el)
  388. // take the first part of locale, fallback to full locale
  389. p = MakePlural[lc[0] || targetLocale]
  390. PluralsForLocale[targetLocale] = p
  391. }
  392. // fallback to 'other' on case of missing translations
  393. msg = msg[p(count)] || msg.other
  394. }
  395. // head over to postProcessing
  396. return postProcess(msg, namedValues, args, count)
  397. }
  398. i18n.setLocale = function i18nSetLocale(object, locale, skipImplicitObjects) {
  399. // when given an array of objects => setLocale on each
  400. if (Array.isArray(object) && typeof locale === 'string') {
  401. for (let i = object.length - 1; i >= 0; i--) {
  402. i18n.setLocale(object[i], locale, true)
  403. }
  404. return i18n.getLocale(object[0])
  405. }
  406. // defaults to called like i18n.setLocale(req, 'en')
  407. let targetObject = object
  408. let targetLocale = locale
  409. // called like req.setLocale('en') or i18n.setLocale('en')
  410. if (locale === undefined && typeof object === 'string') {
  411. targetObject = this
  412. targetLocale = object
  413. }
  414. // consider a fallback
  415. if (!locales[targetLocale]) {
  416. targetLocale = getFallback(targetLocale, fallbacks) || targetLocale
  417. }
  418. // now set locale on object
  419. targetObject.locale = locales[targetLocale] ? targetLocale : defaultLocale
  420. // consider any extra registered objects
  421. if (typeof register === 'object') {
  422. if (Array.isArray(register) && !skipImplicitObjects) {
  423. register.forEach((r) => {
  424. r.locale = targetObject.locale
  425. })
  426. } else {
  427. register.locale = targetObject.locale
  428. }
  429. }
  430. // consider res
  431. if (targetObject.res && !skipImplicitObjects) {
  432. // escape recursion
  433. // @see - https://github.com/balderdashy/sails/pull/3631
  434. // - https://github.com/mashpie/i18n-node/pull/218
  435. if (targetObject.res.locals) {
  436. i18n.setLocale(targetObject.res, targetObject.locale, true)
  437. i18n.setLocale(targetObject.res.locals, targetObject.locale, true)
  438. } else {
  439. i18n.setLocale(targetObject.res, targetObject.locale)
  440. }
  441. }
  442. // consider locals
  443. if (targetObject.locals && !skipImplicitObjects) {
  444. // escape recursion
  445. // @see - https://github.com/balderdashy/sails/pull/3631
  446. // - https://github.com/mashpie/i18n-node/pull/218
  447. if (targetObject.locals.res) {
  448. i18n.setLocale(targetObject.locals, targetObject.locale, true)
  449. i18n.setLocale(targetObject.locals.res, targetObject.locale, true)
  450. } else {
  451. i18n.setLocale(targetObject.locals, targetObject.locale)
  452. }
  453. }
  454. return i18n.getLocale(targetObject)
  455. }
  456. i18n.getLocale = function i18nGetLocale(request) {
  457. // called like i18n.getLocale(req)
  458. if (request && request.locale) {
  459. return request.locale
  460. }
  461. // called like req.getLocale()
  462. return this.locale || defaultLocale
  463. }
  464. i18n.getCatalog = function i18nGetCatalog(object, locale) {
  465. let targetLocale
  466. // called like i18n.getCatalog(req)
  467. if (
  468. typeof object === 'object' &&
  469. typeof object.locale === 'string' &&
  470. locale === undefined
  471. ) {
  472. targetLocale = object.locale
  473. }
  474. // called like i18n.getCatalog(req, 'en')
  475. if (
  476. !targetLocale &&
  477. typeof object === 'object' &&
  478. typeof locale === 'string'
  479. ) {
  480. targetLocale = locale
  481. }
  482. // called like req.getCatalog('en')
  483. if (!targetLocale && locale === undefined && typeof object === 'string') {
  484. targetLocale = object
  485. }
  486. // called like req.getCatalog()
  487. if (
  488. !targetLocale &&
  489. object === undefined &&
  490. locale === undefined &&
  491. typeof this.locale === 'string'
  492. ) {
  493. if (register && register.global) {
  494. targetLocale = ''
  495. } else {
  496. targetLocale = this.locale
  497. }
  498. }
  499. // called like i18n.getCatalog()
  500. if (targetLocale === undefined || targetLocale === '') {
  501. return locales
  502. }
  503. if (!locales[targetLocale]) {
  504. targetLocale = getFallback(targetLocale, fallbacks) || targetLocale
  505. }
  506. if (locales[targetLocale]) {
  507. return locales[targetLocale]
  508. } else {
  509. logWarn('No catalog found for "' + targetLocale + '"')
  510. return false
  511. }
  512. }
  513. i18n.getLocales = function i18nGetLocales() {
  514. return Object.keys(locales)
  515. }
  516. i18n.addLocale = function i18nAddLocale(locale) {
  517. read(locale)
  518. }
  519. i18n.removeLocale = function i18nRemoveLocale(locale) {
  520. delete locales[locale]
  521. }
  522. // ===================
  523. // = private methods =
  524. // ===================
  525. const postProcess = (msg, namedValues, args, count) => {
  526. // test for parsable interval string
  527. if (/\|/.test(msg)) {
  528. msg = parsePluralInterval(msg, count)
  529. }
  530. // replace the counter
  531. if (typeof count === 'number') {
  532. msg = printf(msg, Number(count))
  533. }
  534. // if the msg string contains {{Mustache}} patterns we render it as a mini template
  535. if (!mustacheConfig.disable && mustacheRegex.test(msg)) {
  536. msg = Mustache.render(msg, namedValues, {}, mustacheConfig.tags)
  537. }
  538. // if we have extra arguments with values to get replaced,
  539. // an additional substition injects those strings afterwards
  540. if (/%/.test(msg) && args && args.length > 0) {
  541. msg = printf(msg, ...args)
  542. }
  543. return msg
  544. }
  545. const argsEndWithNamedObject = (args) =>
  546. args.length > 1 &&
  547. args[args.length - 1] !== null &&
  548. typeof args[args.length - 1] === 'object'
  549. const parseArgv = (args) => {
  550. let namedValues, returnArgs
  551. if (argsEndWithNamedObject(args)) {
  552. namedValues = args[args.length - 1]
  553. returnArgs = Array.prototype.slice.call(args, 1, -1)
  554. } else {
  555. namedValues = {}
  556. returnArgs = args.length >= 2 ? Array.prototype.slice.call(args, 1) : []
  557. }
  558. return [namedValues, returnArgs]
  559. }
  560. /**
  561. * registers all public API methods to a given response object when not already declared
  562. */
  563. const applyAPItoObject = (object) => {
  564. let alreadySetted = true
  565. // attach to itself if not provided
  566. for (const method in api) {
  567. if (Object.prototype.hasOwnProperty.call(api, method)) {
  568. const alias = api[method]
  569. // be kind rewind, or better not touch anything already existing
  570. if (!object[alias]) {
  571. alreadySetted = false
  572. object[alias] = i18n[method].bind(object)
  573. }
  574. }
  575. }
  576. // set initial locale if not set
  577. if (!object.locale) {
  578. object.locale = defaultLocale
  579. }
  580. // escape recursion
  581. if (alreadySetted) {
  582. return
  583. }
  584. // attach to response if present (ie. in express)
  585. if (object.res) {
  586. applyAPItoObject(object.res)
  587. }
  588. // attach to locals if present (ie. in express)
  589. if (object.locals) {
  590. applyAPItoObject(object.locals)
  591. }
  592. }
  593. /**
  594. * tries to guess locales by scanning the given directory
  595. */
  596. const guessLocales = (directory) => {
  597. const entries = fs.readdirSync(directory)
  598. const localesFound = []
  599. for (let i = entries.length - 1; i >= 0; i--) {
  600. if (entries[i].match(/^\./)) continue
  601. const localeFromFile = guessLocaleFromFile(entries[i])
  602. if (localeFromFile) localesFound.push(localeFromFile)
  603. }
  604. return localesFound.sort()
  605. }
  606. /**
  607. * tries to guess locales from a given filename
  608. */
  609. const guessLocaleFromFile = (filename) => {
  610. const extensionRegex = new RegExp(extension + '$', 'g')
  611. const prefixRegex = new RegExp('^' + prefix, 'g')
  612. if (!filename) return false
  613. if (prefix && !filename.match(prefixRegex)) return false
  614. if (extension && !filename.match(extensionRegex)) return false
  615. return filename.replace(prefix, '').replace(extensionRegex, '')
  616. }
  617. /**
  618. * @param queryLanguage - language query parameter, either an array or a string.
  619. * @return the first non-empty language query parameter found, null otherwise.
  620. */
  621. const extractQueryLanguage = (queryLanguage) => {
  622. if (Array.isArray(queryLanguage)) {
  623. return queryLanguage.find((lang) => lang !== '' && lang)
  624. }
  625. return typeof queryLanguage === 'string' && queryLanguage
  626. }
  627. /**
  628. * guess language setting based on http headers
  629. */
  630. const guessLanguage = (request) => {
  631. if (typeof request === 'object') {
  632. const languageHeader = request.headers
  633. ? request.headers[languageHeaderName]
  634. : undefined
  635. const languages = []
  636. const regions = []
  637. request.languages = [defaultLocale]
  638. request.regions = [defaultLocale]
  639. request.language = defaultLocale
  640. request.region = defaultLocale
  641. // a query parameter overwrites all
  642. if (queryParameter && request.url) {
  643. const urlAsString =
  644. typeof request.url === 'string' ? request.url : request.url.toString()
  645. /**
  646. * @todo WHATWG new URL() requires full URL including hostname - that might change
  647. * @see https://github.com/nodejs/node/issues/12682
  648. */
  649. // eslint-disable-next-line node/no-deprecated-api
  650. const urlObj = url.parse(urlAsString, true)
  651. const languageQueryParameter = urlObj.query[queryParameter]
  652. if (languageQueryParameter) {
  653. let queryLanguage = extractQueryLanguage(languageQueryParameter)
  654. if (queryLanguage) {
  655. logDebug('Overriding locale from query: ' + queryLanguage)
  656. if (preserveLegacyCase) {
  657. queryLanguage = queryLanguage.toLowerCase()
  658. }
  659. return i18n.setLocale(request, queryLanguage)
  660. }
  661. }
  662. }
  663. // a cookie overwrites headers
  664. if (cookiename && request.cookies && request.cookies[cookiename]) {
  665. request.language = request.cookies[cookiename]
  666. return i18n.setLocale(request, request.language)
  667. }
  668. // 'accept-language' is the most common source
  669. if (languageHeader) {
  670. const acceptedLanguages = getAcceptedLanguagesFromHeader(languageHeader)
  671. let match
  672. let fallbackMatch
  673. let fallback
  674. for (let i = 0; i < acceptedLanguages.length; i++) {
  675. const lang = acceptedLanguages[i]
  676. const lr = lang.split('-', 2)
  677. const parentLang = lr[0]
  678. const region = lr[1]
  679. // Check if we have a configured fallback set for this language.
  680. const fallbackLang = getFallback(lang, fallbacks)
  681. if (fallbackLang) {
  682. fallback = fallbackLang
  683. // Fallbacks for languages should be inserted
  684. // where the original, unsupported language existed.
  685. const acceptedLanguageIndex = acceptedLanguages.indexOf(lang)
  686. const fallbackIndex = acceptedLanguages.indexOf(fallback)
  687. if (fallbackIndex > -1) {
  688. acceptedLanguages.splice(fallbackIndex, 1)
  689. }
  690. acceptedLanguages.splice(acceptedLanguageIndex + 1, 0, fallback)
  691. }
  692. // Check if we have a configured fallback set for the parent language of the locale.
  693. const fallbackParentLang = getFallback(parentLang, fallbacks)
  694. if (fallbackParentLang) {
  695. fallback = fallbackParentLang
  696. // Fallbacks for a parent language should be inserted
  697. // to the end of the list, so they're only picked
  698. // if there is no better match.
  699. if (acceptedLanguages.indexOf(fallback) < 0) {
  700. acceptedLanguages.push(fallback)
  701. }
  702. }
  703. if (languages.indexOf(parentLang) < 0) {
  704. languages.push(parentLang.toLowerCase())
  705. }
  706. if (region) {
  707. regions.push(region.toLowerCase())
  708. }
  709. if (!match && locales[lang]) {
  710. match = lang
  711. break
  712. }
  713. if (!fallbackMatch && locales[parentLang]) {
  714. fallbackMatch = parentLang
  715. }
  716. }
  717. request.language = match || fallbackMatch || request.language
  718. request.region = regions[0] || request.region
  719. return i18n.setLocale(request, request.language)
  720. }
  721. }
  722. // last resort: defaultLocale
  723. return i18n.setLocale(request, defaultLocale)
  724. }
  725. /**
  726. * Get a sorted list of accepted languages from the HTTP Accept-Language header
  727. */
  728. const getAcceptedLanguagesFromHeader = (header) => {
  729. const languages = header.split(',')
  730. const preferences = {}
  731. return languages
  732. .map((item) => {
  733. const preferenceParts = item.trim().split(';q=')
  734. if (preferenceParts.length < 2) {
  735. preferenceParts[1] = 1.0
  736. } else {
  737. const quality = parseFloat(preferenceParts[1])
  738. preferenceParts[1] = quality || 0.0
  739. }
  740. preferences[preferenceParts[0]] = preferenceParts[1]
  741. return preferenceParts[0]
  742. })
  743. .filter((lang) => preferences[lang] > 0)
  744. .sort((a, b) => preferences[b] - preferences[a])
  745. }
  746. /**
  747. * searches for locale in given object
  748. */
  749. const getLocaleFromObject = (obj) => {
  750. let locale
  751. if (obj && obj.scope) {
  752. locale = obj.scope.locale
  753. }
  754. if (obj && obj.locale) {
  755. locale = obj.locale
  756. }
  757. return locale
  758. }
  759. /**
  760. * splits and parses a phrase for mathematical interval expressions
  761. */
  762. const parsePluralInterval = (phrase, count) => {
  763. let returnPhrase = phrase
  764. const phrases = phrase.split(/\|/)
  765. let intervalRuleExists = false
  766. // some() breaks on 1st true
  767. phrases.some((p) => {
  768. const matches = p.match(/^\s*([()[\]]+[\d,]+[()[\]]+)?\s*(.*)$/)
  769. // not the same as in combined condition
  770. if (matches != null && matches[1]) {
  771. intervalRuleExists = true
  772. if (matchInterval(count, matches[1]) === true) {
  773. returnPhrase = matches[2]
  774. return true
  775. }
  776. } else {
  777. // this is a other or catch all case, this only is taken into account if there is actually another rule
  778. if (intervalRuleExists) {
  779. returnPhrase = p
  780. }
  781. }
  782. return false
  783. })
  784. return returnPhrase
  785. }
  786. /**
  787. * test a number to match mathematical interval expressions
  788. * [0,2] - 0 to 2 (including, matches: 0, 1, 2)
  789. * ]0,3[ - 0 to 3 (excluding, matches: 1, 2)
  790. * [1] - 1 (matches: 1)
  791. * [20,] - all numbers ≥20 (matches: 20, 21, 22, ...)
  792. * [,20] - all numbers ≤20 (matches: 20, 21, 22, ...)
  793. */
  794. const matchInterval = (number, interval) => {
  795. interval = parseInterval(interval)
  796. if (interval && typeof number === 'number') {
  797. if (interval.from.value === number) {
  798. return interval.from.included
  799. }
  800. if (interval.to.value === number) {
  801. return interval.to.included
  802. }
  803. return (
  804. Math.min(interval.from.value, number) === interval.from.value &&
  805. Math.max(interval.to.value, number) === interval.to.value
  806. )
  807. }
  808. return false
  809. }
  810. /**
  811. * read locale file, translate a msg and write to fs if new
  812. */
  813. const translate = (locale, singular, plural, skipSyncToAllFiles) => {
  814. // add same key to all translations
  815. if (!skipSyncToAllFiles && syncFiles) {
  816. syncToAllFiles(singular, plural)
  817. }
  818. if (locale === undefined) {
  819. logWarn(
  820. 'WARN: No locale found - check the context of the call to __(). Using ' +
  821. defaultLocale +
  822. ' as current locale'
  823. )
  824. locale = defaultLocale
  825. }
  826. // try to get a fallback
  827. if (!locales[locale]) {
  828. locale = getFallback(locale, fallbacks) || locale
  829. }
  830. // attempt to read when defined as valid locale
  831. if (!locales[locale]) {
  832. read(locale)
  833. }
  834. // fallback to default when missed
  835. if (!locales[locale]) {
  836. logWarn(
  837. 'WARN: Locale ' +
  838. locale +
  839. " couldn't be read - check the context of the call to $__. Using " +
  840. defaultLocale +
  841. ' (default) as current locale'
  842. )
  843. locale = defaultLocale
  844. read(locale)
  845. }
  846. // dotnotaction add on, @todo: factor out
  847. let defaultSingular = singular
  848. let defaultPlural = plural
  849. if (objectNotation) {
  850. let indexOfColon = singular.indexOf(':')
  851. // We compare against 0 instead of -1 because
  852. // we don't really expect the string to start with ':'.
  853. if (indexOfColon > 0) {
  854. defaultSingular = singular.substring(indexOfColon + 1)
  855. singular = singular.substring(0, indexOfColon)
  856. }
  857. if (plural && typeof plural !== 'number') {
  858. indexOfColon = plural.indexOf(':')
  859. if (indexOfColon > 0) {
  860. defaultPlural = plural.substring(indexOfColon + 1)
  861. plural = plural.substring(0, indexOfColon)
  862. }
  863. }
  864. }
  865. const accessor = localeAccessor(locale, singular)
  866. const mutator = localeMutator(locale, singular)
  867. // if (plural) {
  868. // if (accessor() == null) {
  869. // mutator({
  870. // 'one': defaultSingular || singular,
  871. // 'other': defaultPlural || plural
  872. // });
  873. // write(locale);
  874. // }
  875. // }
  876. // if (accessor() == null) {
  877. // mutator(defaultSingular || singular);
  878. // write(locale);
  879. // }
  880. if (plural) {
  881. if (accessor() == null) {
  882. // when retryInDefaultLocale is true - try to set default value from defaultLocale
  883. if (retryInDefaultLocale && locale !== defaultLocale) {
  884. logDebug(
  885. 'Missing ' +
  886. singular +
  887. ' in ' +
  888. locale +
  889. ' retrying in ' +
  890. defaultLocale
  891. )
  892. mutator(translate(defaultLocale, singular, plural, true))
  893. } else {
  894. mutator({
  895. one: defaultSingular || singular,
  896. other: defaultPlural || plural
  897. })
  898. }
  899. write(locale)
  900. }
  901. }
  902. if (accessor() == null) {
  903. // when retryInDefaultLocale is true - try to set default value from defaultLocale
  904. if (retryInDefaultLocale && locale !== defaultLocale) {
  905. logDebug(
  906. 'Missing ' +
  907. singular +
  908. ' in ' +
  909. locale +
  910. ' retrying in ' +
  911. defaultLocale
  912. )
  913. mutator(translate(defaultLocale, singular, plural, true))
  914. } else {
  915. mutator(defaultSingular || singular)
  916. }
  917. write(locale)
  918. }
  919. return accessor()
  920. }
  921. /**
  922. * initialize the same key in all locales
  923. * when not already existing, checked via translate
  924. */
  925. const syncToAllFiles = (singular, plural) => {
  926. // iterate over locales and translate again
  927. // this will implicitly write/sync missing keys
  928. // to the rest of locales
  929. for (const l in locales) {
  930. translate(l, singular, plural, true)
  931. }
  932. }
  933. /**
  934. * Allows delayed access to translations nested inside objects.
  935. * @param {String} locale The locale to use.
  936. * @param {String} singular The singular term to look up.
  937. * @param {Boolean} [allowDelayedTraversal=true] Is delayed traversal of the tree allowed?
  938. * This parameter is used internally. It allows to signal the accessor that
  939. * a translation was not found in the initial lookup and that an invocation
  940. * of the accessor may trigger another traversal of the tree.
  941. * @returns {Function} A function that, when invoked, returns the current value stored
  942. * in the object at the requested location.
  943. */
  944. const localeAccessor = (locale, singular, allowDelayedTraversal) => {
  945. // Bail out on non-existent locales to defend against internal errors.
  946. if (!locales[locale]) return Function.prototype
  947. // Handle object lookup notation
  948. const indexOfDot = objectNotation && singular.lastIndexOf(objectNotation)
  949. if (objectNotation && indexOfDot > 0 && indexOfDot < singular.length - 1) {
  950. // If delayed traversal wasn't specifically forbidden, it is allowed.
  951. if (typeof allowDelayedTraversal === 'undefined')
  952. allowDelayedTraversal = true
  953. // The accessor we're trying to find and which we want to return.
  954. let accessor = null
  955. // An accessor that returns null.
  956. const nullAccessor = () => null
  957. // Do we need to re-traverse the tree upon invocation of the accessor?
  958. let reTraverse = false
  959. // Split the provided term and run the callback for each subterm.
  960. singular.split(objectNotation).reduce((object, index) => {
  961. // Make the accessor return null.
  962. accessor = nullAccessor
  963. // If our current target object (in the locale tree) doesn't exist or
  964. // it doesn't have the next subterm as a member...
  965. if (
  966. object === null ||
  967. !Object.prototype.hasOwnProperty.call(object, index)
  968. ) {
  969. // ...remember that we need retraversal (because we didn't find our target).
  970. reTraverse = allowDelayedTraversal
  971. // Return null to avoid deeper iterations.
  972. return null
  973. }
  974. // We can traverse deeper, so we generate an accessor for this current level.
  975. accessor = () => object[index]
  976. // Return a reference to the next deeper level in the locale tree.
  977. return object[index]
  978. }, locales[locale])
  979. // Return the requested accessor.
  980. return () =>
  981. // If we need to re-traverse (because we didn't find our target term)
  982. // traverse again and return the new result (but don't allow further iterations)
  983. // or return the previously found accessor if it was already valid.
  984. reTraverse ? localeAccessor(locale, singular, false)() : accessor()
  985. } else {
  986. // No object notation, just return an accessor that performs array lookup.
  987. return () => locales[locale][singular]
  988. }
  989. }
  990. /**
  991. * Allows delayed mutation of a translation nested inside objects.
  992. * @description Construction of the mutator will attempt to locate the requested term
  993. * inside the object, but if part of the branch does not exist yet, it will not be
  994. * created until the mutator is actually invoked. At that point, re-traversal of the
  995. * tree is performed and missing parts along the branch will be created.
  996. * @param {String} locale The locale to use.
  997. * @param {String} singular The singular term to look up.
  998. * @param [Boolean} [allowBranching=false] Is the mutator allowed to create previously
  999. * non-existent branches along the requested locale path?
  1000. * @returns {Function} A function that takes one argument. When the function is
  1001. * invoked, the targeted translation term will be set to the given value inside the locale table.
  1002. */
  1003. const localeMutator = function (locale, singular, allowBranching) {
  1004. // Bail out on non-existent locales to defend against internal errors.
  1005. if (!locales[locale]) return Function.prototype
  1006. // Handle object lookup notation
  1007. const indexOfDot = objectNotation && singular.lastIndexOf(objectNotation)
  1008. if (objectNotation && indexOfDot > 0 && indexOfDot < singular.length - 1) {
  1009. // If branching wasn't specifically allowed, disable it.
  1010. if (typeof allowBranching === 'undefined') allowBranching = false
  1011. // This will become the function we want to return.
  1012. let accessor = null
  1013. // An accessor that takes one argument and returns null.
  1014. const nullAccessor = () => null
  1015. // Fix object path.
  1016. let fixObject = () => ({})
  1017. // Are we going to need to re-traverse the tree when the mutator is invoked?
  1018. let reTraverse = false
  1019. // Split the provided term and run the callback for each subterm.
  1020. singular.split(objectNotation).reduce((object, index) => {
  1021. // Make the mutator do nothing.
  1022. accessor = nullAccessor
  1023. // If our current target object (in the locale tree) doesn't exist or
  1024. // it doesn't have the next subterm as a member...
  1025. if (
  1026. object === null ||
  1027. !Object.prototype.hasOwnProperty.call(object, index)
  1028. ) {
  1029. // ...check if we're allowed to create new branches.
  1030. if (allowBranching) {
  1031. // Fix `object` if `object` is not Object.
  1032. if (object === null || typeof object !== 'object') {
  1033. object = fixObject()
  1034. }
  1035. // If we are allowed to, create a new object along the path.
  1036. object[index] = {}
  1037. } else {
  1038. // If we aren't allowed, remember that we need to re-traverse later on and...
  1039. reTraverse = true
  1040. // ...return null to make the next iteration bail our early on.
  1041. return null
  1042. }
  1043. }
  1044. // Generate a mutator for the current level.
  1045. accessor = (value) => {
  1046. object[index] = value
  1047. return value
  1048. }
  1049. // Generate a fixer for the current level.
  1050. fixObject = () => {
  1051. object[index] = {}
  1052. return object[index]
  1053. }
  1054. // Return a reference to the next deeper level in the locale tree.
  1055. return object[index]
  1056. }, locales[locale])
  1057. // Return the final mutator.
  1058. return (value) => {
  1059. // If we need to re-traverse the tree
  1060. // invoke the search again, but allow branching
  1061. // this time (because here the mutator is being invoked)
  1062. // otherwise, just change the value directly.
  1063. value = missingKeyFn(locale, value)
  1064. return reTraverse
  1065. ? localeMutator(locale, singular, true)(value)
  1066. : accessor(value)
  1067. }
  1068. } else {
  1069. // No object notation, just return a mutator that performs array lookup and changes the value.
  1070. return (value) => {
  1071. value = missingKeyFn(locale, value)
  1072. locales[locale][singular] = value
  1073. return value
  1074. }
  1075. }
  1076. }
  1077. /**
  1078. * try reading a file
  1079. */
  1080. const read = (locale) => {
  1081. let localeFile = {}
  1082. const file = getStorageFilePath(locale)
  1083. try {
  1084. logDebug('read ' + file + ' for locale: ' + locale)
  1085. localeFile = fs.readFileSync(file, 'utf-8')
  1086. try {
  1087. // parsing filecontents to locales[locale]
  1088. locales[locale] = parser.parse(localeFile)
  1089. } catch (parseError) {
  1090. logError(
  1091. 'unable to parse locales from file (maybe ' +
  1092. file +
  1093. ' is empty or invalid json?): ',
  1094. parseError
  1095. )
  1096. }
  1097. } catch (readError) {
  1098. // unable to read, so intialize that file
  1099. // locales[locale] are already set in memory, so no extra read required
  1100. // or locales[locale] are empty, which initializes an empty locale.json file
  1101. // since the current invalid locale could exist, we should back it up
  1102. if (fs.existsSync(file)) {
  1103. logDebug(
  1104. 'backing up invalid locale ' + locale + ' to ' + file + '.invalid'
  1105. )
  1106. fs.renameSync(file, file + '.invalid')
  1107. }
  1108. logDebug('initializing ' + file)
  1109. write(locale)
  1110. }
  1111. }
  1112. /**
  1113. * try writing a file in a created directory
  1114. */
  1115. const write = (locale) => {
  1116. let stats, target, tmp
  1117. // don't write new locale information to disk if updateFiles isn't true
  1118. if (!updateFiles) {
  1119. return
  1120. }
  1121. // creating directory if necessary
  1122. try {
  1123. stats = fs.lstatSync(directory)
  1124. } catch (e) {
  1125. logDebug('creating locales dir in: ' + directory)
  1126. try {
  1127. fs.mkdirSync(directory, directoryPermissions)
  1128. } catch (e) {
  1129. // in case of parallel tasks utilizing in same dir
  1130. if (e.code !== 'EEXIST') throw e
  1131. }
  1132. }
  1133. // first time init has an empty file
  1134. if (!locales[locale]) {
  1135. locales[locale] = {}
  1136. }
  1137. // writing to tmp and rename on success
  1138. try {
  1139. target = getStorageFilePath(locale)
  1140. tmp = target + '.tmp'
  1141. fs.writeFileSync(
  1142. tmp,
  1143. parser.stringify(locales[locale], null, indent),
  1144. 'utf8'
  1145. )
  1146. stats = fs.statSync(tmp)
  1147. if (stats.isFile()) {
  1148. fs.renameSync(tmp, target)
  1149. } else {
  1150. logError(
  1151. 'unable to write locales to file (either ' +
  1152. tmp +
  1153. ' or ' +
  1154. target +
  1155. ' are not writeable?): '
  1156. )
  1157. }
  1158. } catch (e) {
  1159. logError(
  1160. 'unexpected error writing files (either ' +
  1161. tmp +
  1162. ' or ' +
  1163. target +
  1164. ' are not writeable?): ',
  1165. e
  1166. )
  1167. }
  1168. }
  1169. /**
  1170. * basic normalization of filepath
  1171. */
  1172. const getStorageFilePath = (locale) => {
  1173. // changed API to use .json as default, #16
  1174. const ext = extension || '.json'
  1175. const filepath = path.normalize(directory + pathsep + prefix + locale + ext)
  1176. const filepathJS = path.normalize(
  1177. directory + pathsep + prefix + locale + '.js'
  1178. )
  1179. // use .js as fallback if already existing
  1180. try {
  1181. if (fs.statSync(filepathJS)) {
  1182. logDebug('using existing file ' + filepathJS)
  1183. extension = '.js'
  1184. return filepathJS
  1185. }
  1186. } catch (e) {
  1187. logDebug('will use ' + filepath)
  1188. }
  1189. return filepath
  1190. }
  1191. /**
  1192. * Get locales with wildcard support
  1193. */
  1194. const getFallback = (targetLocale, fallbacks) => {
  1195. fallbacks = fallbacks || {}
  1196. if (fallbacks[targetLocale]) return fallbacks[targetLocale]
  1197. let fallBackLocale = null
  1198. for (const key in fallbacks) {
  1199. if (targetLocale.match(new RegExp('^' + key.replace('*', '.*') + '$'))) {
  1200. fallBackLocale = fallbacks[key]
  1201. break
  1202. }
  1203. }
  1204. return fallBackLocale
  1205. }
  1206. /**
  1207. * Logging proxies
  1208. */
  1209. const logDebug = (msg) => {
  1210. logDebugFn(msg)
  1211. }
  1212. const logWarn = (msg) => {
  1213. logWarnFn(msg)
  1214. }
  1215. const logError = (msg) => {
  1216. logErrorFn(msg)
  1217. }
  1218. /**
  1219. * Missing key function
  1220. */
  1221. const missingKey = (locale, value) => {
  1222. return value
  1223. }
  1224. /**
  1225. * implicitly configure when created with given options
  1226. * @example
  1227. * const i18n = new I18n({
  1228. * locales: ['en', 'fr']
  1229. * });
  1230. */
  1231. if (_OPTS) i18n.configure(_OPTS)
  1232. return i18n
  1233. }
  1234. module.exports = i18n