From b2e79200da0dc9180a70d035d9e761de36eb3bee Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Tue, 19 Jun 2018 11:00:30 +0200 Subject: [PATCH] Add i18n support Make the "Running on.." string translatable and copy the extraction and conversion of JSX and PO files from Cockpit. --- .gitignore | 2 + Makefile | 38 +++++++++++- package.json | 3 + po/de.po | 18 ++++++ po/po.empty.js | 14 +++++ po/po2json | 127 +++++++++++++++++++++++++++++++++++++++++ src/app.jsx | 4 +- test/check-application | 17 ++++++ 8 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 po/de.po create mode 100644 po/po.empty.js create mode 100755 po/po2json diff --git a/.gitignore b/.gitignore index c20079a..d5df499 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ Test*FAIL* bots/ test/common/ test/images/ +*.pot +POTFILES* diff --git a/Makefile b/Makefile index 9020706..ec92333 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,41 @@ VM_IMAGE=$(CURDIR)/test/images/$(TEST_OS) all: dist/index.js -dist/index.js: node_modules/react-lite $(wildcard src/*) package.json webpack.config.js +# +# i18n +# + +LINGUAS=$(basename $(notdir $(wildcard po/*.po))) + +po/POTFILES.js.in: + mkdir -p $(dir $@) + find src/ -name '*.js' -o -name '*.jsx' -o -name '*.es6' > $@ + +po/$(PACKAGE_NAME).pot: po/POTFILES.js.in + xgettext --default-domain=cockpit --output=$@ --language=C --keyword= \ + --keyword=_:1,1t --keyword=_:1c,2,1t --keyword=C_:1c,2 \ + --keyword=N_ --keyword=NC_:1c,2 \ + --keyword=gettext:1,1t --keyword=gettext:1c,2,2t \ + --keyword=ngettext:1,2,3t --keyword=ngettext:1c,2,3,4t \ + --keyword=gettextCatalog.getString:1,3c --keyword=gettextCatalog.getPlural:2,3,4c \ + --from-code=UTF-8 --files-from=$^ + +# Update translations against current PO template +update-po: po/$(PACKAGE_NAME).pot + for lang in $(LINGUAS); do \ + msgmerge --output-file=po/$$lang.po po/$$lang.po $<; \ + done + +dist/po.%.js: po/%.po + mkdir -p $(dir $@) + po/po2json -m po/po.empty.js -o $@.js.tmp $< + mv $@.js.tmp $@ + +# +# Build/Install/dist +# + +dist/index.js: node_modules/react-lite $(wildcard src/*) package.json webpack.config.js $(patsubst %,dist/po.%.js,$(LINGUAS)) NODE_ENV=$(NODE_ENV) npm run build clean: @@ -80,4 +114,4 @@ test/common: node_modules/react-lite: npm install -.PHONY: all clean install devel-install dist-gzip srpm rpm check vm +.PHONY: all clean install devel-install dist-gzip srpm rpm check vm update-po diff --git a/package.json b/package.json index 2a0d3cf..8563e43 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,12 @@ "eslint": "^3.0.0", "eslint-loader": "~1.6.1", "eslint-plugin-react": "~6.9.0", + "jed": "^1.1.1", "jshint": "~2.9.1", "jshint-loader": "~0.8.3", + "po2json": "^0.4.5", "sizzle": "^2.3.3", + "stdio": "^0.2.7", "webpack": "^2.6.1" }, "dependencies": { diff --git a/po/de.po b/po/de.po new file mode 100644 index 0000000..a37688a --- /dev/null +++ b/po/de.po @@ -0,0 +1,18 @@ +# starter-kit German translations +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: starter-kit 1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-19 10:54+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/app.jsx:40 +msgid "Running on $0" +msgstr "Läuft auf $0" diff --git a/po/po.empty.js b/po/po.empty.js new file mode 100644 index 0000000..b27dad9 --- /dev/null +++ b/po/po.empty.js @@ -0,0 +1,14 @@ +(function (root, data) { + var loaded, module; + + /* Load into Cockpit locale */ + if (typeof cockpit === 'object') { + cockpit.locale(data) + loaded = true; + } + + if (!loaded) + root.po = data; + +/* The syntax of this line is important by po2json */ +}(this, {"":{"language":"en"}})); diff --git a/po/po2json b/po/po2json new file mode 100755 index 0000000..d2185a4 --- /dev/null +++ b/po/po2json @@ -0,0 +1,127 @@ +#!/usr/bin/env node + +function fatal(message, code) { + console.log((filename || "html2po") + ": " + message); + process.exit(code || 1); +} + +function usage() { + console.log("usage: po2json [--module=template.js] input output"); + process.exit(2); +} + +var fs, po2json, Jed, stdio; + +try { + fs = require('fs'); + po2json = require('po2json'); + Jed = require('jed'); + stdio = require('stdio'); +} catch(ex) { + fatal(ex.message, 127); /* missing looks for this */ +} + +var argi = 2; +var filename = null; + +var opts = stdio.getopt({ + module: { key: "m", args: 1, description: "Module template to include" }, + output: { key: "o", args: 1, description: "Output file" }, +}); + +if (opts.args.length != 1) { + usage(); +} + +parse(); + +function prepareHeader(header) { + var body, statement, plurals = header["plural-forms"], ret = null; + if (plurals) { + try { + /* Check that the plural forms isn't being sneaky since we build a function here */ + Jed.PF.parse(plurals); + } catch(ex) { + fatal("bad plural forms: " + ex.message, 1); + } + + /* A function for the front end */ + statement = header["plural-forms"]; + if (statement[statement.length - 1] != ';') + statement += ';'; + ret = 'function(n) {\nvar nplurals, plural;\n' + statement + '\nreturn plural;\n}'; + + /* Added back in later */ + delete header["plural-forms"]; + } + + /* We don't need to be transferring this */ + delete header["project-id-version"]; + delete header["report-msgid-bugs-to"]; + delete header["pot-creation-date"]; + delete header["po-revision-date"]; + delete header["last-translator"]; + delete header["language-team"]; + delete header["mime-version"]; + delete header["content-type"]; + delete header["content-transfer-encoding"]; + + return ret; +} + +/* Parse and process the po data */ +function parse() { + filename = opts.args[0]; + po2json.parseFile(opts.args[0], { "fuzzy": true }, function(err, jsonData) { + var plurals, pos; + + if (err) + fatal(err.message); + + var header = jsonData[""]; + if (header) + plurals = prepareHeader(header); + + var data = JSON.stringify(jsonData, null, 1); + + /* We know the brace in is the location to insert our function */ + if (plurals) { + pos = data.indexOf('{', 1); + data = data.substr(0, pos + 1) + "'plural-forms':" + String(plurals) + "," + data.substr(pos + 1); + } + + if (data == JSON.stringify({})) + finish(""); + else + wrap(data); + }); +} + +/* Wrap the data if desired */ +function wrap(data) { + if (opts.module) { + filename = opts.module; + fs.readFile(opts.module, { encoding: "utf-8" }, function(err, template) { + if (err) + fatal(err.message); + data = template.replace('{"":{"language":"en"}}', data); + finish(data); + }); + } else { + finish(data); + } +} + +/* Write it out */ +function finish(data) { + if (opts.output) { + fs.writeFile(opts.output, data, function(err) { + if (err) + fatal(err.message); + process.exit(0); + }); + } else { + process.stdout.write(data); + process.exit(0); + } +} diff --git a/src/app.jsx b/src/app.jsx index a3a2fb0..8c81d9f 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -21,6 +21,8 @@ import cockpit from 'cockpit'; import React from 'react'; +const _ = cockpit.gettext; + export class Application extends React.Component { constructor() { super(); @@ -35,7 +37,7 @@ export class Application extends React.Component {

Starter Kit

- Running on {this.state.hostname} + { cockpit.format(_("Running on $0"), this.state.hostname) }
); diff --git a/test/check-application b/test/check-application index 0f4b22b..5efe8b0 100755 --- a/test/check-application +++ b/test/check-application @@ -1,4 +1,5 @@ #!/usr/bin/python +# coding: UTF-8 # Run this with --help to see available options for tracing and debugging # See https://github.com/cockpit-project/cockpit/blob/master/test/common/testlib.py # "class Browser" and "class MachineCase" for the available API. @@ -28,6 +29,22 @@ class TestApplication(testlib.MachineCase): b.wait_present(".container-fluid span") b.wait_text(".container-fluid span", "Running on " + hostname) + # change language to German + b.switch_to_top() + b.click("#content-user-name") + b.click(".display-language-menu a") + b.wait_popup('display-language') + b.set_val("#display-language select", "de-de") + b.click("#display-language-select-button") + b.expect_load() + # HACK: work around language switching in Chrome not working in current session (Cockpit issue #8160) + b.reload(ignore_cache=True) + b.wait_present("#content") + + b.go("/starter-kit") + b.enter_page("/starter-kit") + + b.wait_in_text(".container-fluid span", "Läuft auf") if __name__ == '__main__': testlib.test_main()