82f2b76e25
We now use webpack instead of SystemJS, effectively bundling modules into one file (plus commons chunks) for every entry point. This results in a much smaller extension size (almost half). Furthermore we use yarn/npm even for extension run-time dependencies. This relieves us from manually vendoring and building dependencies. It's also easier to understand for new developers familiar with node.
394 lines
12 KiB
TypeScript
394 lines
12 KiB
TypeScript
/*
|
|
This file is part of TALER
|
|
(C) 2016 GNUnet e.V.
|
|
|
|
TALER is free software; you can redistribute it and/or modify it under the
|
|
terms of the GNU General Public License as published by the Free Software
|
|
Foundation; either version 3, or (at your option) any later version.
|
|
|
|
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
|
|
/**
|
|
* Generate .po file from list of source files.
|
|
*
|
|
* Note that duplicate message IDs are NOT merged, to get the same output as
|
|
* you would from xgettext, just run msguniq.
|
|
*
|
|
* @author Florian Dold
|
|
*/
|
|
|
|
/// <reference path="../decl/node.d.ts" />
|
|
|
|
"use strict";
|
|
|
|
import {readFileSync} from "fs";
|
|
import * as ts from "typescript";
|
|
|
|
|
|
function wordwrap(str: string, width: number = 80): string[] {
|
|
var regex = '.{1,' + width + '}(\\s|$)|\\S+(\\s|$)';
|
|
return str.match(RegExp(regex, 'g'));
|
|
}
|
|
|
|
export function processFile(sourceFile: ts.SourceFile) {
|
|
processNode(sourceFile);
|
|
let lastTokLine = 0;
|
|
let preLastTokLine = 0;
|
|
|
|
function getTemplate(node: ts.Node): string {
|
|
switch (node.kind) {
|
|
case ts.SyntaxKind.FirstTemplateToken:
|
|
return (<any>node).text;
|
|
case ts.SyntaxKind.TemplateExpression:
|
|
let te = <ts.TemplateExpression>node;
|
|
let textFragments = [te.head.text];
|
|
for (let tsp of te.templateSpans) {
|
|
textFragments.push(`%${(textFragments.length-1)/2+1}$s`);
|
|
textFragments.push(tsp.literal.text.replace(/%/g, "%%"));
|
|
}
|
|
return textFragments.join('');
|
|
default:
|
|
return "(pogen.ts: unable to parse)";
|
|
}
|
|
}
|
|
|
|
function getComment(node: ts.Node): string {
|
|
let lc = ts.getLineAndCharacterOfPosition(sourceFile, node.pos);
|
|
let lastComments;
|
|
for (let l = preLastTokLine; l < lastTokLine; l++) {
|
|
let pos = ts.getPositionOfLineAndCharacter(sourceFile, l, 0);
|
|
let comments = ts.getTrailingCommentRanges(sourceFile.text, pos);
|
|
if (comments) {
|
|
lastComments = comments;
|
|
}
|
|
}
|
|
if (!lastComments) {
|
|
return;
|
|
}
|
|
let candidate = lastComments[lastComments.length-1];
|
|
let candidateEndLine = ts.getLineAndCharacterOfPosition(sourceFile, candidate.end).line;
|
|
if (candidateEndLine != lc.line - 1) {
|
|
return;
|
|
}
|
|
let text = sourceFile.text.slice(candidate.pos, candidate.end);
|
|
switch (candidate.kind) {
|
|
case ts.SyntaxKind.SingleLineCommentTrivia:
|
|
// Remove comment leader
|
|
text = text.replace(/^[/][/]\s*/, "");
|
|
break;
|
|
case ts.SyntaxKind.MultiLineCommentTrivia:
|
|
// Remove comment leader and trailer,
|
|
// handling white space just like xgettext.
|
|
text = text
|
|
.replace(/^[/][*](\s*?\n|\s*)?/, "")
|
|
.replace(/(\n[ \t]*?)?[*][/]$/, "");
|
|
break;
|
|
}
|
|
return text;
|
|
}
|
|
|
|
function getPath(node: ts.Node): string[] {
|
|
switch (node.kind) {
|
|
case ts.SyntaxKind.PropertyAccessExpression:
|
|
let pae = <ts.PropertyAccessExpression>node;
|
|
return Array.prototype.concat(getPath(pae.expression), [pae.name.text]);
|
|
case ts.SyntaxKind.Identifier:
|
|
let id = <ts.Identifier>node;
|
|
return [id.text];
|
|
}
|
|
return ["(other)"];
|
|
}
|
|
|
|
function arrayEq<T>(a1: T[], a2: T[]) {
|
|
if (a1.length != a2.length) {
|
|
return false;
|
|
}
|
|
for (let i = 0; i < a1.length; i++) {
|
|
if (a1[i] != a2[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
interface TemplateResult {
|
|
comment: string;
|
|
path: string[];
|
|
template: string;
|
|
line: number;
|
|
}
|
|
|
|
function processTaggedTemplateExpression(tte: ts.TaggedTemplateExpression): TemplateResult {
|
|
let lc = ts.getLineAndCharacterOfPosition(sourceFile, tte.pos);
|
|
if (lc.line != lastTokLine) {
|
|
preLastTokLine = lastTokLine;
|
|
lastTokLine = lc.line;
|
|
}
|
|
let path = getPath(tte.tag)
|
|
let res: TemplateResult = {
|
|
path,
|
|
line: lc.line,
|
|
comment: getComment(tte),
|
|
template: getTemplate(tte.template).replace(/"/g, '\\"'),
|
|
};
|
|
return res;
|
|
}
|
|
|
|
function formatMsgComment(line: number, comment?: string) {
|
|
if (comment) {
|
|
for (let cl of comment.split('\n')) {
|
|
console.log(`#. ${cl}`);
|
|
}
|
|
}
|
|
console.log(`#: ${sourceFile.fileName}:${line+1}`);
|
|
console.log(`#, c-format`);
|
|
}
|
|
|
|
function formatMsgLine(head: string, msg: string) {
|
|
// Do escaping, wrap break at newlines
|
|
let parts = msg
|
|
.match(/(.*\n|.+$)/g)
|
|
.map((x) => x.replace(/\n/g, '\\n'))
|
|
.map((p) => wordwrap(p))
|
|
.reduce((a,b) => a.concat(b));
|
|
if (parts.length == 1) {
|
|
console.log(`${head} "${parts[0]}"`);
|
|
} else {
|
|
console.log(`${head} ""`);
|
|
for (let p of parts) {
|
|
console.log(`"${p}"`);
|
|
}
|
|
}
|
|
}
|
|
|
|
interface JsxProcessingContext {
|
|
|
|
}
|
|
|
|
function getJsxElementPath(node: ts.Node) {
|
|
let path;
|
|
let process = (childNode) => {
|
|
switch (childNode.kind) {
|
|
case ts.SyntaxKind.JsxOpeningElement:
|
|
{
|
|
let e = childNode as ts.JsxOpeningElement;
|
|
return path = getPath(e.tagName);
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
ts.forEachChild(node, process);
|
|
return path;
|
|
}
|
|
|
|
function translateJsxExpression(node: ts.Node, h) {
|
|
switch (node.kind) {
|
|
case ts.SyntaxKind.StringLiteral:
|
|
{
|
|
let e = node as ts.StringLiteral;
|
|
return e.text;
|
|
}
|
|
default:
|
|
return `%${h[0]++}$s`;
|
|
}
|
|
}
|
|
|
|
function trim(s) {
|
|
return s.replace(/^[ \n\t]*/, "").replace(/[ \n\t]*$/, "");
|
|
}
|
|
|
|
function getJsxContent(node: ts.Node) {
|
|
let fragments = [];
|
|
let holeNum = [1];
|
|
let process = (childNode) => {
|
|
switch (childNode.kind) {
|
|
case ts.SyntaxKind.JsxText:
|
|
{
|
|
let e = childNode as ts.JsxText;
|
|
let t = e.getText().split("\n").map(trim).join("\n");
|
|
fragments.push(t);
|
|
}
|
|
case ts.SyntaxKind.JsxOpeningElement:
|
|
break;
|
|
case ts.SyntaxKind.JsxElement:
|
|
fragments.push(`%${holeNum[0]++}$s`);
|
|
break;
|
|
case ts.SyntaxKind.JsxExpression:
|
|
{
|
|
let e = childNode as ts.JsxExpression;
|
|
fragments.push(translateJsxExpression(e.expression, holeNum));
|
|
break;
|
|
}
|
|
case ts.SyntaxKind.JsxClosingElement:
|
|
break;
|
|
default:
|
|
let lc = ts.getLineAndCharacterOfPosition(childNode.getSourceFile(), childNode.getStart());
|
|
console.error(`unrecognized syntax in JSX Element ${ts.SyntaxKind[childNode.kind]} (${childNode.getSourceFile().fileName}:${lc.line+1}:${lc.character+1}`);
|
|
break;
|
|
}
|
|
};
|
|
ts.forEachChild(node, process);
|
|
return fragments.join("");
|
|
}
|
|
|
|
function getJsxSingular(node: ts.Node) {
|
|
let res;
|
|
let process = (childNode) => {
|
|
switch (childNode.kind) {
|
|
case ts.SyntaxKind.JsxElement:
|
|
{
|
|
let path = getJsxElementPath(childNode);
|
|
if (arrayEq(path, ["i18n", "TranslateSingular"])) {
|
|
res = getJsxContent(childNode);
|
|
}
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
ts.forEachChild(node, process);
|
|
return res;
|
|
}
|
|
|
|
function getJsxPlural(node: ts.Node) {
|
|
let res;
|
|
let process = (childNode) => {
|
|
switch (childNode.kind) {
|
|
case ts.SyntaxKind.JsxElement:
|
|
{
|
|
let path = getJsxElementPath(childNode);
|
|
if (arrayEq(path, ["i18n", "TranslatePlural"])) {
|
|
res = getJsxContent(childNode);
|
|
}
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
ts.forEachChild(node, process);
|
|
return res;
|
|
}
|
|
|
|
|
|
function processNode(node: ts.Node) {
|
|
switch (node.kind) {
|
|
case ts.SyntaxKind.JsxElement:
|
|
let path = getJsxElementPath(node);
|
|
if (arrayEq(path, ["i18n", "Translate"])) {
|
|
let content = getJsxContent(node);
|
|
let {line} = ts.getLineAndCharacterOfPosition(sourceFile, node.pos);
|
|
let comment = getComment(node);
|
|
formatMsgComment(line, comment);
|
|
formatMsgLine("msgid", content);
|
|
console.log(`msgstr ""`);
|
|
console.log();
|
|
return;
|
|
}
|
|
if (arrayEq(path, ["i18n", "TranslateSwitch"])) {
|
|
let {line} = ts.getLineAndCharacterOfPosition(sourceFile, node.pos);
|
|
let comment = getComment(node);
|
|
formatMsgComment(line, comment);
|
|
let singularForm = getJsxSingular(node);
|
|
if (!singularForm) {
|
|
console.error("singular form missing");
|
|
process.exit(1);
|
|
}
|
|
let pluralForm = getJsxPlural(node);
|
|
if (!pluralForm) {
|
|
console.error("plural form missing");
|
|
process.exit(1);
|
|
}
|
|
formatMsgLine("msgid", singularForm);
|
|
formatMsgLine("msgid_plural", pluralForm);
|
|
console.log(`msgstr[0] ""`);
|
|
console.log(`msgstr[1] ""`);
|
|
console.log();
|
|
return;
|
|
}
|
|
break;
|
|
case ts.SyntaxKind.CallExpression:
|
|
{
|
|
// might be i18n.plural(i18n[.X]`...`, i18n[.X]`...`)
|
|
let ce = <ts.CallExpression>node;
|
|
let path = getPath(ce.expression);
|
|
if (!arrayEq(path, ["i18n", "plural"])) {
|
|
break;
|
|
}
|
|
if (ce.arguments[0].kind != ts.SyntaxKind.TaggedTemplateExpression) {
|
|
break;
|
|
}
|
|
if (ce.arguments[1].kind != ts.SyntaxKind.TaggedTemplateExpression) {
|
|
break;
|
|
}
|
|
let {line} = ts.getLineAndCharacterOfPosition(sourceFile, ce.pos);
|
|
let t1 = processTaggedTemplateExpression(<ts.TaggedTemplateExpression>ce.arguments[0]);
|
|
let t2 = processTaggedTemplateExpression(<ts.TaggedTemplateExpression>ce.arguments[1]);
|
|
let comment = getComment(ce);
|
|
|
|
formatMsgComment(line, comment);
|
|
formatMsgLine("msgid", t1.template);
|
|
formatMsgLine("msgid_plural", t2.template);
|
|
console.log(`msgstr[0] ""`);
|
|
console.log(`msgstr[1] ""`);
|
|
console.log();
|
|
|
|
// Important: no processing for child i18n expressions here
|
|
return;
|
|
}
|
|
case ts.SyntaxKind.TaggedTemplateExpression:
|
|
{
|
|
let tte = <ts.TaggedTemplateExpression>node;
|
|
let {comment, template, line, path} = processTaggedTemplateExpression(tte);
|
|
if (path[0] != "i18n") {
|
|
break;
|
|
}
|
|
formatMsgComment(line, comment);
|
|
formatMsgLine("msgid", template);
|
|
console.log(`msgstr ""`);
|
|
console.log();
|
|
break;
|
|
}
|
|
}
|
|
|
|
ts.forEachChild(node, processNode);
|
|
}
|
|
}
|
|
|
|
const fileNames = process.argv.slice(2);
|
|
|
|
console.log(
|
|
`# SOME DESCRIPTIVE TITLE.
|
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
# This file is distributed under the same license as the PACKAGE package.
|
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
#
|
|
#, fuzzy
|
|
msgid ""
|
|
msgstr ""
|
|
"Project-Id-Version: PACKAGE VERSION\\n"
|
|
"Report-Msgid-Bugs-To: \\n"
|
|
"POT-Creation-Date: 2016-11-23 00:00+0100\\n"
|
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
|
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
|
|
"Language-Team: LANGUAGE <LL@li.org>\\n"
|
|
"Language: \\n"
|
|
"MIME-Version: 1.0\\n"
|
|
"Content-Type: text/plain; charset=UTF-8\\n"
|
|
"Content-Transfer-Encoding: 8bit\\n"`);
|
|
console.log()
|
|
|
|
fileNames.sort();
|
|
|
|
fileNames.forEach(fileName => {
|
|
let sourceFile = ts.createSourceFile(fileName, readFileSync(fileName).toString(), ts.ScriptTarget.ES2016, /*setParentNodes */ true);
|
|
processFile(sourceFile);
|
|
});
|