i18n.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. 'use strict';
  2. const fs = require('fs');
  3. const path = require('path');
  4. const gutil = require('gulp-util');
  5. const through = require('through2');
  6. const yaml = require('js-yaml');
  7. function readFile(file) {
  8. return new Promise((resolve, reject) => {
  9. fs.readFile(file, 'utf8', (err, data) => err ? reject(err) : resolve(data));
  10. });
  11. }
  12. function Locale(lang, _path, base) {
  13. this.lang = lang;
  14. const ext = this.ext = path.extname(_path);
  15. if (ext) _path = _path.slice(0, -ext.length);
  16. this.path = _path;
  17. this.base = base || '.';
  18. this.data = {};
  19. this.loaded = this.load();
  20. }
  21. Locale.prototype.extensions = ['.yml', '.json'];
  22. Locale.prototype.load = function () {
  23. const file = this.base + '/' + this.path;
  24. return (
  25. this.ext
  26. ? readFile(file + this.ext)
  27. : this.extensions.reduce((promise, ext) => promise.catch(() => (
  28. readFile(file + ext)
  29. .then(data => {
  30. this.ext = ext;
  31. return data;
  32. })
  33. )), Promise.reject())
  34. ).then(data => {
  35. const desc = {};
  36. if (this.ext === '.json') {
  37. data = JSON.parse(data);
  38. } else if (this.ext === '.yml') {
  39. data = yaml.safeLoad(data);
  40. } else {
  41. throw 'Unknown extension name!';
  42. }
  43. for (let key in data) {
  44. this.data[key] = data[key].message;
  45. desc[key] = data[key].description;
  46. }
  47. return desc;
  48. });
  49. };
  50. Locale.prototype.get = function (key, def) {
  51. return this.data[key] || def;
  52. };
  53. Locale.prototype.dump = function (data, ext) {
  54. ext = ext || this.ext;
  55. if (ext === '.json') {
  56. data = JSON.stringify(data, null, 2);
  57. } else if (ext === '.yml') {
  58. data = yaml.safeDump(data);
  59. } else {
  60. throw 'Unknown extension name!';
  61. }
  62. return {
  63. path: this.path + ext,
  64. data,
  65. };
  66. };
  67. function Locales(prefix, base) {
  68. this.prefix = prefix || '.';
  69. this.base = base || '.';
  70. this.langs = [];
  71. this.data = {};
  72. this.desc = {};
  73. this.loaded = this.load();
  74. }
  75. Locales.prototype.defaultLang = 'en';
  76. Locales.prototype.newLocaleItem = 'NEW_LOCALE_ITEM';
  77. Locales.prototype.getLanguages = function () {
  78. const localeDir = this.base + '/' + this.prefix;
  79. return new Promise((resolve, reject) => {
  80. fs.readdir(localeDir, (err, files) => err ? reject(err) : resolve(files));
  81. });
  82. };
  83. Locales.prototype.load = function () {
  84. return this.getLanguages().then(langs => {
  85. this.langs = langs;
  86. return Promise.all(langs.map(lang => {
  87. const locale = this.data[lang] = new Locale(lang, `${this.prefix}/${lang}/messages`, this.base);
  88. return locale.loaded;
  89. }));
  90. }).then(data => {
  91. const desc = data[this.langs.indexOf(this.defaultLang)];
  92. for (let key in desc) {
  93. this.desc[key] = {
  94. touched: false,
  95. value: desc[key],
  96. };
  97. }
  98. });
  99. };
  100. Locales.prototype.getData = function (lang, options) {
  101. options = options || {};
  102. const data = {};
  103. const langData = this.data[lang];
  104. const defaultData = options.useDefaultLang && lang != this.defaultLang && this.data[this.defaultLang];
  105. for (let key in this.desc) {
  106. if (options.touchedOnly && !this.desc[key].touched) continue;
  107. data[key] = {
  108. description: this.desc[key].value || this.newLocaleItem,
  109. message: langData.get(key) || defaultData && defaultData.get(key) || '',
  110. };
  111. if (options.markUntouched && !this.desc[key].touched)
  112. data[key].touched = false;
  113. }
  114. return data;
  115. };
  116. Locales.prototype.dump = function (options) {
  117. return this.langs.map(lang => {
  118. const data = this.getData(lang, options);
  119. const locale = this.data[lang];
  120. const out = locale.dump(data, options.extension);
  121. return new gutil.File({
  122. base: '',
  123. path: out.path,
  124. contents: new Buffer(out.data),
  125. });
  126. });
  127. };
  128. Locales.prototype.touch = function (key) {
  129. let item = this.desc[key];
  130. if (!item) item = this.desc[key] = {
  131. value: this.newLocaleItem,
  132. };
  133. item.touched = true;
  134. };
  135. function extract(options) {
  136. const keys = new Set();
  137. const patterns = {
  138. js: ['_\\.i18n\\(\'(\\w+)\'', 1],
  139. json: ['__MSG_(\\w+)__', 1],
  140. html: ['\'(\\w+)\'\\|i18n', 1],
  141. };
  142. const locales = new Locales(options.prefix, options.base);
  143. function extract(data, types) {
  144. if (!Array.isArray(types)) types = [types];
  145. data = String(data);
  146. types.forEach(function (type) {
  147. const patternData = patterns[type];
  148. const pattern = new RegExp(patternData[0], 'g');
  149. const groupId = patternData[1];
  150. let groups;
  151. while (groups = pattern.exec(data)) {
  152. keys.add(groups[groupId]);
  153. }
  154. });
  155. }
  156. function bufferContents(file, enc, cb) {
  157. if (file.isNull()) return cb();
  158. if (file.isStream())
  159. return this.emit('error', new gutil.PluginError('VM-i18n', 'Stream is not supported.'));
  160. if (file.path.endsWith('.js'))
  161. extract(file.contents, 'js');
  162. else if (file.path.endsWith('.json'))
  163. extract(file.contents, 'json');
  164. else if (file.path.endsWith('.html'))
  165. extract(file.contents, ['html', 'js']);
  166. cb();
  167. }
  168. function endStream(cb) {
  169. locales.loaded.then(() => {
  170. keys.forEach(key => {
  171. locales.touch(key);
  172. });
  173. return locales.dump({
  174. touchedOnly: options.touchedOnly,
  175. useDefaultLang: options.useDefaultLang,
  176. markUntouched: options.markUntouched,
  177. extension: options.extension,
  178. });
  179. }).then(files => {
  180. files.forEach(file => {
  181. this.push(file);
  182. });
  183. cb();
  184. });
  185. }
  186. return through.obj(bufferContents, endStream);
  187. }
  188. module.exports = {
  189. extract,
  190. };