From b1a6e3c6ff6bf28b4e16d1b3a90a4cb9bdda5bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Sun, 23 Jul 2023 09:57:56 +0200 Subject: [PATCH 1/3] GitHub Action: Create a pull request with updated translations Helper for merging the changes from the Weblate repository to the main repository. --- .github/workflows/weblate-merge-po.yml | 107 ++++++++++ web/po/de.po | 39 ---- web/po/html2po | 264 ------------------------- web/po/manifest2po | 193 ------------------ 4 files changed, 107 insertions(+), 496 deletions(-) create mode 100644 .github/workflows/weblate-merge-po.yml delete mode 100644 web/po/de.po delete mode 100755 web/po/html2po delete mode 100755 web/po/manifest2po diff --git a/.github/workflows/weblate-merge-po.yml b/.github/workflows/weblate-merge-po.yml new file mode 100644 index 0000000000..c5dae31136 --- /dev/null +++ b/.github/workflows/weblate-merge-po.yml @@ -0,0 +1,107 @@ +name: Weblate Merge PO + +on: + schedule: + # run every Monday at 2:42AM UTC + - cron: "42 2 * * 0" + + # allow running manually + workflow_dispatch: + +jobs: + merge-po: + # allow pushing and creating pull requests + permissions: + contents: write + pull-requests: write + + # do not run in forks + if: github.repository == 'openSUSE/agama' + + runs-on: ubuntu-latest + + container: + image: registry.opensuse.org/opensuse/tumbleweed:latest + + steps: + - name: Configure and refresh repositories + run: | + # install the GitHub command line tool "gh" + zypper addrepo https://cli.github.com/packages/rpm/gh-cli.repo + # disable unused repositories to have a faster refresh + zypper modifyrepo -d repo-non-oss repo-openh264 repo-update && \ + zypper --non-interactive --gpg-auto-import-keys ref + + - name: Install tools + run: zypper --non-interactive install --no-recommends gh git gettext-tools + + - name: Configure Git + run: | + git config --global user.name "YaST Bot" + git config --global user.email "yast-devel@opensuse.org" + + - name: Checkout sources + uses: actions/checkout@v3 + with: + path: agama + + - name: Checkout Agama-weblate sources + uses: actions/checkout@v3 + with: + path: agama-weblate + repository: openSUSE/agama-weblate + + - name: Update PO files + working-directory: ./agama + run: | + mkdir -p web/po + # delete the current translations + find web/po -name '*.po' -exec git rm '{}' ';' + + # copy the new ones + cp -a ../agama-weblate/web/*.po web/po + git add web/po/*.po + + - name: Validate the PO files + working-directory: ./agama + run: msgfmt --check-format -o /dev/null web/po/*.po + + # any changes besides the timestamps in the PO files? + - name: Check changes + id: check_changes + working-directory: ./agama + run: | + git diff --staged --ignore-matching-lines="POT-Creation-Date:" \ + --ignore-matching-lines="PO-Revision-Date:" web/po > po.diff + + if [ -s po.diff ]; then + echo "PO files updated" + echo "po_updated=true" >> $GITHUB_OUTPUT + else + echo "PO files unchanged" + echo "po_updated=false" >> $GITHUB_OUTPUT + fi + + rm po.diff + + - name: Push updated PO files + # run only when a PO file has been updated + if: steps.check_changes.outputs.po_updated == 'true' + working-directory: ./agama + run: | + # use a unique branch to avoid possible conflicts with already existing branches + git checkout -b "po_merge_${GITHUB_RUN_ID}" + git commit -a -m "Update PO files"$'\n\n'"Agama-weblate commit: `git -C ../agama-weblate rev-parse HEAD`" + git push origin "po_merge_${GITHUB_RUN_ID}" + + - name: Create pull request + # run only when a PO file has been updated + if: steps.check_changes.outputs.po_updated == 'true' + working-directory: ./agama + run: | + gh pr create -B master -H "po_merge_${GITHUB_RUN_ID}" \ + --label translations --label bot \ + --title "Update PO files" \ + --body "Updating the translation files from the agama-weblate repository" + env: + GH_TOKEN: ${{ github.token }} diff --git a/web/po/de.po b/web/po/de.po deleted file mode 100644 index 0394e917d0..0000000000 --- a/web/po/de.po +++ /dev/null @@ -1,39 +0,0 @@ -# starter-kit German translations -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: starter-kit 1.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-03-09 16:09+0100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: de\n" -"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/index.html:20 -msgid "Cockpit Starter Kit" -msgstr "Cockpit Bausatz" - -#: src/app.jsx:43 -msgid "Running on $0" -msgstr "Läuft auf $0" - -#: org.cockpit-project.starter-kit.metainfo.xml:6 -msgid "Scaffolding for a cockpit module" -msgstr "Gerüst für ein Cockpit-Modul" - -#: org.cockpit-project.starter-kit.metainfo.xml:8 -msgid "Scaffolding for a cockpit module." -msgstr "Gerüst für ein Cockpit-Modul." - -#: src/manifest.json:0 org.cockpit-project.starter-kit.metainfo.xml:5 -msgid "Starter Kit" -msgstr "Bausatz" - -#: src/app.jsx:29 -msgid "Unknown" -msgstr "Unbekannt" diff --git a/web/po/html2po b/web/po/html2po deleted file mode 100755 index 8b34fa05dd..0000000000 --- a/web/po/html2po +++ /dev/null @@ -1,264 +0,0 @@ -#!/usr/bin/env node - -/* - * Extracts translatable strings from HTML files in the following forms: - * - * String - * String - * String - * - * - * Supports the following Glade compatible forms: - * - * String - * String - * - * Supports the following angular-gettext compatible forms: - * - * String - * Singular - * - * Note that some of the use of the translated may not support all the strings - * depending on the code actually using these strings to translate the HTML. - */ - - -function fatal(message, code) { - console.log((filename || "html2po") + ": " + message); - process.exit(code || 1); -} - -function usage() { - console.log("usage: html2po input output"); - process.exit(2); -} - -var fs, htmlparser, path, stdio; - -try { - fs = require('fs'); - path = require('path'); - htmlparser = require('htmlparser'); - stdio = require('stdio'); -} catch (ex) { - fatal(ex.message, 127); /* missing looks for this */ -} - -var opts = stdio.getopt({ - directory: { key: "d", args: 1, description: "Base directory for input files", default: "." }, - output: { key: "o", args: 1, description: "Output file" }, - from: { key: "f", args: 1, description: "File containing list of input files", default: "" }, -}); - -if (!opts.from && opts.args.length < 1) { - usage(); -} - -var input = opts.args; -var entries = { }; - -/* Filename being parsed and offset of line number */ -var filename = null; -var offsets = 0; - -/* The HTML parser we're using */ -var handler = new htmlparser.DefaultHandler(function(error, dom) { - if (error) - fatal(error); - else - walk(dom); -}); - -prepare(); - -/* Decide what input files to process */ -function prepare() { - if (opts.from) { - fs.readFile(opts.from, { encoding: "utf-8"}, function(err, data) { - if (err) - fatal(err.message); - input = data.split("\n").filter(function(value) { - return !!value; - }).concat(input); - step(); - }); - } else { - step(); - } -} - -/* Now process each file in turn */ -function step() { - filename = input.shift(); - if (filename === undefined) { - finish(); - return; - } - - /* Qualify the filename if necessary */ - var full = filename; - if (opts.directory) - full = path.join(opts.directory, filename); - - fs.readFile(full, { encoding: "utf-8"}, function(err, data) { - if (err) - fatal(err.message); - - var parser = new htmlparser.Parser(handler, { includeLocation: true }); - parser.parseComplete(data); - step(); - }); -} - -/* Process an array of nodes */ -function walk(children) { - if (!children) - return; - - children.forEach(function(child) { - var line = (child.location || { }).line || 0; - var offset = line - 1; - - /* Scripts get their text processed as HTML */ - if (child.type == 'script' && child.children) { - var parser = new htmlparser.Parser(handler, { includeLocation: true }); - - /* Make note of how far into the outer HTML file we are */ - offsets += offset; - - child.children.forEach(function(node) { - parser.parseChunk(node.raw); - }); - parser.done(); - - offsets -= offset; - - /* Tags get extracted as usual */ - } else if (child.type == 'tag') { - tag(child); - } - }); -} - -/* Process a single loaded tag */ -function tag(node) { - - var tasks, line, entry; - var attrs = node.attribs || { }; - var nest = true; - - /* Extract translate strings */ - if ("translate" in attrs || "translatable" in attrs) { - tasks = (attrs["translate"] || attrs["translatable"] || "yes").split(" "); - - /* Calculate the line location taking into account nested parsing */ - line = (node.location || { })["line"] || 0; - line += offsets; - - entry = { - msgctxt: attrs['translate-context'] || attrs['context'], - msgid_plural: attrs['translate-plural'], - locations: [ filename + ":" + line ] - }; - - /* For each thing listed */ - tasks.forEach(function(task) { - var copy = Object.assign({}, entry); - - /* The element text itself */ - if (task == "yes" || task == "translate") { - copy.msgid = extract(node.children); - nest = false; - - /* An attribute */ - } else if (task) { - copy.msgid = attrs[task]; - } - - if (copy.msgid) - push(copy); - }); - } - - /* Walk through all the children */ - if (nest) - walk(node.children); -} - -/* Push an entry onto the list */ -function push(entry) { - var key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt; - var prev = entries[key]; - if (prev) { - prev.locations = prev.locations.concat(entry.locations); - } else { - entries[key] = entry; - } -} - -/* Extract the given text */ -function extract(children) { - if (!children) - return null; - - var i, len, node, str = []; - children.forEach(function(node) { - if (node.type == 'tag' && node.children) - str.push(extract(node.children)) - else if (node.type == 'text' && node.data) - str.push(node.data); - }); - - return str.join(""); -} - -/* Escape a string for inclusion in po file */ -function escape(string) { - var bs = string.split('\\').join('\\\\').split('"').join('\\"'); - return bs.split("\n").map(function(line) { - return '"' + line + '"'; - }).join("\n"); -} - -/* Finish by writing out the strings */ -function finish() { - var result = [ - 'msgid ""', - 'msgstr ""', - '"Project-Id-Version: PACKAGE_VERSION\\n"', - '"MIME-Version: 1.0\\n"', - '"Content-Type: text/plain; charset=UTF-8\\n"', - '"Content-Transfer-Encoding: 8bit\\n"', - '"X-Generator: Cockpit html2po\\n"', - '', - ]; - - var msgid, entry; - for (msgid in entries) { - entry = entries[msgid]; - result.push('#: ' + entry.locations.join(" ")); - if (entry.msgctxt) - result.push('msgctxt ' + escape(entry.msgctxt)); - result.push('msgid ' + escape(entry.msgid)); - if (entry.msgid_plural) { - result.push('msgid_plural ' + escape(entry.msgid_plural)); - result.push('msgstr[0] ""'); - result.push('msgstr[1] ""'); - } else { - result.push('msgstr ""'); - } - result.push(''); - } - - var data = result.join('\n'); - if (!opts.output) { - process.stdout.write(data); - process.exit(0); - } else { - fs.writeFile(opts.output, data, function(err) { - if (err) - fatal(err.message); - process.exit(0); - }); - } -} diff --git a/web/po/manifest2po b/web/po/manifest2po deleted file mode 100755 index 46fa744b51..0000000000 --- a/web/po/manifest2po +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env node - -/* - * Extracts translatable strings from manifest.json files. - * - */ - -function fatal(message, code) { - console.log((filename || "manifest2po") + ": " + message); - process.exit(code || 1); -} - -function usage() { - console.log("usage: manifest2po [-o output] input..."); - process.exit(2); -} - -var fs, path, stdio; - -try { - fs = require('fs'); - path = require('path'); - stdio = require('stdio'); -} catch (ex) { - fatal(ex.message, 127); /* missing looks for this */ -} - -var opts = stdio.getopt({ - directory: { key: "d", args: 1, description: "Base directory for input files", default: "." }, - output: { key: "o", args: 1, description: "Output file" }, - from: { key: "f", args: 1, description: "File containing list of input files", default: "" }, -}); - -if (!opts.from && opts.args.length < 1) { - usage(); -} - -var input = opts.args; -var entries = { }; - -/* Filename being parsed */ -var filename = null; - -prepare(); - -/* Decide what input files to process */ -function prepare() { - if (opts.from) { - fs.readFile(opts.from, { encoding: "utf-8"}, function(err, data) { - if (err) - fatal(err.message); - input = data.split("\n").filter(function(value) { - return !!value; - }).concat(input); - step(); - }); - } else { - step(); - } -} - -/* Now process each file in turn */ -function step() { - filename = input.shift(); - if (filename === undefined) { - finish(); - return; - } - - if (path.basename(filename) != "manifest.json") - return step(); - - /* Qualify the filename if necessary */ - var full = filename; - if (opts.directory) - full = path.join(opts.directory, filename); - - fs.readFile(full, { encoding: "utf-8"}, function(err, data) { - if (err) - fatal(err.message); - - // There are variables which when not substituted can cause JSON.parse to fail - // Dummy replace them. None variable is going to be translated anyway - safe_data = data.replace(/\@.+?\@/gi, 1); - process_manifest(JSON.parse(safe_data)); - - return step(); - }); -} - -function process_manifest(manifest) { - if (manifest.menu) - process_menu(manifest.menu); - if (manifest.tools) - process_menu(manifest.tools); -} - -function process_keywords(keywords) { - keywords.forEach(v => { - v.matches.forEach(keyword => - push({ - msgid: keyword, - locations: [ filename + ":0" ] - }) - ); - }); -} - -function process_docs(docs) { - docs.forEach(doc => { - push({ - msgid: doc.label, - locations: [ filename + ":0" ] - }) - }); -} - -function process_menu(menu) { - for (var m in menu) { - if (menu[m].label) { - push({ - msgid: menu[m].label, - locations: [ filename + ":0" ] - }); - } - if (menu[m].keywords) - process_keywords(menu[m].keywords); - if (menu[m].docs) - process_docs(menu[m].docs); - } -} - -/* Push an entry onto the list */ -function push(entry) { - var key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt; - var prev = entries[key]; - if (prev) { - prev.locations = prev.locations.concat(entry.locations); - } else { - entries[key] = entry; - } -} - -/* Escape a string for inclusion in po file */ -function escape(string) { - var bs = string.split('\\').join('\\\\').split('"').join('\\"'); - return bs.split("\n").map(function(line) { - return '"' + line + '"'; - }).join("\n"); -} - -/* Finish by writing out the strings */ -function finish() { - var result = [ - 'msgid ""', - 'msgstr ""', - '"Project-Id-Version: PACKAGE_VERSION\\n"', - '"MIME-Version: 1.0\\n"', - '"Content-Type: text/plain; charset=UTF-8\\n"', - '"Content-Transfer-Encoding: 8bit\\n"', - '"X-Generator: Cockpit manifest2po\\n"', - '', - ]; - - var msgid, entry; - for (msgid in entries) { - entry = entries[msgid]; - result.push('#: ' + entry.locations.join(" ")); - if (entry.msgctxt) - result.push('msgctxt ' + escape(entry.msgctxt)); - result.push('msgid ' + escape(entry.msgid)); - if (entry.msgid_plural) { - result.push('msgid_plural ' + escape(entry.msgid_plural)); - result.push('msgstr[0] ""'); - result.push('msgstr[1] ""'); - } else { - result.push('msgstr ""'); - } - result.push(''); - } - - var data = result.join('\n'); - if (!opts.output) { - process.stdout.write(data); - process.exit(0); - } else { - fs.writeFile(opts.output, data, function(err) { - if (err) - fatal(err.message); - process.exit(0); - }); - } -} From 7b4e2a0b398e6e54e077cbe4657234fa5a09c86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 27 Jul 2023 17:00:37 +0200 Subject: [PATCH 2/3] Update .github/workflows/weblate-merge-po.yml Co-authored-by: Martin Vidner --- .github/workflows/weblate-merge-po.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/weblate-merge-po.yml b/.github/workflows/weblate-merge-po.yml index c5dae31136..0b54d95ac8 100644 --- a/.github/workflows/weblate-merge-po.yml +++ b/.github/workflows/weblate-merge-po.yml @@ -76,6 +76,8 @@ jobs: if [ -s po.diff ]; then echo "PO files updated" + # this is an Output Parameter + # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter echo "po_updated=true" >> $GITHUB_OUTPUT else echo "PO files unchanged" From 680e2d6ab477493d5a192151f29542e981810fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 27 Jul 2023 17:13:08 +0200 Subject: [PATCH 3/3] README.md - added badges --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 36a4d84087..b12c27f19e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,15 @@ +**Checks** + [![CI Status](https://github.com/openSUSE/agama/actions/workflows/ci.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/openSUSE/agama/badge.svg?branch=master)](https://coveralls.io/github/openSUSE/agama?branch=master) [![GitHub Pages](https://github.com/openSUSE/agama/actions/workflows/github-pages.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/github-pages.yml) +**Translations** + +[![Weblate Update POT](https://github.com/openSUSE/agama/actions/workflows/weblate-update-pot.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/weblate-update-pot.yml) +[![Weblate Merge PO](https://github.com/openSUSE/agama/actions/workflows/weblate-merge-po.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/weblate-merge-po.yml) +[![Translation Status](https://l10n.opensuse.org/widgets/agama/-/agama-web/svg-badge.svg)](https://l10n.opensuse.org/engage/agama/) + **[OBS systemsmanagement:Agama:Staging](https://build.opensuse.org/project/show/systemsmanagement:Agama:Staging)** [![Submit agama-cli](https://github.com/openSUSE/agama/actions/workflows/obs-staging-rust.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/obs-staging-rust.yml)