## Problem
- The storage page crashes after selecting Chinese language ("zh-Hans")
- https://bugzilla.suse.com/show_bug.cgi?id=1238584
- A similar problem exists also with the `pt_BR` language as it also
uses a country code. (All other languages use just a simple language
code without any separator.) But it is less serious as the `pt_BR`
locale is known by glibc so we do not need to do any additional
conversions besides using the correct language separator.
- The underlying problem is that Linux and the Web (browsers) use two
different standards for identifying locales and languages: POSIX locale
identifiers such as `zh_TW.UTF-8` and IETF language tags such as
`zh-Hant`. Agama sometimes uses a punctuation-only conversion which only
works for some cases.
## Solution
- Change the `zh_Hans` in Weblate to `zh_CN` so we do not need to do any
conversions
- Change the web frontend to consistently use the dash (`-`) separator
in all places, avoid unnecessary conversions, convert to underscore only
when sending to the backend
- Convert the underscore (`_`) separator to dash (`-`) when importing
the frontend translations from Weblate
- Handle exceptions in the `Intl.ListFormat` formatting function, in
case of exception fallback to a simple formatting function with comma
separator (", ")
## Notes
- It turned out that actually only the `C.UTF-8` and `en_US.UTF-8`
locales were present in the Live ISO. Unfortunately the filtering regexp
was not updated after renaming the PO translation files from
`po.<lang>.js` to `po-<lang>.js`. 😱
- I have changed the code to read the supported languages from the
`languages.json` file. That allows more precise filtering. In the past
we kept e.g. all `de_DE`, `de_AT`, `de_CH` for German language. Now we
know that after selecting German the `de_DE` locale will be used so we
can delete the `de_AT` and `de_CH` locales from the live ISO.
## Testing
- Added a new unit test
- Tested manually
- The web frontend does not crash a properly displays the formatted
text, the `Intl.ListFormat` function is used in the `New partitions will
be created for "/" <and> "swap"` text
- The backend receives a valid Linux locale (`zh_CN.UTF-8`)
- The Web UI translation files use the dash separator
- The Live ISO contains all supported locales (`locale -a`)


---------
Co-authored-by: YaST Bot <yast-devel@opensuse.org>
Co-authored-by: Martin Vidner <mvidner@suse.com>
115 lines
3.6 KiB
JavaScript
Executable file
115 lines
3.6 KiB
JavaScript
Executable file
#! /usr/bin/env node
|
|
|
|
// Helper script for converting Gettext PO files to Javascript so they can be
|
|
// loaded by the web frontend.
|
|
//
|
|
// Inspired by the Cockpit Webpack plugin
|
|
// https://github.com/cockpit-project/cockpit/blob/main/pkg/lib/cockpit-po-plugin.js
|
|
//
|
|
// Usage:
|
|
// cd web/src/po
|
|
// SRCDIR=../../../../agama-weblate/web ../../share/po/po-converter.js
|
|
|
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
|
|
import path from "node:path";
|
|
import fs from "node:fs";
|
|
import { fileURLToPath } from "url";
|
|
import * as glob from "glob";
|
|
import gettext_parser from "gettext-parser";
|
|
import jed from "jed";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const srcdir = process.env.SRCDIR || process.cwd();
|
|
const template = fs.readFileSync(path.resolve(__dirname, "po.template.js"), "utf-8");
|
|
|
|
function poFiles() {
|
|
return glob.sync(path.resolve(srcdir, "*.po"));
|
|
}
|
|
|
|
// read the supported languages from languages.json file
|
|
function supportedLanguages() {
|
|
const langs = path.resolve(__dirname, "../../src/languages.json");
|
|
const data = JSON.parse(fs.readFileSync(langs, "utf8"));
|
|
return Object.keys(data);
|
|
}
|
|
|
|
// extract the plural form function
|
|
function pluralForm(statement) {
|
|
try {
|
|
// try parsing the plural form function definition string to ensure it is valid, the jed parser
|
|
// contains a full parser for the Gettext plural forms definitions and avoids using eval() which
|
|
// is insecure for 3rd party files
|
|
jed.PF.parse(statement);
|
|
} catch (error) {
|
|
console.error("Invalid plural form definition", statement);
|
|
console.error(error.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
const pluralFunc = statement.replace(/nplurals=[1-9]; plural=([^;]*);?$/, "(n) => $1");
|
|
if (pluralFunc === statement) {
|
|
console.error("Cannot extract the plural form function from definition: ", statement);
|
|
process.exit(1);
|
|
}
|
|
|
|
return pluralFunc;
|
|
}
|
|
|
|
// convert a single PO file to JS
|
|
function buildFile(po_file) {
|
|
return new Promise((resolve, _reject) => {
|
|
const parsed = gettext_parser.po.parse(fs.readFileSync(po_file), "utf8");
|
|
const language = parsed.headers.Language.replace("_", "-");
|
|
// remove the second header copy
|
|
delete parsed.translations[""][""];
|
|
|
|
const result = {
|
|
// translations header
|
|
"": {
|
|
"plural-forms": pluralForm(parsed.headers["Plural-Forms"]),
|
|
language,
|
|
},
|
|
};
|
|
|
|
for (const [_msgctxt, context] of Object.entries(parsed.translations)) {
|
|
for (const [msgid, translation] of Object.entries(context)) {
|
|
// ignore fuzzy translations
|
|
if (translation.comments.flag && translation.comments.flag.match(/\bfuzzy\b/)) continue;
|
|
|
|
result[msgid] = translation.msgstr;
|
|
}
|
|
}
|
|
|
|
// sort the keys
|
|
const sortedResult = {};
|
|
Object.keys(result)
|
|
.sort()
|
|
.forEach((k) => {
|
|
sortedResult[k] = result[k];
|
|
});
|
|
|
|
// remove the double quotes to convert the plural forms from a string to a Javascript function
|
|
const js = JSON.stringify(sortedResult, null, 2).replace(
|
|
/"plural-forms": "([^"]+)"/,
|
|
'"plural-forms": $1',
|
|
);
|
|
|
|
// inject the result to the template file
|
|
const output = template.replace('"PO_CONTENT"', js);
|
|
fs.writeFileSync(`po.${language}.js`, output);
|
|
|
|
resolve();
|
|
});
|
|
}
|
|
|
|
const supported = supportedLanguages();
|
|
const files = poFiles().filter((f) => {
|
|
const base = path.basename(f, ".po").replace("_", "-");
|
|
// full match or language match
|
|
return supported.includes(base) || supported.some((s) => s.split("-")[0] === base);
|
|
});
|
|
|
|
Promise.all(files.map((f) => buildFile(f)));
|