Add i18n for HTML and manifest
This commit is contained in:
parent
7ce7b2b40b
commit
47e02ef136
6 changed files with 452 additions and 3 deletions
15
Makefile
15
Makefile
|
|
@ -17,7 +17,7 @@ po/POTFILES.js.in:
|
||||||
mkdir -p $(dir $@)
|
mkdir -p $(dir $@)
|
||||||
find src/ -name '*.js' -o -name '*.jsx' -o -name '*.es6' > $@
|
find src/ -name '*.js' -o -name '*.jsx' -o -name '*.es6' > $@
|
||||||
|
|
||||||
po/$(PACKAGE_NAME).pot: po/POTFILES.js.in
|
po/$(PACKAGE_NAME).js.pot: po/POTFILES.js.in
|
||||||
xgettext --default-domain=cockpit --output=$@ --language=C --keyword= \
|
xgettext --default-domain=cockpit --output=$@ --language=C --keyword= \
|
||||||
--keyword=_:1,1t --keyword=_:1c,2,1t --keyword=C_:1c,2 \
|
--keyword=_:1,1t --keyword=_:1c,2,1t --keyword=C_:1c,2 \
|
||||||
--keyword=N_ --keyword=NC_:1c,2 \
|
--keyword=N_ --keyword=NC_:1c,2 \
|
||||||
|
|
@ -26,6 +26,19 @@ po/$(PACKAGE_NAME).pot: po/POTFILES.js.in
|
||||||
--keyword=gettextCatalog.getString:1,3c --keyword=gettextCatalog.getPlural:2,3,4c \
|
--keyword=gettextCatalog.getString:1,3c --keyword=gettextCatalog.getPlural:2,3,4c \
|
||||||
--from-code=UTF-8 --files-from=$^
|
--from-code=UTF-8 --files-from=$^
|
||||||
|
|
||||||
|
po/POTFILES.html.in:
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
find src -name '*.html' > $@
|
||||||
|
|
||||||
|
po/$(PACKAGE_NAME).html.pot: po/POTFILES.html.in
|
||||||
|
po/html2po -f $^ -o $@
|
||||||
|
|
||||||
|
po/$(PACKAGE_NAME).manifest.pot:
|
||||||
|
po/manifest2po src/manifest.json -o $@
|
||||||
|
|
||||||
|
po/$(PACKAGE_NAME).pot: po/$(PACKAGE_NAME).html.pot po/$(PACKAGE_NAME).js.pot po/$(PACKAGE_NAME).manifest.pot
|
||||||
|
msgcat --sort-output --output-file=$@ $^
|
||||||
|
|
||||||
# Update translations against current PO template
|
# Update translations against current PO template
|
||||||
update-po: po/$(PACKAGE_NAME).pot
|
update-po: po/$(PACKAGE_NAME).pot
|
||||||
for lang in $(LINGUAS); do \
|
for lang in $(LINGUAS); do \
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"eslint": "^3.0.0",
|
"eslint": "^3.0.0",
|
||||||
"eslint-loader": "~1.6.1",
|
"eslint-loader": "~1.6.1",
|
||||||
"eslint-plugin-react": "~6.9.0",
|
"eslint-plugin-react": "~6.9.0",
|
||||||
|
"htmlparser": "^1.7.7",
|
||||||
"jed": "^1.1.1",
|
"jed": "^1.1.1",
|
||||||
"jshint": "~2.9.1",
|
"jshint": "~2.9.1",
|
||||||
"jshint-loader": "~0.8.3",
|
"jshint-loader": "~0.8.3",
|
||||||
|
|
|
||||||
10
po/de.po
10
po/de.po
|
|
@ -4,7 +4,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: starter-kit 1.0\n"
|
"Project-Id-Version: starter-kit 1.0\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2018-06-19 10:54+0200\n"
|
"POT-Creation-Date: 2018-06-19 17:07+0200\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
|
@ -13,6 +13,14 @@ msgstr ""
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: src/index.html:20
|
||||||
|
msgid "Cockpit Starter Kit"
|
||||||
|
msgstr "Cockpit Bausatz"
|
||||||
|
|
||||||
#: src/app.jsx:40
|
#: src/app.jsx:40
|
||||||
msgid "Running on $0"
|
msgid "Running on $0"
|
||||||
msgstr "Läuft auf $0"
|
msgstr "Läuft auf $0"
|
||||||
|
|
||||||
|
#: src/manifest.json
|
||||||
|
msgid "Starter Kit"
|
||||||
|
msgstr "Bausatz"
|
||||||
|
|
|
||||||
264
po/html2po
Executable file
264
po/html2po
Executable file
|
|
@ -0,0 +1,264 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extracts translatable strings from HTML files in the following forms:
|
||||||
|
*
|
||||||
|
* <tag translate>String</tag>
|
||||||
|
* <tag translate context="value">String</tag>
|
||||||
|
* <tag translate="...">String</tag>
|
||||||
|
* <tag translate-attr attr="String"></tag>
|
||||||
|
*
|
||||||
|
* Supports the following Glade compatible forms:
|
||||||
|
*
|
||||||
|
* <tag translatable="yes">String</tag>
|
||||||
|
* <tag translatable="yes" context="value">String</tag>
|
||||||
|
*
|
||||||
|
* Supports the following angular-gettext compatible forms:
|
||||||
|
*
|
||||||
|
* <translate>String</translate>
|
||||||
|
* <tag translate-plural="Plural">Singular</tag>
|
||||||
|
*
|
||||||
|
* Note that some of the use of the translated may not support all the strings
|
||||||
|
* depending on the code actually using these strings to translate the HTML.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
function fatal(message, code) {
|
||||||
|
console.log((filename || "html2po") + ": " + message);
|
||||||
|
process.exit(code || 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
console.log("usage: html2po input output");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fs, htmlparser, path, stdio;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs = require('fs');
|
||||||
|
path = require('path');
|
||||||
|
htmlparser = require('htmlparser');
|
||||||
|
stdio = require('stdio');
|
||||||
|
} catch (ex) {
|
||||||
|
fatal(ex.message, 127); /* missing looks for this */
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts = stdio.getopt({
|
||||||
|
directory: { key: "d", args: 1, description: "Base directory for input files" },
|
||||||
|
output: { key: "o", args: 1, description: "Output file" },
|
||||||
|
from: { key: "f", args: 1, description: "File containing list of input files" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!opts.from && opts.args.length < 1) {
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
|
||||||
|
var input = opts.args;
|
||||||
|
var entries = { };
|
||||||
|
|
||||||
|
/* Filename being parsed and offset of line number */
|
||||||
|
var filename = null;
|
||||||
|
var offsets = 0;
|
||||||
|
|
||||||
|
/* The HTML parser we're using */
|
||||||
|
var handler = new htmlparser.DefaultHandler(function(error, dom) {
|
||||||
|
if (error)
|
||||||
|
fatal(error);
|
||||||
|
else
|
||||||
|
walk(dom);
|
||||||
|
});
|
||||||
|
|
||||||
|
prepare();
|
||||||
|
|
||||||
|
/* Decide what input files to process */
|
||||||
|
function prepare() {
|
||||||
|
if (opts.from) {
|
||||||
|
fs.readFile(opts.from, { encoding: "utf-8"}, function(err, data) {
|
||||||
|
if (err)
|
||||||
|
fatal(err.message);
|
||||||
|
input = data.split("\n").filter(function(value) {
|
||||||
|
return !!value;
|
||||||
|
}).concat(input);
|
||||||
|
step();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
step();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Now process each file in turn */
|
||||||
|
function step() {
|
||||||
|
filename = input.shift();
|
||||||
|
if (filename === undefined) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Qualify the filename if necessary */
|
||||||
|
var full = filename;
|
||||||
|
if (opts.directory)
|
||||||
|
full = path.join(opts.directory, filename);
|
||||||
|
|
||||||
|
fs.readFile(full, { encoding: "utf-8"}, function(err, data) {
|
||||||
|
if (err)
|
||||||
|
fatal(err.message);
|
||||||
|
|
||||||
|
var parser = new htmlparser.Parser(handler, { includeLocation: true });
|
||||||
|
parser.parseComplete(data);
|
||||||
|
step();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Process an array of nodes */
|
||||||
|
function walk(children) {
|
||||||
|
if (!children)
|
||||||
|
return;
|
||||||
|
|
||||||
|
children.forEach(function(child) {
|
||||||
|
var line = (child.location || { }).line || 0;
|
||||||
|
var offset = line - 1;
|
||||||
|
|
||||||
|
/* Scripts get their text processed as HTML */
|
||||||
|
if (child.type == 'script' && child.children) {
|
||||||
|
var parser = new htmlparser.Parser(handler, { includeLocation: true });
|
||||||
|
|
||||||
|
/* Make note of how far into the outer HTML file we are */
|
||||||
|
offsets += offset;
|
||||||
|
|
||||||
|
child.children.forEach(function(node) {
|
||||||
|
parser.parseChunk(node.raw);
|
||||||
|
});
|
||||||
|
parser.done();
|
||||||
|
|
||||||
|
offsets -= offset;
|
||||||
|
|
||||||
|
/* Tags get extracted as usual */
|
||||||
|
} else if (child.type == 'tag') {
|
||||||
|
tag(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Process a single loaded tag */
|
||||||
|
function tag(node) {
|
||||||
|
|
||||||
|
var tasks, line, entry;
|
||||||
|
var attrs = node.attribs || { };
|
||||||
|
var nest = true;
|
||||||
|
|
||||||
|
/* Extract translate strings */
|
||||||
|
if ("translate" in attrs || "translatable" in attrs) {
|
||||||
|
tasks = (attrs["translate"] || attrs["translatable"] || "yes").split(" ");
|
||||||
|
|
||||||
|
/* Calculate the line location taking into account nested parsing */
|
||||||
|
line = (node.location || { })["line"] || 0;
|
||||||
|
line += offsets;
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
msgctxt: attrs['translate-context'] || attrs['context'],
|
||||||
|
msgid_plural: attrs['translate-plural'],
|
||||||
|
locations: [ filename + ":" + line ]
|
||||||
|
};
|
||||||
|
|
||||||
|
/* For each thing listed */
|
||||||
|
tasks.forEach(function(task) {
|
||||||
|
var copy = Object.assign({}, entry);
|
||||||
|
|
||||||
|
/* The element text itself */
|
||||||
|
if (task == "yes" || task == "translate") {
|
||||||
|
copy.msgid = extract(node.children);
|
||||||
|
nest = false;
|
||||||
|
|
||||||
|
/* An attribute */
|
||||||
|
} else if (task) {
|
||||||
|
copy.msgid = attrs[task];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copy.msgid)
|
||||||
|
push(copy);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Walk through all the children */
|
||||||
|
if (nest)
|
||||||
|
walk(node.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Push an entry onto the list */
|
||||||
|
function push(entry) {
|
||||||
|
var key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt;
|
||||||
|
var prev = entries[key];
|
||||||
|
if (prev) {
|
||||||
|
prev.locations = prev.locations.concat(entry.locations);
|
||||||
|
} else {
|
||||||
|
entries[key] = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extract the given text */
|
||||||
|
function extract(children) {
|
||||||
|
if (!children)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var i, len, node, str = [];
|
||||||
|
children.forEach(function(node) {
|
||||||
|
if (node.type == 'tag' && node.children)
|
||||||
|
str.push(extract(node.children))
|
||||||
|
else if (node.type == 'text' && node.data)
|
||||||
|
str.push(node.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
return str.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Escape a string for inclusion in po file */
|
||||||
|
function escape(string) {
|
||||||
|
var bs = string.split('\\').join('\\\\').split('"').join('\\"');
|
||||||
|
return bs.split("\n").map(function(line) {
|
||||||
|
return '"' + line + '"';
|
||||||
|
}).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Finish by writing out the strings */
|
||||||
|
function finish() {
|
||||||
|
var result = [
|
||||||
|
'msgid ""',
|
||||||
|
'msgstr ""',
|
||||||
|
'"Project-Id-Version: PACKAGE_VERSION\\n"',
|
||||||
|
'"MIME-Version: 1.0\\n"',
|
||||||
|
'"Content-Type: text/plain; charset=UTF-8\\n"',
|
||||||
|
'"Content-Transfer-Encoding: 8bit\\n"',
|
||||||
|
'"X-Generator: Cockpit html2po\\n"',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
var msgid, entry;
|
||||||
|
for (msgid in entries) {
|
||||||
|
entry = entries[msgid];
|
||||||
|
result.push('#: ' + entry.locations.join(" "));
|
||||||
|
if (entry.msgctxt)
|
||||||
|
result.push('msgctxt ' + escape(entry.msgctxt));
|
||||||
|
result.push('msgid ' + escape(entry.msgid));
|
||||||
|
if (entry.msgid_plural) {
|
||||||
|
result.push('msgid_plural ' + escape(entry.msgid_plural));
|
||||||
|
result.push('msgstr[0] ""');
|
||||||
|
result.push('msgstr[1] ""');
|
||||||
|
} else {
|
||||||
|
result.push('msgstr ""');
|
||||||
|
}
|
||||||
|
result.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = result.join('\n');
|
||||||
|
if (!opts.output) {
|
||||||
|
process.stdout.write(data);
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
fs.writeFile(opts.output, data, function(err) {
|
||||||
|
if (err)
|
||||||
|
fatal(err.message);
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
161
po/manifest2po
Executable file
161
po/manifest2po
Executable file
|
|
@ -0,0 +1,161 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extracts translatable strings from manifest.json files.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fatal(message, code) {
|
||||||
|
console.log((filename || "manifest2po") + ": " + message);
|
||||||
|
process.exit(code || 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
console.log("usage: manifest2po [-o output] input...");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fs, path, stdio;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs = require('fs');
|
||||||
|
path = require('path');
|
||||||
|
stdio = require('stdio');
|
||||||
|
} catch (ex) {
|
||||||
|
fatal(ex.message, 127); /* missing looks for this */
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts = stdio.getopt({
|
||||||
|
directory: { key: "d", args: 1, description: "Base directory for input files" },
|
||||||
|
output: { key: "o", args: 1, description: "Output file" },
|
||||||
|
from: { key: "f", args: 1, description: "File containing list of input files" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!opts.from && opts.args.length < 1) {
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
|
||||||
|
var input = opts.args;
|
||||||
|
var entries = { };
|
||||||
|
|
||||||
|
/* Filename being parsed */
|
||||||
|
var filename = null;
|
||||||
|
|
||||||
|
prepare();
|
||||||
|
|
||||||
|
/* Decide what input files to process */
|
||||||
|
function prepare() {
|
||||||
|
if (opts.from) {
|
||||||
|
fs.readFile(opts.from, { encoding: "utf-8"}, function(err, data) {
|
||||||
|
if (err)
|
||||||
|
fatal(err.message);
|
||||||
|
input = data.split("\n").filter(function(value) {
|
||||||
|
return !!value;
|
||||||
|
}).concat(input);
|
||||||
|
step();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
step();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Now process each file in turn */
|
||||||
|
function step() {
|
||||||
|
filename = input.shift();
|
||||||
|
if (filename === undefined) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.basename(filename) != "manifest.json")
|
||||||
|
return step();
|
||||||
|
|
||||||
|
fs.readFile(filename, { encoding: "utf-8"}, function(err, data) {
|
||||||
|
if (err)
|
||||||
|
fatal(err.message);
|
||||||
|
|
||||||
|
process_manifest(JSON.parse(data));
|
||||||
|
|
||||||
|
return step();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function process_manifest(manifest) {
|
||||||
|
if (manifest.menu)
|
||||||
|
process_menu(manifest.menu);
|
||||||
|
if (manifest.tools)
|
||||||
|
process_menu(manifest.tools);
|
||||||
|
}
|
||||||
|
|
||||||
|
function process_menu(menu) {
|
||||||
|
for (var m in menu) {
|
||||||
|
if (menu[m].label) {
|
||||||
|
push({
|
||||||
|
msgid: menu[m].label,
|
||||||
|
locations: [ filename ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Push an entry onto the list */
|
||||||
|
function push(entry) {
|
||||||
|
var key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt;
|
||||||
|
var prev = entries[key];
|
||||||
|
if (prev) {
|
||||||
|
prev.locations = prev.locations.concat(entry.locations);
|
||||||
|
} else {
|
||||||
|
entries[key] = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Escape a string for inclusion in po file */
|
||||||
|
function escape(string) {
|
||||||
|
var bs = string.split('\\').join('\\\\').split('"').join('\\"');
|
||||||
|
return bs.split("\n").map(function(line) {
|
||||||
|
return '"' + line + '"';
|
||||||
|
}).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Finish by writing out the strings */
|
||||||
|
function finish() {
|
||||||
|
var result = [
|
||||||
|
'msgid ""',
|
||||||
|
'msgstr ""',
|
||||||
|
'"Project-Id-Version: PACKAGE_VERSION\\n"',
|
||||||
|
'"MIME-Version: 1.0\\n"',
|
||||||
|
'"Content-Type: text/plain; charset=UTF-8\\n"',
|
||||||
|
'"Content-Transfer-Encoding: 8bit\\n"',
|
||||||
|
'"X-Generator: Cockpit manifest2po\\n"',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
var msgid, entry;
|
||||||
|
for (msgid in entries) {
|
||||||
|
entry = entries[msgid];
|
||||||
|
result.push('#: ' + entry.locations.join(" "));
|
||||||
|
if (entry.msgctxt)
|
||||||
|
result.push('msgctxt ' + escape(entry.msgctxt));
|
||||||
|
result.push('msgid ' + escape(entry.msgid));
|
||||||
|
if (entry.msgid_plural) {
|
||||||
|
result.push('msgid_plural ' + escape(entry.msgid_plural));
|
||||||
|
result.push('msgstr[0] ""');
|
||||||
|
result.push('msgstr[1] ""');
|
||||||
|
} else {
|
||||||
|
result.push('msgstr ""');
|
||||||
|
}
|
||||||
|
result.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = result.join('\n');
|
||||||
|
if (!opts.output) {
|
||||||
|
process.stdout.write(data);
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
fs.writeFile(opts.output, data, function(err) {
|
||||||
|
if (err)
|
||||||
|
fatal(err.message);
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,10 +40,12 @@ class TestApplication(testlib.MachineCase):
|
||||||
# HACK: work around language switching in Chrome not working in current session (Cockpit issue #8160)
|
# HACK: work around language switching in Chrome not working in current session (Cockpit issue #8160)
|
||||||
b.reload(ignore_cache=True)
|
b.reload(ignore_cache=True)
|
||||||
b.wait_present("#content")
|
b.wait_present("#content")
|
||||||
|
# menu label (from manifest) should be translated
|
||||||
|
b.wait_text("#host-apps a[href='/starter-kit']", "Bausatz")
|
||||||
|
|
||||||
b.go("/starter-kit")
|
b.go("/starter-kit")
|
||||||
b.enter_page("/starter-kit")
|
b.enter_page("/starter-kit")
|
||||||
|
# page label (from js) should be translated
|
||||||
b.wait_in_text(".container-fluid span", "Läuft auf")
|
b.wait_in_text(".container-fluid span", "Läuft auf")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue