index.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. let shim;
  2. class Y18N {
  3. constructor(opts) {
  4. // configurable options.
  5. opts = opts || {};
  6. this.directory = opts.directory || './locales';
  7. this.updateFiles = typeof opts.updateFiles === 'boolean' ? opts.updateFiles : true;
  8. this.locale = opts.locale || 'en';
  9. this.fallbackToLanguage = typeof opts.fallbackToLanguage === 'boolean' ? opts.fallbackToLanguage : true;
  10. // internal stuff.
  11. this.cache = Object.create(null);
  12. this.writeQueue = [];
  13. }
  14. __(...args) {
  15. if (typeof arguments[0] !== 'string') {
  16. return this._taggedLiteral(arguments[0], ...arguments);
  17. }
  18. const str = args.shift();
  19. let cb = function () { }; // start with noop.
  20. if (typeof args[args.length - 1] === 'function')
  21. cb = args.pop();
  22. cb = cb || function () { }; // noop.
  23. if (!this.cache[this.locale])
  24. this._readLocaleFile();
  25. // we've observed a new string, update the language file.
  26. if (!this.cache[this.locale][str] && this.updateFiles) {
  27. this.cache[this.locale][str] = str;
  28. // include the current directory and locale,
  29. // since these values could change before the
  30. // write is performed.
  31. this._enqueueWrite({
  32. directory: this.directory,
  33. locale: this.locale,
  34. cb
  35. });
  36. }
  37. else {
  38. cb();
  39. }
  40. return shim.format.apply(shim.format, [this.cache[this.locale][str] || str].concat(args));
  41. }
  42. __n() {
  43. const args = Array.prototype.slice.call(arguments);
  44. const singular = args.shift();
  45. const plural = args.shift();
  46. const quantity = args.shift();
  47. let cb = function () { }; // start with noop.
  48. if (typeof args[args.length - 1] === 'function')
  49. cb = args.pop();
  50. if (!this.cache[this.locale])
  51. this._readLocaleFile();
  52. let str = quantity === 1 ? singular : plural;
  53. if (this.cache[this.locale][singular]) {
  54. const entry = this.cache[this.locale][singular];
  55. str = entry[quantity === 1 ? 'one' : 'other'];
  56. }
  57. // we've observed a new string, update the language file.
  58. if (!this.cache[this.locale][singular] && this.updateFiles) {
  59. this.cache[this.locale][singular] = {
  60. one: singular,
  61. other: plural
  62. };
  63. // include the current directory and locale,
  64. // since these values could change before the
  65. // write is performed.
  66. this._enqueueWrite({
  67. directory: this.directory,
  68. locale: this.locale,
  69. cb
  70. });
  71. }
  72. else {
  73. cb();
  74. }
  75. // if a %d placeholder is provided, add quantity
  76. // to the arguments expanded by util.format.
  77. const values = [str];
  78. if (~str.indexOf('%d'))
  79. values.push(quantity);
  80. return shim.format.apply(shim.format, values.concat(args));
  81. }
  82. setLocale(locale) {
  83. this.locale = locale;
  84. }
  85. getLocale() {
  86. return this.locale;
  87. }
  88. updateLocale(obj) {
  89. if (!this.cache[this.locale])
  90. this._readLocaleFile();
  91. for (const key in obj) {
  92. if (Object.prototype.hasOwnProperty.call(obj, key)) {
  93. this.cache[this.locale][key] = obj[key];
  94. }
  95. }
  96. }
  97. _taggedLiteral(parts, ...args) {
  98. let str = '';
  99. parts.forEach(function (part, i) {
  100. const arg = args[i + 1];
  101. str += part;
  102. if (typeof arg !== 'undefined') {
  103. str += '%s';
  104. }
  105. });
  106. return this.__.apply(this, [str].concat([].slice.call(args, 1)));
  107. }
  108. _enqueueWrite(work) {
  109. this.writeQueue.push(work);
  110. if (this.writeQueue.length === 1)
  111. this._processWriteQueue();
  112. }
  113. _processWriteQueue() {
  114. const _this = this;
  115. const work = this.writeQueue[0];
  116. // destructure the enqueued work.
  117. const directory = work.directory;
  118. const locale = work.locale;
  119. const cb = work.cb;
  120. const languageFile = this._resolveLocaleFile(directory, locale);
  121. const serializedLocale = JSON.stringify(this.cache[locale], null, 2);
  122. shim.fs.writeFile(languageFile, serializedLocale, 'utf-8', function (err) {
  123. _this.writeQueue.shift();
  124. if (_this.writeQueue.length > 0)
  125. _this._processWriteQueue();
  126. cb(err);
  127. });
  128. }
  129. _readLocaleFile() {
  130. let localeLookup = {};
  131. const languageFile = this._resolveLocaleFile(this.directory, this.locale);
  132. try {
  133. // When using a bundler such as webpack, readFileSync may not be defined:
  134. if (shim.fs.readFileSync) {
  135. localeLookup = JSON.parse(shim.fs.readFileSync(languageFile, 'utf-8'));
  136. }
  137. }
  138. catch (err) {
  139. if (err instanceof SyntaxError) {
  140. err.message = 'syntax error in ' + languageFile;
  141. }
  142. if (err.code === 'ENOENT')
  143. localeLookup = {};
  144. else
  145. throw err;
  146. }
  147. this.cache[this.locale] = localeLookup;
  148. }
  149. _resolveLocaleFile(directory, locale) {
  150. let file = shim.resolve(directory, './', locale + '.json');
  151. if (this.fallbackToLanguage && !this._fileExistsSync(file) && ~locale.lastIndexOf('_')) {
  152. // attempt fallback to language only
  153. const languageFile = shim.resolve(directory, './', locale.split('_')[0] + '.json');
  154. if (this._fileExistsSync(languageFile))
  155. file = languageFile;
  156. }
  157. return file;
  158. }
  159. _fileExistsSync(file) {
  160. return shim.exists(file);
  161. }
  162. }
  163. export function y18n(opts, _shim) {
  164. shim = _shim;
  165. const y18n = new Y18N(opts);
  166. return {
  167. __: y18n.__.bind(y18n),
  168. __n: y18n.__n.bind(y18n),
  169. setLocale: y18n.setLocale.bind(y18n),
  170. getLocale: y18n.getLocale.bind(y18n),
  171. updateLocale: y18n.updateLocale.bind(y18n),
  172. locale: y18n.locale
  173. };
  174. }