Skip to content

Instantly share code, notes, and snippets.

@paumoreno
Last active June 8, 2022 04:41
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save paumoreno/cdfa14942424e895168a269a2deef1f3 to your computer and use it in GitHub Desktop.
Save paumoreno/cdfa14942424e895168a269a2deef1f3 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'].
const fs = require('fs')
const parse5 = require('parse5')
const gettext = require('gettext-extractor')
const GettextExtractor = gettext.GettextExtractor
const JsExtractors = gettext.JsExtractors
const Readable = require('stream').Readable
const glob = require("glob")
const queue = require('queue')
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) => {
return new Promise((resolve) => {
const readStream = fs.createReadStream(filename, {
encoding: 'utf8'
})
const parser = new parse5.SAXParser({locationInfo: 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', (name, attrs, selfClosing, location) => {
if (depth === 0) {
if (name === 'template' || name === 'script') {
sectionLocations[name] = {
start: location.endOffset,
line: location.line
}
}
}
if (!(selfClosing || selfClosingTags.indexOf(name) > -1)) {
depth++
}
})
parser.on('endTag', (name, location) => {
depth--
if (depth === 0) {
if (name === 'template' || name === 'script') {
sectionLocations[name].end = location.startOffset
}
}
})
readStream.on('open', () => {
readStream.pipe(parser)
})
readStream.on('end', () => {
const content = fs.readFileSync(filename, {
encoding: 'utf8'
})
// 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
let script = 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 parse5.SAXParser({locationInfo: true})
// Look for JS expressions in tag attributes
templateParser.on('startTag', (name, attrs, selfClosing, location) => {
for (let i = 0; i < attrs.length; i++) {
// We're only looking for data bindings, events and directives
if (attrs[i].name.match(/^(:|@|v-)/)) {
snippets.push({
filename,
code: attrs[i].value,
line: location.attrs[attrs[i].name].line
})
}
}
})
// 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', (text, location) => {
let exprMatch
let lineOffset = 0
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.line + lineOffset
})
text = text.substr(exprMatch.index + exprMatch[0].length)
lineOffset += matchedLines - 1
}
})
const s = new Readable
s.on('end', () => {
resolve(snippets)
})
s.push(template)
s.push(null)
s.pipe(templateParser)
})
})
}
const parser = extractor
.createJsParser([
// Place all the possible expressions to extract here:
JsExtractors.callExpression(['$t', '[this].$t', 'i18n.t'], {
arguments: {
text: 0
}
})
])
parser.parseFilesGlob('src/**/*.js')
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) {
extractor.savePotFile('./i18n/messages.pot')
extractor.printStats()
}
})
}
})
@rgoupil
Copy link

rgoupil commented May 4, 2020

FYI we needed a similar script for our needs.
It didn't run out of the box (nor did any of the existing forks) so we fixed it here with a few small additions, in case anyone need it: https://gist.github.com/rgoupil/bba97f652701189108cacb9f0ee8d392

@johnfelipe
Copy link

how can solve this?

Error: Cannot find module '/root/XREngine/root/XREngine/packages/client/extract.js'
	at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
	at Function.Module._load (node:internal/modules/cjs/loader:778:27)
	at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
	at node:internal/main/run_main_module:17:47 {
  code: 'MODULE_NOT_FOUND',
  requireStack: []
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment