XLIFF to/from JSON converter for Polymer i18n-behavior
- Update bundle.*.json values with those from XLIFF
- Generate XLIFF from bundles
- Map todo operations in bundles onto XLIFF states
- Update todo operations in bundles with XLIFF states
- Concise and flexible expressions to customize conversion
- Handy migration from xliff2bundlejson
- UMD support
npm install --save-dev xliff-conv
Quick Tour with polymer-starter-kit-i18n
bower install --save xliff-conv
var XliffConv = require('xliff-conv');
<script src="path/to/bower_components/xliff-conv/xliff-conv.js"></script>
Note: This task has to be processed before Leverage task with unbundle to pick up outputs of this task.
- Next XLIFF files in source
- Current bundle JSON files in source (as output templates)
- Overwritten bundle JSON files in source
var gulp = require('gulp');
var JSONstringify = require('json-stringify-safe');
var stripBom = require('strip-bom');
var through = require('through2');
var XliffConv = require('xliff-conv');
// Import bundles.{lang}.xlf
gulp.task('import-xliff', function () {
var xliffPath = path.join('app', 'xliff');
var xliffConv = new XliffConv();
return gulp.src([
'app/**/xliff/bundle.*.xlf'
])
.pipe(through.obj(function (file, enc, callback) {
var bundle, bundlePath;
var base = path.basename(file.path, '.xlf').match(/^(.*)[.]([^.]*)$/);
var xliff = String(file.contents);
if (base) {
try {
bundlePath = path.join(file.base, 'locales', 'bundle.' + base[2] + '.json');
bundle = JSON.parse(stripBom(fs.readFileSync(bundlePath, 'utf8')));
xliffConv.parseXliff(xliff, { bundle: bundle }, function (output) {
file.contents = new Buffer(JSONstringify(output, null, 2));
file.path = bundlePath;
callback(null, file);
});
}
catch (ex) {
callback(null, file);
}
}
else {
callback(null, file);
}
}))
.pipe(gulp.dest('app'))
.pipe($.size({
title: 'import-xliff'
}));
});
Note: If the todo
items in JSON files are removed, the corresponding trans-unit
s are treated as approved="yes"
and state="translated"
.
- Next bundles object in gulpfile.js
- bundle.{lang}.xlf XLIFF in DEST_DIR/xliff
var gulp = require('gulp');
var through = require('through2');
var XliffConv = require('xliff-conv');
var bundles; // bundles object generated by preprocess and leverage tasks
// Generate bundles.{lang}.xlf
gulp.task('export-xliff', function (callback) {
var DEST_DIR = 'dist';
var srcLanguage = 'en';
var xliffPath = path.join(DEST_DIR, 'xliff');
var xliffConv = new XliffConv();
var promises = [];
try {
fs.mkdirSync(xliffPath);
}
catch (e) {
}
for (var lang in bundles) {
if (lang) {
(function (destLanguage) {
promises.push(new Promise(function (resolve, reject) {
xliffConv.parseJSON(bundles, {
srcLanguage: srcLanguage,
destLanguage: destLanguage
}, function (output) {
fs.writeFile(path.join(xliffPath, 'bundle.' + destLanguage + '.xlf'), output, resolve);
});
}));
})(lang);
}
}
Promise.all(promises).then(function (outputs) {
callback();
});
});
var xliffConv = new XliffConv(options)
- date: Date, default: new Date() - date attribute value for XLIFF
- xliffStates: Object, default: XliffConv.xliffStates.default - todo.op to XLIFF state mapping table
- patterns: Object, default: XliffConv.patterns - A set of named regular expressions for pattern matching
- logger: Function, default: console.log - information logger
- warnLogger: Function, default: console.warn - warning logger
- errorLogger: Function, default: console.error - error logger
XliffConv.xliffStates = {
// All state-less unapproved strings are regarded as needs-translation
'default': {
'add' : [ 'new' ],
'replace': [ 'needs-translation', 'needs-adaptation', 'needs-l10n', '' ],
'review' : [ 'needs-review-translation', 'needs-review-adaptation', 'needs-review-l10n' ],
'default': [ 'translated', 'signed-off', 'final', '[approved]' ]
},
// Aannotations {{name}} and tags <tag-name> are regarded as translated
'annotationsAsTranslated': {
'add' : [ 'new' ],
'replace': [ 'needs-translation', 'needs-adaptation', 'needs-l10n', '' ],
'review' : [ 'needs-review-translation', 'needs-review-adaptation', 'needs-review-l10n' ],
'default': [ 'translated', 'signed-off', 'final', '[approved]', '[source~=annotationsAndTags]' ]
},
// Newly added annotations {{name}} and tags <tag-name> are regarded as translated
'newAnnotationsAsTranslated': {
'add' : [ 'new' ],
'replace': [ 'needs-translation', 'needs-adaptation', 'needs-l10n', '' ],
'review' : [ 'needs-review-translation', 'needs-review-adaptation', 'needs-review-l10n' ],
'default': [ 'translated', 'signed-off', 'final', '[approved]', '[state==new&&source~=annotationsAndTags]' ]
},
// Newly added annotations {{name}} and tags <tag-name> are regarded as translated only at export
'newAnnotationsAsTranslatedAtExport': {
'add' : [ 'new' ],
'replace': [ 'needs-translation', 'needs-adaptation', 'needs-l10n', '' ],
'review' : [ 'needs-review-translation', 'needs-review-adaptation', 'needs-review-l10n' ],
'default': [ 'translated', 'signed-off', 'final', '[approved]', '[export&&state==new&&source~=annotationsAndTags]' ]
},
// Annotations {{name}} and tags <tag-name> are skipped in translation by translate=no
'annotationsAsNoTranslate': {
'add' : [ 'new' ],
'replace': [ 'needs-translation', 'needs-adaptation', 'needs-l10n', '' ],
'review' : [ 'needs-review-translation', 'needs-review-adaptation', 'needs-review-l10n' ],
'default': [ 'translated', 'signed-off', 'final', '[source~=annotationsAndTags&&translate:=no&&state:=final]', '[approved]' ],
},
/* === State Mapping Tables for migration from xliff2bundlejson === */
// All state-less strings are regarded as approved=yes
'approveAll': {
'add' : [ 'new' ],
'replace': [ 'needs-translation', 'needs-adaptation', 'needs-l10n' ],
'review' : [ 'needs-review-translation', 'needs-review-adaptation', 'needs-review-l10n' ],
'default': [ 'translated', 'signed-off', 'final', '' ]
},
// State-less translated strings need review
'reviewTranslated': {
'add' : [ 'new' ],
'replace': [ 'needs-translation', 'needs-adaptation', 'needs-l10n', '[!state&&!approved&&source==target]', '' ],
'review' : [ 'needs-review-translation', 'needs-review-adaptation', 'needs-review-l10n', '[!state&&!approved&&source!=target]' ],
'default': [ 'translated', 'signed-off', 'final', '[approved]' ]
},
// State-less translated strings are regarded as approved=yes
'approveTranslated': {
'add' : [ 'new' ],
'replace': [ 'needs-translation', 'needs-adaptation', 'needs-l10n', '[!state&&!approved&&source==target]', '' ],
'review' : [ 'needs-review-translation', 'needs-review-adaptation', 'needs-review-l10n' ],
'default': [ 'translated', 'signed-off', 'final', '[!state&&!approved&&source!=target]', '[approved]' ]
}
/*
Expression format:
[condition1&&condition2&&...&&effect1&&effect2&&...]
- expression is true when all the conditions are true
- optional effects are processed if the expression is true
Operators for conditions:
parameter
- true if parameter is non-null
!parameter
- true if parameter is undefined, null, or ''
parameter1==parameter2
- true if parameter1 is equal to parameter2
parameter1!=parameter2
- true if parameter1 is not equal to parameter2
parameter~=pattern
- true if parameter matches the regular expression options.patterns.pattern
- if options.patterns.pattern is undefined, pattern is treated as the matching string
tag.attribute~=pattern
- true if attribute value of tag matched the regular expression options.patterns.pattern
- if options.patterns.pattern is undefined, pattern is treated as the matching string
Operators for effects:
tag.attribute:=value
- assign attribute of tag with the string value
attribute:=value
- assign predefined alias attribute with the string value
tag:=value
- assign textContent of tag with the string value
Predefined parameters: Undefined parameters are treated as strings for matching
state
- state attribute of target
id
- id attribute of trans-unit
component
- component name in id
restype
- restype attribute of trans-unit. 'x-json-string' for strings
source
- text content of source tag
target
- text content of target tag
approved
- true if approved attribute of trans-unit is 'yes'
import
- true on XLIFF import (parseXliff); false on XLIFF export (parseJSON)
export
- true on XLIFF export (parseJSON); false on XLIFF import (parseXliff)
Predefined tags:
file
- file tag
trans-unit
- trans-unit tag
source
- source tag
target
- target tag
Predefined alias attributes:
translate
- alias for trans-unit.translate
approved
- alias for trans-unit.approved
state
- alias for target.state
*/
};
XliffConv.patterns = {
'annotationsAndTags': /^({{[^{} ]*}}|\[\[[^\[\] ]*\]\]|<[-a-zA-Z]{1,}>)$/,
'annotations': /^({{[^{} ]*}}|\[\[[^\[\] ]*\]\])$/,
'numbers': /^[0-9.]{1,}$/,
'tags': /^<[-a-zA-Z]{1,}>$/
};
- xliff: String, XLIFF as a string
- options: Object, options.bundle as target bundle JSON object
- callback: Function, callback(output) with output JSON object
- bundles: Object, bundles object
- options.srcLanguage: String, default: 'en' -
<file source-language>
attribute - options.destLanguage: String, default: 'fr' -
<file target-language>
attribute - options.xmlSpace: String, default: 'default' -
<file xml:space>
attribute - options.dataType: String, default: 'plaintext' -
<file datatype>
attribute - options.original: String, default: 'messages' -
<file original>
attribute - options.productName: String, default: 'messages' -
<file product-name>
attribute - options.xmlHeader: String, default:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd">
- options.xliffTemplate: String, default:
<xliff version="1.0">
<file xml:space="[options.xmlSpace]"
source-language="[options.srcLanguage]"
target-language="[options.destLanguage]"
datatype="[options.dataType]"
original="[options.original]"
date="[this.date.toISOString().replace(/[.][0-9]*Z$/, 'Z')]"
product-name="[options.productName]">
<header>
<tool tool-id="xliff-conv" tool-name="xliff-conv" tool-version="[toolVersion]"/>
</header>
<body>
</body>
</file>
</xliff>
- options.transUnitTemplate: String, default:
<trans-unit>
<source></source>
<target></target>
</trans-unit>
- options.addNewAttr: Object, default:
undefined
- Customize id and add a new attribute to
<trans-unit>
with the original id value - labelArrayWithUniqueId is an Object mapping a new attribute value for each id
- Customize id and add a new attribute to
xliffConv.parseJSON(bundles, {
srcLanguage: srcLanguage,
destLanguage: destLanguage,
addNewAttr: {
newAttrName: labelMapWithUniqueId
}
}, function (output) {
fs.writeFile(path.join(xliffPath, 'bundle.' + destLanguage + '.xlf'), output, resolve);
});
// example labelMapWithUniqueId Object
labelMapWithUniqueId =
{
// id: attribute value
"Factory_audit_address": "ckv7ymf07ahqog4lur12bwobg1z3dsxzkqkdwxan",
"alert_info_when_update_config_preferences": "ybsqyempsolypcf4poq1wdxxl8c04oam03ei27bc",
"application_title": "rj7rtcdbefchcbrq9itw6sewjifd2v3c5dn99969",
"back": "48gtruuew3ndd7pnj26lttt0kbgnlv2iyhtti99v",
"barcode_section": "i2d0t2y11b5zlrlhbn5it8qkbxbp7ub0bdgxy7tr",
"cancel_title": "bbzgu18z7wl6thj0eh9p83nlcrz4znyfox4khjuq",
"client_initial_2_letter": "ilttwryn5jccb4wnhfu3nq9z72ds21m2ho7fnsgs"
}
<!-- example trans-unit -->
<!-- without options.addNewAttr -->
<trans-unit id="Factory_audit_address" approved="yes">
<source>Address</source>
<target state="translated">Adresse</target>
</trans-unit>
<!-- with options.addNewAttr = { resname: labelMapWithUniqueId } above -->
<trans-unit id="ckv7ymf07ahqog4lur12bwobg1z3dsxzkqkdwxan" resname="Factory_audit_address" approved="yes">
<source>Address</source>
<target state="translated">Adresse</target>
</trans-unit>
- callback: Function, callback(output) with output XLIFF as a string
- With
options.xliffTemplate
, all the attribute values within the template are NOT replaced. It is the caller's responsibility to set appropriate values to the attributes. - With
options.transUnitTemplate
, XliffConv does NOT recognize<note>
tags in the template in importing XLIFF and discards the contents.
restype | JSON type | Note |
---|---|---|
x-json-string | string | Omitted |
x-json-boolean | boolean | "true" or "false" in value |
x-json-number | number | |
x-json-object | object | Unused for now |
x-json-undefined | undefined | Empty string in value |
todo.op | XLIFF state |
---|---|
add | new |
replace | needs-translation |
review | needs-review-translation |
N/A | translated |
XLIFF state | approved | todo.op | Note |
---|---|---|---|
new | no or N/A | add | |
needs-translation | no or N/A | replace | |
needs-adaptation | no or N/A | replace | |
needs-l10n | no or N/A | replace | |
N/A | no or N/A | replace | |
needs-review-translation | no or N/A | review | |
needs-review-adaptation | no or N/A | review | |
needs-review-l10n | no or N/A | review | |
translated | yes | N/A | Remove todo |
signed-off | yes | N/A | Remove todo |
final | yes | N/A | Remove todo |
N/A | yes | N/A | Remove todo |