Skip to content

Instantly share code, notes, and snippets.

@rgoupil
Forked from paumoreno/README.md
Last active June 20, 2022 08:01
Show Gist options
  • Save rgoupil/bba97f652701189108cacb9f0ee8d392 to your computer and use it in GitHub Desktop.
Save rgoupil/bba97f652701189108cacb9f0ee8d392 to your computer and use it in GitHub Desktop.
Extract all messages from a Vue.js with vue-i18n app, using gettext-extractor.

This script uses the great message extraction library gettext-extractor by lukasgeiter.

The script assumes that the location of the source files is ./src. It parses both .js and .vue files. It writes the PO template file in ./i18n/messages.pot.

Some things to note:

  • It assumes that interpolations in the templates use the delimieters {{}} (it is the most commmon case).
  • It assumes that both the template and the script sections of the .vue single file components are defined inline, and not referenced by a src attribue (it is the most common case).
  • Expressions to extract are hardcoded. Currently they are ['$t', '[this].$t', 'i18n.t'].

Edits:

  • Use parse5-sax-parser
  • Fix runtime error preventing the script from running (might not be efficient with large files)
  • Add mkdirp to dynamically create the folder if it doesn't exist
  • Parse js, jsx, ts and tsx files
const fs = require('fs');
const mkdirp = require('mkdirp');
const gettext = require('gettext-extractor');
const { GettextExtractor } = gettext;
const { JsExtractors } = gettext;
const glob = require('glob');
const queue = require('queue');
const SAXParser = require('parse5-sax-parser');
const extractor = new GettextExtractor();
const selfClosingTags = [
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr',
];
const parseVueFile = (filename) => new Promise((resolve) => {
const content = fs.readFileSync(filename, { encoding: 'utf8' });
const parser = new SAXParser({ sourceCodeLocationInfo: true });
let depth = 0;
const sectionLocations = {
template: null,
script: null,
};
// Get the location of the `template` and `script` tags, which should be top-level
parser.on('startTag', token => {
const name = token.tagName;
const location = token.sourceCodeLocation;
const { selfClosing } = token;
if (depth === 0) {
if (name === 'template' || name === 'script') {
sectionLocations[name] = {
start: location.endOffset,
line: location.startLine,
};
}
}
if (!(selfClosing || selfClosingTags.indexOf(name) > -1)) {
depth++;
}
});
parser.on('endTag', token => {
const name = token.tagName;
const location = token.sourceCodeLocation;
depth--;
if (depth === 0) {
if (name === 'template' || name === 'script') {
sectionLocations[name].end = location.startOffset;
}
}
});
parser.on('end', () => {
// Get the contents of the `template` and `script` sections, if present.
// We're assuming that the content is inline, not referenced by an `src` attribute.
// https://vue-loader.vuejs.org/en/start/spec.html
let template = null;
const snippets = [];
if (sectionLocations.template) {
template = content.substr(
sectionLocations.template.start,
sectionLocations.template.end - sectionLocations.template.start,
);
}
if (sectionLocations.script) {
snippets.push({
filename,
code: content.substr(
sectionLocations.script.start,
sectionLocations.script.end - sectionLocations.script.start,
),
line: sectionLocations.script.line,
});
}
// Parse the template looking for JS expressions
const templateParser = new SAXParser({
sourceCodeLocationInfo: true,
});
// Look for JS expressions in tag attributes
templateParser.on('startTag', token => {
const { attrs } = token;
const location = token.sourceCodeLocation;
for (let i = 0; i < attrs.length; i++) {
// We're only looking for data bindings, events and directives
const { name } = attrs[i];
// let wtf = location.attrs[name]
// if (wtf) {
// }else {
// debugger
// }
if (name.match(/^(:|@|v-)/)) {
snippets.push({
filename,
code: attrs[i].value,
line: location.attrs[name].startLine,
});
}
}
});
// Look for interpolations in text contents.
// We're assuming {{}} as delimiters for interpolations.
// These delimiters could change using Vue's `delimiters` option.
// https://vuejs.org/v2/api/#delimiters
templateParser.on('text', token => {
let { text } = token;
const location = token.sourceCodeLocation;
let exprMatch;
let lineOffset = 0;
// eslint-disable-next-line no-cond-assign
while (exprMatch = text.match(/{{([\s\S]*?)}}/)) {
const prevLines = text.substr(0, exprMatch.index).split(/\r\n|\r|\n/).length;
const matchedLines = exprMatch[1].split(/\r\n|\r|\n/).length;
lineOffset += prevLines - 1;
snippets.push({
code: exprMatch[1],
line: location.startLine + lineOffset,
});
text = text.substr(exprMatch.index + exprMatch[0].length);
lineOffset += matchedLines - 1;
}
});
templateParser.on('end', () => {
resolve(snippets);
});
templateParser.write(template);
templateParser.end();
});
parser.write(content);
parser.end();
});
const parser = extractor
.createJsParser([
JsExtractors.callExpression(['$t', '[this].$t', 'i18n.t'], {
arguments: {
text: 0,
context: 1,
},
comments: {
otherLineLeading: true,
sameLineLeading: true,
sameLineTrailing: true,
regex: /\s*@i18n\s*(.*)/,
},
}),
]);
parser.parseFilesGlob('src/**/*.(ts|tsx|js|jsx)');
const q = queue({
concurrency: 1,
});
glob('src/**/*.vue', (err, files) => {
if (!err) {
files.map((filename) => {
q.push((cb) => {
console.log(`parsing ${filename}`);
parseVueFile(filename).then((snippets) => {
for (let i = 0; i < snippets.length; i++) {
parser.parseString(snippets[i].code, filename, {
lineNumberStart: snippets[i].line,
});
}
cb();
});
});
});
q.start((err) => {
if (!err) {
const folder = './i18n';
mkdirp.sync(folder);
extractor.savePotFile(`${folder}/messages.pot`);
extractor.printStats();
}
});
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment