i18n.js 5.3 KB

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