subcommands for i18n tooling, unique message IDs

This commit is contained in:
Florian Dold 2022-02-15 17:45:26 +01:00
parent e6c1294c91
commit 465ccdaa06
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
5 changed files with 317 additions and 251 deletions

View File

@ -23,38 +23,44 @@ import * as po2json from "po2json";
import * as fs from "fs";
import * as path from "path";
const files = fs
.readdirSync("./src/i18n")
.filter((x) => x.endsWith(".po"))
.map((x) => path.join("./src/i18n/", x));
export function po2ts(): void {
const files = fs
.readdirSync("./src/i18n")
.filter((x) => x.endsWith(".po"))
.map((x) => path.join("./src/i18n/", x));
if (files.length === 0) {
console.error("no .po files found in src/i18n/");
process.exit(1);
}
console.log(files);
const chunks: string[] = [];
for (const filename of files) {
const m = filename.match(/([a-zA-Z0-9-_]+).po/);
if (!m) {
console.error("error: unexpected filename (expected <lang>.po)");
if (files.length === 0) {
console.error("no .po files found in src/i18n/");
process.exit(1);
}
const lang = m[1];
const pojson = po2json.parseFileSync(filename, {
format: "jed1.x",
fuzzy: true,
});
const s =
"strings['" + lang + "'] = " + JSON.stringify(pojson, null, " ") + ";\n\n";
chunks.push(s);
console.log(files);
const chunks: string[] = [];
for (const filename of files) {
const m = filename.match(/([a-zA-Z0-9-_]+).po/);
if (!m) {
console.error("error: unexpected filename (expected <lang>.po)");
process.exit(1);
}
const lang = m[1];
const pojson = po2json.parseFileSync(filename, {
format: "jed1.x",
fuzzy: true,
});
const s =
"strings['" +
lang +
"'] = " +
JSON.stringify(pojson, null, " ") +
";\n\n";
chunks.push(s);
}
const tsContents = chunks.join("");
fs.writeFileSync("src/i18n/strings.ts", tsContents);
}
const tsContents = chunks.join("");
fs.writeFileSync("src/i18n/strings.ts", tsContents);

View File

@ -0,0 +1,21 @@
import { potextract } from "./potextract.js";
function usage(): never {
console.log("usage: pogen <extract|merge|emit>");
process.exit(1);
}
export function main() {
const subcommand = process.argv[2];
if (process.argv.includes("--help") || !subcommand) {
usage();
}
switch (subcommand) {
case "extract":
potextract();
break;
default:
console.error(`unknown subcommand '${subcommand}'`);
usage();
}
}

View File

@ -14,21 +14,27 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import * as ts from "typescript";
import * as fs from "fs";
import * as os from "os";
import path = require("path/posix");
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, outChunks: string[]) {
processNode(sourceFile);
function processFile(
sourceFile: ts.SourceFile,
outChunks: string[],
knownMessageIds: Set<string>,
) {
let lastTokLine = 0;
let preLastTokLine = 0;
processNode(sourceFile);
function getTemplate(node: ts.Node): string {
switch (node.kind) {
@ -140,7 +146,8 @@ export function processFile(sourceFile: ts.SourceFile, outChunks: string[]) {
outChunks.push(`#. ${cl}\n`);
}
}
outChunks.push(`#: ${sourceFile.fileName}:${line + 1}\n`);
const fn = path.relative(process.cwd(), sourceFile.fileName);
outChunks.push(`#: ${fn}:${line + 1}\n`);
outChunks.push(`#, c-format\n`);
}
@ -148,7 +155,7 @@ export function processFile(sourceFile: ts.SourceFile, outChunks: string[]) {
// Do escaping, wrap break at newlines
let parts = msg
.match(/(.*\n|.+$)/g)
.map((x) => x.replace(/\n/g, "\\n"))
.map((x) => x.replace(/\n/g, "\\n").replace(/"/g, '\\"'))
.map((p) => wordwrap(p))
.reduce((a, b) => a.concat(b));
if (parts.length == 1) {
@ -188,7 +195,7 @@ export function processFile(sourceFile: ts.SourceFile, outChunks: string[]) {
}
}
function trim(s) {
function trim(s: string) {
return s.replace(/^[ \n\t]*/, "").replace(/[ \n\t]*$/, "");
}
@ -284,10 +291,13 @@ export function processFile(sourceFile: ts.SourceFile, outChunks: string[]) {
let content = getJsxContent(node);
let { line } = ts.getLineAndCharacterOfPosition(sourceFile, node.pos);
let comment = getComment(node);
formatMsgComment(line, comment);
formatMsgLine("msgid", content);
outChunks.push(`msgstr ""\n`);
outChunks.push("\n");
if (!knownMessageIds.has(content)) {
knownMessageIds.add(content);
formatMsgComment(line, comment);
formatMsgLine("msgid", content);
outChunks.push(`msgstr ""\n`);
outChunks.push("\n");
}
return;
}
if (arrayEq(path, ["i18n", "TranslateSwitch"])) {
@ -304,11 +314,14 @@ export function processFile(sourceFile: ts.SourceFile, outChunks: string[]) {
console.error("plural form missing");
process.exit(1);
}
formatMsgLine("msgid", singularForm);
formatMsgLine("msgid_plural", pluralForm);
outChunks.push(`msgstr[0] ""\n`);
outChunks.push(`msgstr[1] ""\n`);
outChunks.push(`\n`);
if (!knownMessageIds.has(singularForm)) {
knownMessageIds.add(singularForm);
formatMsgLine("msgid", singularForm);
formatMsgLine("msgid_plural", pluralForm);
outChunks.push(`msgstr[0] ""\n`);
outChunks.push(`msgstr[1] ""\n`);
outChunks.push(`\n`);
}
return;
}
break;
@ -333,13 +346,16 @@ export function processFile(sourceFile: ts.SourceFile, outChunks: string[]) {
<ts.TaggedTemplateExpression>ce.arguments[1],
);
let comment = getComment(ce);
formatMsgComment(line, comment);
formatMsgLine("msgid", t1.template);
formatMsgLine("msgid_plural", t2.template);
outChunks.push(`msgstr[0] ""\n`);
outChunks.push(`msgstr[1] ""\n`);
outChunks.push("\n");
const msgid = t1.template;
if (!knownMessageIds.has(msgid)) {
knownMessageIds.add(msgid);
formatMsgComment(line, comment);
formatMsgLine("msgid", t1.template);
formatMsgLine("msgid_plural", t2.template);
outChunks.push(`msgstr[0] ""\n`);
outChunks.push(`msgstr[1] ""\n`);
outChunks.push("\n");
}
// Important: no processing for child i18n expressions here
return;
@ -351,10 +367,14 @@ export function processFile(sourceFile: ts.SourceFile, outChunks: string[]) {
if (path[0] != "i18n") {
break;
}
formatMsgComment(line, comment);
formatMsgLine("msgid", template);
outChunks.push(`msgstr ""\n`);
outChunks.push("\n");
const msgid = template;
if (!knownMessageIds.has(msgid)) {
knownMessageIds.add(msgid);
formatMsgComment(line, comment);
formatMsgLine("msgid", template);
outChunks.push(`msgstr ""\n`);
outChunks.push("\n");
}
break;
}
}
@ -363,51 +383,48 @@ export function processFile(sourceFile: ts.SourceFile, outChunks: string[]) {
}
}
const configPath = ts.findConfigFile(
/*searchPath*/ "./",
ts.sys.fileExists,
"tsconfig.json",
);
if (!configPath) {
throw new Error("Could not find a valid 'tsconfig.json'.");
}
export function potextract() {
const configPath = ts.findConfigFile(
/*searchPath*/ "./",
ts.sys.fileExists,
"tsconfig.json",
);
if (!configPath) {
throw new Error("Could not find a valid 'tsconfig.json'.");
}
console.log(configPath);
const cmdline = ts.getParsedCommandLineOfConfigFile(
configPath,
{},
{
fileExists: ts.sys.fileExists,
getCurrentDirectory: ts.sys.getCurrentDirectory,
onUnRecoverableConfigFileDiagnostic: (e) => console.log(e),
readDirectory: ts.sys.readDirectory,
readFile: ts.sys.readFile,
useCaseSensitiveFileNames: true,
},
);
const cmdline = ts.getParsedCommandLineOfConfigFile(
configPath,
{},
{
fileExists: ts.sys.fileExists,
getCurrentDirectory: ts.sys.getCurrentDirectory,
onUnRecoverableConfigFileDiagnostic: (e) => console.log(e),
readDirectory: ts.sys.readDirectory,
readFile: ts.sys.readFile,
useCaseSensitiveFileNames: true,
},
);
const prog = ts.createProgram({
options: cmdline.options,
rootNames: cmdline.fileNames,
});
console.log(cmdline);
const allFiles = prog.getSourceFiles();
const prog = ts.createProgram({
options: cmdline.options,
rootNames: cmdline.fileNames,
});
const ownFiles = allFiles.filter(
(x) =>
!x.isDeclarationFile &&
!prog.isSourceFileFromExternalLibrary(x) &&
!prog.isSourceFileDefaultLibrary(x),
);
const allFiles = prog.getSourceFiles();
//console.log(ownFiles.map((x) => x.fileName));
const ownFiles = allFiles.filter(
(x) =>
!x.isDeclarationFile &&
!prog.isSourceFileFromExternalLibrary(x) &&
!prog.isSourceFileDefaultLibrary(x),
);
const chunks = [];
console.log(ownFiles.map((x) => x.fileName));
const chunks = [];
chunks.push(`# SOME DESCRIPTIVE TITLE.
chunks.push(`# 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.
@ -424,10 +441,26 @@ msgstr ""
"Language: \\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"`);
"Content-Transfer-Encoding: 8bit\\n"\n\n`);
for (const f of ownFiles) {
processFile(f, chunks);
const knownMessageIds = new Set<string>();
for (const f of ownFiles) {
processFile(f, chunks, knownMessageIds);
}
const pot = chunks.join("");
//console.log(pot);
const packageJson = JSON.parse(
fs.readFileSync("./package.json", { encoding: "utf-8" }),
);
const poDomain = packageJson.pogen?.domain;
if (!poDomain) {
console.error("missing 'pogen.domain' field in package.json");
process.exit(1);
}
fs.writeFileSync(`./src/i18n/${poDomain}.pot`, pot);
}
console.log(chunks.join(""));

View File

@ -15,7 +15,9 @@
"build-storybook": "build-storybook",
"storybook": "start-storybook -s . -p 6006",
"pretty": "prettier --write src",
"watch": "tsc --watch & rollup -w -c"
"watch": "tsc --watch & rollup -w -c",
"i18n:extract": "pogen extract",
"i18n:msgmerge": "pogen msgmerge"
},
"dependencies": {
"@gnu-taler/taler-util": "workspace:*",
@ -71,5 +73,8 @@
"**"
],
"exclude": []
},
"pogen": {
"domain": "taler-wallet-webex"
}
}

View File

@ -1,21 +1,12 @@
# 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/>
# 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: Taler Wallet\n"
"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"
@ -25,266 +16,276 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/util/wire.ts:37
#, c-format
msgid "Invalid Wire"
msgstr ""
#: src/util/wire.ts:42 src/util/wire.ts:45
#, c-format
msgid "Invalid Test Wire Detail"
msgstr ""
#: src/util/wire.ts:47
#, c-format
msgid "Test Wire Acct #%1$s on %2$s"
msgstr ""
#: src/util/wire.ts:49
#, c-format
msgid "Unknown Wire Detail"
msgstr ""
#: src/webex/pages/benchmark.tsx:52
#, c-format
msgid "Operation"
msgstr ""
#: src/webex/pages/benchmark.tsx:53
#, c-format
msgid "time (ms/op)"
msgstr ""
#: src/webex/pages/pay.tsx:130
#, c-format
msgid "The merchant %1$s offers you to purchase:"
msgstr ""
#: src/webex/pages/pay.tsx:136
#, c-format
msgid "The total price is %1$s (plus %2$s fees)."
msgstr ""
#: src/webex/pages/pay.tsx:141
#, c-format
msgid "The total price is %1$s."
msgstr ""
#: src/webex/pages/pay.tsx:163
#, c-format
msgid "Retry"
msgstr ""
#: src/webex/pages/pay.tsx:173
#, c-format
msgid "Confirm payment"
msgstr ""
#: src/webex/pages/popup.tsx:153
#: src/NavigationBar.tsx:86
#, c-format
msgid "Balance"
msgstr ""
#: src/webex/pages/popup.tsx:154
#: src/NavigationBar.tsx:87
#, c-format
msgid "History"
msgid "Pending"
msgstr ""
#: src/webex/pages/popup.tsx:155
#: src/NavigationBar.tsx:88
#, c-format
msgid "Debug"
msgid "Backup"
msgstr ""
#: src/webex/pages/popup.tsx:175
#: src/NavigationBar.tsx:89
#, c-format
msgid "Settings"
msgstr ""
#: src/NavigationBar.tsx:90
#, c-format
msgid "Dev"
msgstr ""
#: src/wallet/BackupPage.tsx:127
#, c-format
msgid "Add provider"
msgstr ""
#: src/wallet/BackupPage.tsx:137
#, c-format
msgid "Sync all backups"
msgstr ""
#: src/wallet/BackupPage.tsx:139
#, c-format
msgid "Sync now"
msgstr ""
#: src/popup/BalancePage.tsx:79
#, c-format
msgid "You have no balance to show. Need some %1$s getting started?"
msgstr ""
#: src/webex/pages/popup.tsx:238
#: src/wallet/ProviderAddPage.tsx:145
#, c-format
msgid "%1$s incoming"
msgid "&lt; Back"
msgstr ""
#: src/webex/pages/popup.tsx:250
#: src/wallet/ProviderAddPage.tsx:156
#, c-format
msgid "%1$s being spent"
msgid "Next"
msgstr ""
#: src/webex/pages/popup.tsx:281
#: src/wallet/ProviderDetailPage.tsx:57
#, c-format
msgid "Error: could not retrieve balance information."
msgid "Loading..."
msgstr ""
#: src/webex/pages/popup.tsx:390
#: src/wallet/ProviderDetailPage.tsx:64
#, c-format
msgid "Invalid "
msgid "There was an error loading the provider detail for \"%1$s\""
msgstr ""
#: src/webex/pages/popup.tsx:396
#: src/wallet/ProviderDetailPage.tsx:75
#, c-format
msgid "Fees "
msgid "There is not known provider with url \"%1$s\". Redirecting back..."
msgstr ""
#: src/webex/pages/popup.tsx:434
#: src/wallet/ProviderDetailPage.tsx:131
#, c-format
msgid "Refresh sessions has completed"
msgid "Back up"
msgstr ""
#: src/webex/pages/popup.tsx:451
#: src/wallet/ProviderDetailPage.tsx:142
#, c-format
msgid "Order Refused"
msgid "Extend"
msgstr ""
#: src/webex/pages/popup.tsx:465
#: src/wallet/ProviderDetailPage.tsx:148
#, c-format
msgid "Order redirected"
msgid ""
"terms has changed, extending the service will imply accepting the new terms of "
"service"
msgstr ""
#: src/webex/pages/popup.tsx:482
#: src/wallet/ProviderDetailPage.tsx:158
#, c-format
msgid "Payment aborted"
msgid "old"
msgstr ""
#: src/webex/pages/popup.tsx:512
#: src/wallet/ProviderDetailPage.tsx:162
#, c-format
msgid "Payment Sent"
msgid "new"
msgstr ""
#: src/webex/pages/popup.tsx:536
#: src/wallet/ProviderDetailPage.tsx:169
#, c-format
msgid "Order accepted"
msgid "fee"
msgstr ""
#: src/webex/pages/popup.tsx:547
#: src/wallet/ProviderDetailPage.tsx:177
#, c-format
msgid "Reserve balance updated"
msgid "storage"
msgstr ""
#: src/webex/pages/popup.tsx:559
#: src/wallet/ProviderDetailPage.tsx:190
#, c-format
msgid "Payment refund"
msgid "&lt; back"
msgstr ""
#: src/webex/pages/popup.tsx:584
#: src/wallet/ProviderDetailPage.tsx:194
#, c-format
msgid "Withdrawn"
msgid "remove provider"
msgstr ""
#: src/webex/pages/popup.tsx:596
#: src/wallet/ProviderDetailPage.tsx:213
#, c-format
msgid "Tip Accepted"
msgid "There is conflict with another backup from %1$s"
msgstr ""
#: src/webex/pages/popup.tsx:606
#: src/wallet/ProviderDetailPage.tsx:228
#, c-format
msgid "Tip Declined"
msgid "Unknown backup problem: %1$s"
msgstr ""
#: src/webex/pages/popup.tsx:615
#: src/wallet/ProviderDetailPage.tsx:247
#, c-format
msgid "%1$s"
msgid "service paid"
msgstr ""
#: src/webex/pages/popup.tsx:707
#: src/popup/Settings.tsx:46
#, c-format
msgid "Your wallet has no events recorded."
msgid "Permissions"
msgstr ""
#: src/webex/pages/return-coins.tsx:124
#: src/cta/TermsOfServiceSection.tsx:37
#, c-format
msgid "Wire to bank account"
msgid "Exchange doesn't have terms of service"
msgstr ""
#: src/webex/pages/return-coins.tsx:206
#: src/cta/TermsOfServiceSection.tsx:56
#, c-format
msgid "Confirm"
msgid "Review exchange terms of service"
msgstr ""
#: src/webex/pages/return-coins.tsx:209
#: src/cta/TermsOfServiceSection.tsx:63
#, c-format
msgid "Review new version of terms of service"
msgstr ""
#: src/cta/TermsOfServiceSection.tsx:75
#, c-format
msgid "Show terms of service"
msgstr ""
#: src/cta/TermsOfServiceSection.tsx:83
#, c-format
msgid "I accept the exchange terms of service"
msgstr ""
#: src/cta/TermsOfServiceSection.tsx:127
#, c-format
msgid "Hide terms of service"
msgstr ""
#: src/wallet/ExchangeAddConfirm.tsx:110
#, c-format
msgid "Cancel"
msgstr ""
#: src/webex/pages/withdraw.tsx:73
#: src/wallet/ExchangeAddConfirm.tsx:114
#, c-format
msgid "Could not get details for withdraw operation:"
msgid "Loading terms.."
msgstr ""
#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
#: src/wallet/ExchangeAddConfirm.tsx:121
#, c-format
msgid "Chose different exchange provider"
msgid "Add exchange"
msgstr ""
#: src/webex/pages/withdraw.tsx:109
#: src/wallet/ExchangeAddConfirm.tsx:131
#, c-format
msgid ""
"Please select an exchange. You can review the details before after your "
"selection."
msgid "Add exchange anyway"
msgstr ""
#: src/webex/pages/withdraw.tsx:121
#: src/wallet/Settings.tsx:95
#, c-format
msgid "Select %1$s"
msgid "Known exchanges"
msgstr ""
#: src/webex/pages/withdraw.tsx:143
#: src/wallet/Transaction.tsx:159
#, c-format
msgid "Select custom exchange"
msgid "retry"
msgstr ""
#: src/webex/pages/withdraw.tsx:163
#: src/wallet/Transaction.tsx:163
#, c-format
msgid "You are about to withdraw %1$s from your bank account into your wallet."
msgid "Forget"
msgstr ""
#: src/webex/pages/withdraw.tsx:174
#: src/wallet/Transaction.tsx:198
#, c-format
msgid "Accept fees and withdraw"
msgid "Confirm"
msgstr ""
#: src/webex/pages/withdraw.tsx:192
#: src/cta/Pay.tsx:211
#, c-format
msgid "Cancel withdraw operation"
msgid "Pay with a mobile phone"
msgstr ""
#: src/webex/renderHtml.tsx:249
#: src/cta/Pay.tsx:211
#, c-format
msgid "Withdrawal fees:"
msgid "Hide QR"
msgstr ""
#: src/webex/renderHtml.tsx:252
#: src/cta/Pay.tsx:241
#, c-format
msgid "Rounding loss:"
msgid "Pay"
msgstr ""
#: src/webex/renderHtml.tsx:254
#: src/cta/Pay.tsx:265
#, c-format
msgid "Earliest expiration (for deposit): %1$s"
msgid "Withdraw digital cash"
msgstr ""
#: src/webex/renderHtml.tsx:262
#: src/cta/Pay.tsx:295
#, c-format
msgid "# Coins"
msgid "Digital cash payment"
msgstr ""
#: src/webex/renderHtml.tsx:263
#: src/cta/Withdraw.tsx:101
#, c-format
msgid "Value"
msgid "Digital cash withdrawal"
msgstr ""
#: src/webex/renderHtml.tsx:264
#: src/cta/Withdraw.tsx:149
#, c-format
msgid "Withdraw Fee"
msgid "Cancel exchange selection"
msgstr ""
#: src/webex/renderHtml.tsx:265
#: src/cta/Withdraw.tsx:150
#, c-format
msgid "Refresh Fee"
msgid "Confirm exchange selection"
msgstr ""
#: src/webex/renderHtml.tsx:266
#: src/cta/Withdraw.tsx:155
#, c-format
msgid "Deposit Fee"
msgid "Switch exchange"
msgstr ""
#: src/cta/Withdraw.tsx:174
#, c-format
msgid "Confirm withdrawal"
msgstr ""
#: src/cta/Withdraw.tsx:183
#, c-format
msgid "Withdraw anyway"
msgstr ""
#: src/cta/Withdraw.tsx:310
#, c-format
msgid "missing withdraw uri"
msgstr ""
#: src/cta/Deposit.tsx:186
#, c-format
msgid "Digital cash deposit"
msgstr ""