i18n.js 5.3 KB

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