i18n.js 5.0 KB

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