123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- const fs = require('fs').promises;
- const path = require('path');
- const Vinyl = require('vinyl');
- const PluginError = require('plugin-error');
- const through = require('through2');
- const yaml = require('js-yaml');
- const transformers = {
- '.yml': data => yaml.load(data),
- '.json': data => JSON.parse(data),
- };
- class Locale {
- constructor(lang, base) {
- this.defaultLocale = 'messages.yml';
- this.lang = lang;
- this.base = base;
- this.data = {};
- this.desc = {};
- }
- async load() {
- const localeDir = `${this.base}/${this.lang}`;
- let files = await fs.readdir(localeDir);
- files = [this.defaultLocale].concat(files.filter(file => file !== this.defaultLocale));
- for (const file of files) {
- const ext = path.extname(file);
- const transformer = transformers[ext];
- if (transformer) {
- try {
- const res = await fs.readFile(`${localeDir}/${file}`, 'utf8');
- Object.assign(this.data, transformer(res));
- } catch (err) {
- if (err.code !== 'ENOENT') {
- throw err;
- }
- }
- }
- }
- Object.keys(this.data)
- .forEach(key => {
- this.desc[key] = this.desc[key] || this.data[key].description;
- });
- }
- getMessage(key, def) {
- const item = this.data[key];
- return item && item.message || def;
- }
- get(key) {
- return this.data[key];
- }
- dump(data, { extension, stripDescriptions }) {
- if (extension === '.json') {
- if (stripDescriptions) {
- data = Object.entries(data).reduce((res, [key, value]) => {
- // eslint-disable-next-line no-unused-vars
- const { description, ...stripped } = value;
- res[key] = stripped;
- return res;
- }, {});
- }
- data = JSON.stringify(data, null, 2);
- } else if (extension === '.yml') {
- data = yaml.dump(data, { sortKeys: true });
- } else {
- throw 'Unknown extension name!';
- }
- return {
- path: `${this.lang}/messages${extension}`,
- data,
- };
- }
- }
- class Locales {
- constructor(options) {
- this.options = options;
- this.defaultLang = 'en';
- this.newLocaleItem = 'NEW_LOCALE_ITEM';
- this.base = options.base || '.';
- this.langs = [];
- this.locales = {};
- this.data = {};
- }
- async load() {
- const langs = await fs.readdir(this.base);
- this.langs = langs;
- await Promise.all(langs.map(async lang => {
- const locale = new Locale(lang, this.base);
- await locale.load();
- this.locales[lang] = locale;
- }));
- const defaultData = this.locales[this.defaultLang];
- Object.keys(defaultData.desc).forEach(key => {
- this.data[key] = {
- ...defaultData.data[key],
- touched: this.options.markUntouched ? false : defaultData.get(key).touched !== false,
- };
- });
- }
- getData(lang, options) {
- options = options || {};
- const data = {};
- const langData = this.locales[lang];
- const defaultData = options.useDefaultLang && lang != this.defaultLang && this.locales[this.defaultLang];
- Object.keys(this.data).forEach(key => {
- if (options.touchedOnly && !this.data[key].touched) return;
- data[key] = {
- description: this.data[key].description || this.newLocaleItem,
- message: langData.getMessage(key) || defaultData && defaultData.getMessage(key) || '',
- };
- if (options.markUntouched && !this.data[key].touched) data[key].touched = false;
- });
- return data;
- }
- dump(options) {
- options = { ...this.options, ...options };
- return this.langs.map(lang => {
- const data = this.getData(lang, options);
- const locale = this.locales[lang];
- return locale.dump(data, options);
- });
- }
- touch(key) {
- let item = this.data[key];
- if (!item) item = this.data[key] = {
- description: this.newLocaleItem,
- };
- item.touched = true;
- }
- }
- function extract(options) {
- const keys = new Set();
- const patterns = {
- default: ['\\b(?:i18n\\(\'|i18n-key=")(\\w+)[\'"]', 1],
- json: ['__MSG_(\\w+)__', 1],
- };
- const typePatternMap = {
- '.js': 'default',
- '.json': 'json',
- '.html': 'default',
- '.vue': 'default',
- };
- const locales = new Locales(options);
- function extractFile(data, types) {
- if (!Array.isArray(types)) types = [types];
- data = String(data);
- types.forEach(function (type) {
- const patternData = patterns[type];
- const pattern = new RegExp(patternData[0], 'g');
- const groupId = patternData[1];
- let groups;
- while ((groups = pattern.exec(data))) {
- keys.add(groups[groupId]);
- }
- });
- }
- function bufferContents(file, enc, cb) {
- if (file.isNull()) return cb();
- if (file.isStream()) return this.emit('error', new PluginError('VM-i18n', 'Stream is not supported.'));
- const extname = path.extname(file.path);
- const type = typePatternMap[extname];
- if (type) extractFile(file.contents, type);
- cb();
- }
- function endStream(cb) {
- locales.load()
- .then(() => {
- keys.forEach(key => {
- locales.touch(key);
- });
- return locales.dump()
- .map(out => new Vinyl({
- path: out.path,
- contents: Buffer.from(out.data),
- }));
- })
- .then(files => {
- files.forEach(file => {
- this.push(file);
- });
- cb();
- })
- .catch(cb);
- }
- return through.obj(bufferContents, endStream);
- }
- function read(options) {
- const stream = extract(options);
- process.nextTick(() => stream.end());
- return stream;
- }
- module.exports = {
- extract,
- read,
- };
|