i18n.js 5.3 KB

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