i18n.js 5.3 KB

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