diff --git a/.github/workflows/product_translations/.gitignore b/.github/workflows/product_translations/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/.github/workflows/product_translations/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/.github/workflows/product_translations/package-lock.json b/.github/workflows/product_translations/package-lock.json new file mode 100644 index 0000000000..c85e3946d9 --- /dev/null +++ b/.github/workflows/product_translations/package-lock.json @@ -0,0 +1,218 @@ +{ + "name": "product_translations", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "gettext-parser": "^7.0.1", + "yaml": "^2.3.4" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/gettext-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-7.0.1.tgz", + "integrity": "sha512-LU+ieGH3L9HmKEArTlX816/iiAlyA0fx/n/QSeQpkAaH/+jxMk/5UtDkAzcVvW+KlY25/U+IE6dnfkJ8ynt8pQ==", + "dev": true, + "dependencies": { + "content-type": "^1.0.5", + "encoding": "^0.1.13", + "readable-stream": "^4.3.0", + "safe-buffer": "^5.2.1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/.github/workflows/product_translations/package.json b/.github/workflows/product_translations/package.json new file mode 100644 index 0000000000..f542ad082f --- /dev/null +++ b/.github/workflows/product_translations/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "gettext-parser": "^7.0.1", + "yaml": "^2.3.4" + } +} diff --git a/.github/workflows/product_translations/products_pot b/.github/workflows/product_translations/products_pot new file mode 100755 index 0000000000..08f143c655 --- /dev/null +++ b/.github/workflows/product_translations/products_pot @@ -0,0 +1,143 @@ +#! /usr/bin/node + +/* +This script generates the POT file with product translations from product +YAML files. + +Requirements: NodeJS, run "npm ci" to install the needed NPM packages + +Usage: + + products_pot *.yaml +*/ + +const fs = require("fs"); +const process = require("process"); + +const { Parser, LineCounter, parseDocument } = require("yaml"); +const gettextParser = require("gettext-parser"); + +/** + * Translatable text with source location + */ +class POEntry { + text; + file; + line; + product; + + /** + * Constructor + * @param {string} text - text of the description + * @param {string} file - file name + * @param {number} line - line location + * @param {string} product - name of the product + */ + constructor(text, file, line, product) { + this.text = text; + this.file = file; + this.line = line; + this.product = product; + } +} + +// collects translatable texts (POEntries) and generates the final POT file +class POFile { + entries = []; + + /** + * generate a time stamp string for the POT file header + * @returns {string} timestamp + */ + timestamp() { + const date = new Date(); + return date.getUTCFullYear() + "-" + date.getUTCMonth() + "-" + + date.getUTCDate() + " " + date.getUTCHours() + ":" + date.getUTCMinutes() + + "+0000"; + } + + /** + * generate the POT file content + * @returns {string} generated POT file or empty string if there are no entries + */ + dump() { + if (this.entries.length === 0) return ""; + + // template file with the default POT file header + const template = require("./template.json"); + template.headers["POT-Creation-Date"] = this.timestamp(); + + const translations = template.translations[""]; + + this.entries.forEach(e => { + translations[e.text] = { + msgid: e.text, + comments: { + translator: `TRANSLATORS: description of product "${e.product}"`, + reference: e.file + ":" + e.line + }, + msgstr: [""] + }; + }); + + // sort the output by the msgid to have consistent results + return String(gettextParser.po.compile(template, { sort: true })); + } +} + +/** + * Reads and parses the YAML product file + */ +class YamlReader { + file; + + /** + * Constructor + * @param {string} file - name of the YAML file to read + */ + constructor(file) { + this.file = file; + } + + /** + * Read and parse the YAML file + * @returns {undefined,POEntry} the found description entry or `undefined` if not found + */ + description() { + const data = fs.readFileSync(this.file, "utf-8"); + + // get the parsed text value + const parsed = parseDocument(data); + const description = parsed.get("description"); + if (!description) return; + + const product = parsed.get("name"); + + const lineCounter = new LineCounter(); + const tokens = new Parser(lineCounter.addNewLine).parse(data); + + for (const token of tokens) { + if (token.type === "document") { + // get the line position of the value + const description_token = token.value.items.find(i => i.key.source === "description"); + const line = lineCounter.linePos(description_token.value.offset).line; + return new POEntry(description, this.file, line, product); + } + } + } +} + +const output = new POFile(); +// script arguments (the first arg is executor path ("/usr/bin/node"), +// the second is name of this script) +const [,, ...params] = process.argv; + +params.forEach(f => { + const reader = new YamlReader(f); + const descriptionEntry = reader.description(); + if (descriptionEntry) { + output.entries.push(descriptionEntry); + } +}); + +console.log(output.dump()); diff --git a/.github/workflows/product_translations/template.json b/.github/workflows/product_translations/template.json new file mode 100644 index 0000000000..a2f3a02074 --- /dev/null +++ b/.github/workflows/product_translations/template.json @@ -0,0 +1,26 @@ +{ + "charset": "utf-8", + "headers": { + "Project-Id-Version": "PACKAGE VERSION", + "Report-Msgid-Bugs-To": "", + "PO-Revision-Date": "YEAR-MO-DA HO:MI+ZONE", + "Last-Translator": "FULL NAME ", + "Language-Team": "LANGUAGE ", + "Language": "", + "MIME-Version": "1.0", + "Content-Type": "text/plain; charset=utf-8", + "Content-Transfer-Encoding": "8bit", + "Plural-Forms": "nplurals=INTEGER; plural=EXPRESSION;" + }, + "translations": { + "": { + "": { + "msgid": "", + "comments": { + "translator": "SOME DESCRIPTIVE TITLE.\nCopyright (C) YEAR SuSE Linux Products GmbH, Nuernberg\nThis file is distributed under the same license as the PACKAGE package.\nFIRST AUTHOR , YEAR.\n", + "flag": "fuzzy" + } + } + } + } +} diff --git a/.github/workflows/weblate-update-pot.yml b/.github/workflows/weblate-update-pot.yml index a833ce1284..08bc1f6123 100644 --- a/.github/workflows/weblate-update-pot.yml +++ b/.github/workflows/weblate-update-pot.yml @@ -24,20 +24,20 @@ jobs: run: zypper modifyrepo -d repo-non-oss repo-openh264 repo-update && zypper ref - name: Install tools - run: zypper --non-interactive install --no-recommends diffutils git gettext-tools + run: zypper --non-interactive install --no-recommends diffutils git gettext-tools npm-default - name: Checkout Agama sources uses: actions/checkout@v3 with: path: agama - - name: Generate POT file + - name: Generate web POT file run: | cd agama/web ./build_pot msgfmt --statistics agama.pot - - name: Validate the generated POT file + - name: Validate the generated web POT file run: msgfmt --check-format agama/web/agama.pot - name: Checkout Weblate sources @@ -52,13 +52,13 @@ jobs: git config --global user.name "YaST Bot" git config --global user.email "yast-devel@opensuse.org" - - name: Update POT file + - name: Update web POT file run: | mkdir -p agama-weblate/web cp agama/web/agama.pot agama-weblate/web/agama.pot # any change besides the timestamp in the POT file? - - name: Check changes + - name: Check web POT changes id: check_changes run: | git -C agama-weblate diff --ignore-matching-lines="POT-Creation-Date:" web/agama.pot > pot.diff @@ -71,11 +71,53 @@ jobs: echo "pot_updated=false" >> $GITHUB_OUTPUT fi - - name: Push updated POT file + - name: Push updated web POT file # run only when the POT file has been updated if: steps.check_changes.outputs.pot_updated == 'true' run: | cd agama-weblate git add web/agama.pot - git commit -m "Update POT file"$'\n\n'"Agama commit: $GITHUB_SHA" + git commit -m "Update web POT file"$'\n\n'"Agama commit: $GITHUB_SHA" + git push + + - name: Install NPM packages + run: | + cd agama/.github/workflows/product_translations + npm ci + + - name: Generate products POT file + run: | + cd agama/products.d + ../.github/workflows/product_translations/products_pot *.yaml > products.pot + msgfmt --statistics products.pot + + - name: Validate the generated products POT file + run: msgfmt --check-format agama/products.d/products.pot + + - name: Update products POT file + run: | + mkdir -p agama-weblate/products + cp agama/products.d/products.pot agama-weblate/products/products.pot + + # any change besides the timestamp in the POT file? + - name: Check products POT changes + id: check_changes_products + run: | + git -C agama-weblate diff --ignore-matching-lines="POT-Creation-Date:" products/products.pot > pot.diff + + if [ -s pot.diff ]; then + echo "POT file updated" + echo "pot_updated=true" >> $GITHUB_OUTPUT + else + echo "POT file unchanged" + echo "pot_updated=false" >> $GITHUB_OUTPUT + fi + + - name: Push updated products POT file + # run only when the POT file has been updated + if: steps.check_changes_products.outputs.pot_updated == 'true' + run: | + cd agama-weblate + git add products/products.pot + git commit -m "Update products POT file"$'\n\n'"Agama commit: $GITHUB_SHA" git push