diff --git a/.github/workflows/ci-integration-tests.yml b/.github/workflows/ci-integration-tests.yml index 2b604d1c73..f2534a6eb0 100644 --- a/.github/workflows/ci-integration-tests.yml +++ b/.github/workflows/ci-integration-tests.yml @@ -1,95 +1,18 @@ name: "CI - Integration Tests" on: - push: - paths: - # NOTE: GitHub Actions do not allow using YAML references, the same path - # list is used below for the pull request event. Keep both lists in sync!! - - # this file itself - - .github/workflows/ci-integration-tests.yml - - # the web frontend - - web/**.json - - web/**.html - - web/**.scss - - web/**.jsx? - # ignore unit tests, we do not run them here - - "!web/**.test.jsx?" - - web/Makefile - - # the service backend - - setup-service.sh - - service/lib/**.rb - - service/bin/agamactl - - service/Gemfile* - - service/*.gemspec - # D-Bus and systemd configs - - service/share/*.conf - - service/share/*.service - # Rust services - - rust/Cargo.lock - - rust/agama-dbus-server/** - - rust/agama-locale-data/** - - rust/agama-lib/** - # ignore the JSON profile and the examples - - "!rust/agama-lib/share/**" - - rust/share/*.service - - # the playwright tests and configs - - playwright/**.ts - - playwright/config/agama.yaml - - pull_request: - paths: - # NOTE: GitHub Actions do not allow using YAML references, the same path - # list is used above for the push event. Keep both lists in sync!! - - # this file itself - - .github/workflows/ci-integration-tests.yml - - # the web frontend - - web/**.json - - web/**.html - - web/**.scss - - web/**.jsx? - # ignore unit tests, we do not run them here - - "!web/**.test.jsx?" - - web/Makefile - - # the service backend - - setup-service.sh - - service/lib/**.rb - - service/bin/agamactl - - service/Gemfile* - - service/*.gemspec - # D-Bus and systemd configs - - service/share/*.conf - - service/share/*.service - # Rust services - - rust/Cargo.lock - - rust/agama-dbus-server/** - - rust/agama-locale-data/** - - rust/agama-lib/** - # ignore the JSON profile and the examples - - "!rust/agama-lib/share/**" - - rust/share/*.service - - # the playwright tests and configs - - playwright/**.ts - - playwright/config/agama.yaml + schedule: + # at 10:50 every day from Monday to Friday + - cron: "50 10 * * 1-5" + + # allow running manually + workflow_dispatch: jobs: integration-tests: timeout-minutes: 60 runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - # TW is needed because of the Cockpit packages - distro: [ "tumbleweed" ] - steps: # TODO: Reuse/share building the frontend and backend with the other steps @@ -154,7 +77,7 @@ jobs: - name: Upload the test results uses: actions/upload-artifact@v3 - # run even when the previous step fails + # run even when any previous step fails if: always() with: name: test-results @@ -162,3 +85,12 @@ jobs: path: | playwright/test-results/**/* /tmp/log/YaST2/y2log + + - name: IRC notification + # see https://github.com/marketplace/actions/irc-message-action + uses: Gottox/irc-message-action@v2 + if: failure() + with: + channel: "#yast" + nickname: github-action + message: "Agama integration test failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml new file mode 100644 index 0000000000..302df173b0 --- /dev/null +++ b/.github/workflows/ci-rust.yml @@ -0,0 +1,106 @@ +name: CI - Rust + +on: + push: + paths: + # NOTE: GitHub Actions do not allow using YAML references, the same path + # list is used below for the pull request event. Keep both lists in sync!! + + # this file as well + - .github/workflows/ci-rust.yml + # any change in the rust subfolder + - rust/** + # except Markdown documentation + - "!rust/**.md" + # except the packaging + - "!rust/package/**" + # except the DBus configuration + - "!rust/share/**" + + pull_request: + paths: + # NOTE: GitHub Actions do not allow using YAML references, the same path + # list is used above for the push event. Keep both lists in sync!! + + # this file as well + - .github/workflows/ci-rust.yml + # any change in the rust subfolder + - rust/** + # except Markdown documentation + - "!rust/**.md" + # except the packaging + - "!rust/package/**" + # except the DBus configuration + - "!rust/share/**" + + # allow running manually + workflow_dispatch: + +jobs: + rust_ci: + runs-on: ubuntu-latest + env: + COVERAGE: 1 + + defaults: + run: + working-directory: ./rust + + strategy: + fail-fast: false + matrix: + distro: [ "tumbleweed" ] + + container: + image: registry.opensuse.org/yast/head/containers_${{matrix.distro}}/yast-ruby + options: --security-opt seccomp=unconfined + + steps: + + - name: Git Checkout + uses: actions/checkout@v3 + + - name: Configure and refresh repositories + # disable unused repositories to have faster refresh + run: zypper modifyrepo -d repo-non-oss repo-openh264 repo-update && ( zypper ref || zypper ref || zypper ref ) + + - name: Install Rust development files + run: zypper --non-interactive install rustup + + - name: Install required packages + run: zypper --non-interactive install python-langtable-data openssl-3 libopenssl-3-devel jq + + - name: Install Rust toolchains + run: rustup toolchain install stable + + - name: Install cargo-binstall + uses: taiki-e/install-action@v2 + with: + tool: cargo-binstall + + - name: Install Tarpaulin (for code coverage) + run: cargo-binstall --no-confirm cargo-tarpaulin + + - name: Run the tests + run: cargo tarpaulin --out xml + + - name: Lint formatting + run: cargo fmt --all -- --check + + # send the code coverage for the Rust part to the coveralls.io + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v2 + with: + base-path: ./rust + format: cobertura + flag-name: rust + parallel: true + + # close the code coverage and inherit the previous coverage for the Ruby and + # Web parts (it needs a separate step, the "carryforward" flag can be used + # only with the "parallel-finished: true" option) + - name: Coveralls Finished + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + carryforward: "service,web" diff --git a/.github/workflows/ci-service.yml b/.github/workflows/ci-service.yml new file mode 100644 index 0000000000..620a8bb75c --- /dev/null +++ b/.github/workflows/ci-service.yml @@ -0,0 +1,93 @@ +name: CI - Service + +on: + push: + paths: + # NOTE: GitHub Actions do not allow using YAML references, the same path + # list is used below for the pull request event. Keep both lists in sync!! + + # this file as well + - .github/workflows/ci-service.yml + # any change in the service subfolder + - service/** + # except Markdown documentation + - "!service/**.md" + # except the packaging + - "!service/package/**" + # except the DBus configuration + - "!service/share/**" + + pull_request: + paths: + # NOTE: GitHub Actions do not allow using YAML references, the same path + # list is used above for the push event. Keep both lists in sync!! + + # this file as well + - .github/workflows/ci-service.yml + # any change in the service subfolder + - service/** + # except Markdown documentation + - "!service/**.md" + # except the packaging + - "!service/package/**" + # except the DBus configuration + - "!service/share/**" + + # allow running manually + workflow_dispatch: + +jobs: + ruby_tests: + runs-on: ubuntu-latest + env: + COVERAGE: 1 + + defaults: + run: + working-directory: ./service + + strategy: + fail-fast: false + matrix: + distro: [ "tumbleweed" ] + + container: + image: registry.opensuse.org/yast/head/containers_${{matrix.distro}}/yast-ruby + + steps: + + - name: Git Checkout + uses: actions/checkout@v3 + + - name: Configure and refresh repositories + # disable unused repositories to have faster refresh + run: zypper modifyrepo -d repo-non-oss repo-openh264 repo-update && zypper ref + + - name: Install Ruby development files + run: zypper --non-interactive install gcc gcc-c++ make openssl-devel ruby-devel npm augeas-devel + + - name: Install required packages + run: zypper --non-interactive install yast2-iscsi-client + + - name: Install RubyGems dependencies + run: bundle config set --local with 'development' && bundle install + + - name: Run the tests and generate coverage report + run: bundle exec rspec + + # send the code coverage for the Ruby part to the coveralls.io + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v2 + with: + base-path: ./service + flag-name: service + parallel: true + + # close the code coverage and inherit the previous coverage for the Web and + # Rust parts (it needs a separate step, the "carryforward" flag can be used + # only with the "parallel-finished: true" option) + - name: Coveralls Finished + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + carryforward: "rust,web" diff --git a/.github/workflows/ci-web.yml b/.github/workflows/ci-web.yml new file mode 100644 index 0000000000..f003997225 --- /dev/null +++ b/.github/workflows/ci-web.yml @@ -0,0 +1,94 @@ +name: CI - Web + +on: + push: + paths: + # NOTE: GitHub Actions do not allow using YAML references, the same path + # list is used below for the pull request event. Keep both lists in sync!! + + # this file as well + - .github/workflows/ci-web.yml + # any change in the web subfolder + - web/** + # except Markdown documentation + - "!web/**.md" + # except the packaging + - "!web/package/**" + + pull_request: + paths: + # NOTE: GitHub Actions do not allow using YAML references, the same path + # list is used above for the push event. Keep both lists in sync!! + + # this file as well + - .github/workflows/ci-web.yml + # any change in the web subfolder + - web/** + # except Markdown documentation + - "!web/**.md" + # except the packaging + - "!web/package/**" + + # allow running manually + workflow_dispatch: + +jobs: + frontend_build: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ./web + + strategy: + matrix: + node-version: ["18.x"] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: 'web/package-lock.json' + + - name: Install dependencies + run: npm install + + - name: Build the application + run: make + + - name: Run check spell + run: npm run cspell + + - name: Check types + run: npm run check-types + + - name: Run ESLint + run: npm run eslint + + - name: Run Stylelint + run: npm run stylelint + + - name: Run the tests and generate coverage report + run: npm test -- --coverage + + # send the code coverage for the web part to the coveralls.io + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v2 + with: + base-path: ./web + flag-name: web + parallel: true + + # close the code coverage and inherit the previous coverage for the Ruby and + # Rust parts (it needs a separate step, the "carryforward" flag can be used + # only with the "parallel-finished: true" option) + - name: Coveralls Finished + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + carryforward: "service,rust" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index d941c5d41d..0000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,169 +0,0 @@ -name: CI - -on: [push, pull_request] - -jobs: - frontend_build: - runs-on: ubuntu-latest - - defaults: - run: - working-directory: ./web - - strategy: - matrix: - node-version: ["18.x"] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - uses: actions/checkout@v3 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - cache-dependency-path: 'web/package-lock.json' - - - name: Install dependencies - run: npm install - - - name: Build the application - run: make - - - name: Run check spell - run: npm run cspell - - - name: Check types - run: npm run check-types - - - name: Run ESLint - run: npm run eslint - - - name: Run Stylelint - run: npm run stylelint - - - name: Run the tests and generate coverage report - run: npm test -- --coverage - - - name: Coveralls GitHub Action - uses: coverallsapp/github-action@v2 - with: - base-path: ./web - flag-name: frontend - parallel: true - - ruby_tests: - runs-on: ubuntu-latest - env: - COVERAGE: 1 - - defaults: - run: - working-directory: ./service - - strategy: - fail-fast: false - matrix: - distro: [ "tumbleweed" ] - - container: - image: registry.opensuse.org/yast/head/containers_${{matrix.distro}}/yast-ruby - - steps: - - - name: Git Checkout - uses: actions/checkout@v3 - - - name: Configure and refresh repositories - # disable unused repositories to have faster refresh - run: zypper modifyrepo -d repo-non-oss repo-openh264 repo-update && ( zypper ref || zypper ref || zypper ref ) - - - name: Install Ruby development files - run: zypper --non-interactive install gcc gcc-c++ make openssl-devel ruby-devel npm augeas-devel - - - name: Install required packages - run: zypper --non-interactive install yast2-iscsi-client - - - name: Install RubyGems dependencies - run: bundle config set --local with 'development' && bundle install - - - name: Run the tests and generate coverage report - run: bundle exec rspec - - - name: Coveralls GitHub Action - uses: coverallsapp/github-action@v2 - with: - base-path: ./service - flag-name: backend - parallel: true - - rust_ci: - runs-on: ubuntu-latest - env: - COVERAGE: 1 - - defaults: - run: - working-directory: ./rust - - strategy: - fail-fast: false - matrix: - distro: [ "tumbleweed" ] - - container: - image: registry.opensuse.org/yast/head/containers_${{matrix.distro}}/yast-ruby - options: --security-opt seccomp=unconfined - - steps: - - - name: Git Checkout - uses: actions/checkout@v3 - - - name: Configure and refresh repositories - # disable unused repositories to have faster refresh - run: zypper modifyrepo -d repo-non-oss repo-openh264 repo-update && ( zypper ref || zypper ref || zypper ref ) - - - name: Install Rust development files - run: zypper --non-interactive install rustup - - - name: Install required packages - run: zypper --non-interactive install python-langtable-data openssl-3 libopenssl-3-devel jq - - - name: Install Rust toolchains - run: rustup toolchain install stable - - - name: Install cargo-binstall - uses: taiki-e/install-action@v2 - with: - tool: cargo-binstall - - - name: Install Tarpaulin (for code coverage) - run: cargo-binstall --no-confirm cargo-tarpaulin - - - name: Run the tests - run: cargo tarpaulin --out xml - - - name: Lint formatting - run: cargo fmt --all -- --check - - - name: Coveralls GitHub Action - uses: coverallsapp/github-action@v2 - with: - base-path: ./rust - format: cobertura - flag-name: rust-backend - parallel: true - - finish: - runs-on: ubuntu-latest - - needs: [frontend_build, ruby_tests, rust_ci] - - steps: - - - name: Coveralls Finished - uses: coverallsapp/github-action@v1 - with: - parallel-finished: true 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/merge_po b/.github/workflows/product_translations/merge_po new file mode 100755 index 0000000000..ebfb079a37 --- /dev/null +++ b/.github/workflows/product_translations/merge_po @@ -0,0 +1,166 @@ +#! /usr/bin/node + +/* +This script merges the product translations back to the product YAML files. + +Requirements: NodeJS, run "npm ci" to install the needed NPM packages + +Usage: + + merge_po +*/ + +const fs = require("fs"); +const process = require("process"); +const path = require("path"); + +const { parseDocument } = require("yaml"); +const { globSync } = require("glob"); +const gettextParser = require("gettext-parser"); + +/** + * Translation object with original text, its translations and the locale name. + */ +class Translation { + msgid; + msgstr; + locale; + + /** + * Constructor + * @param {string} msgid - the original text + * @param {string} msgstr - the translation + * @param {string} locale - the locale + */ + constructor(msgid, msgstr, locale) { + this.msgid = msgid; + this.msgstr = msgstr; + this.locale = locale; + } + + /** + * Read all translations (PO files) in the specified directory + * @param {string} dir - input directory for reading the the PO files + * @returns {Translation[]} list of found translations + */ + static read(dir) { + const ret = []; + // sort the files so the translations in the YAML files are also sorted + const files = globSync(dir + "/*.po").sort(); + + files.forEach(f => { + console.log(`Reading ${path.basename(f)}`); + + const locale = path.basename(f, ".po"); + const input = fs.readFileSync(f, "utf-8"); + const po = gettextParser.po.parse(input); + + // translations for the default (empty) context + const translations = po.translations[""]; + + Object.values(translations).forEach(t => { + if (t.msgid !== "") { + ret.push(new Translation(t.msgid, t.msgstr[0], locale)); + } + }); + }); + + return ret; + } +} + +/** + * YAML product file + */ +class YamlProduct { + file; + document; + + /** + * Constructor + * @param {string} file - the path to the YAML file + */ + constructor(file) { + this.file = file; + // parse the file + const data = fs.readFileSync(this.file, "utf-8"); + this.document = parseDocument(data); + } + + /** + * Read all product definitions (YAML files) in the specified directory + * @param {string} dir - input directory for reading the the PO files + * @returns {YamlProduct[]} all found products + */ + static read(dir) { + const ret = []; + const files = globSync(dir + "/*.y{a,}ml"); + return files.map(f => { + console.log(`Reading ${path.basename(f)}`); + return new YamlProduct(f); + }); + } + + /** + * Find the matching translations in the list and add them to the YAML document + * @param {Translation[]} translations + */ + addTranslations(translations) { + const description = this.document.get("description"); + const newTranslations = {}; + + translations.forEach(t => { + if (t.msgid === description) { + newTranslations[t.locale] = t.msgstr; + } + }); + + // add or update the translations depending on whether they already exist + if (!this.document.has("translations")) { + this.document.add({key: "translations", value: { description: newTranslations}}); + } else { + const trans = this.document.get("translations"); + if (!trans) { + this.document.set("translations", { description: newTranslations}); + } else { + trans.set("description", newTranslations); + } + } + } + + /** + * Convert back the parsed YAML to String + * @returns {string} new update YAML data + */ + dump() { + return this.document.toString(); + } + + /** + * Save the current YAML data back to the file, the original content is + * overwritten. + */ + save() { + console.log(`Writing ${path.basename(this.file)}`); + fs.writeFileSync(this.file, this.dump(), "utf-8"); + } +} + +// script arguments (the first arg is executor path ("/usr/bin/node"), +// the second is name of this script) +const [, , translations_dir, products_dir] = process.argv; + +if (!translations_dir || !products_dir) { + console.log("ERROR: missing argument"); + process.exit(1); +} + +// read all YAML products and all translations +const translations = Translation.read(translations_dir); +const products = YamlProduct.read(products_dir); + +// inject the translations to the products and save the changes +products.forEach(p => { + p.addTranslations(translations); + p.save(); +}); diff --git a/.github/workflows/product_translations/package-lock.json b/.github/workflows/product_translations/package-lock.json new file mode 100644 index 0000000000..f43a58745c --- /dev/null +++ b/.github/workflows/product_translations/package-lock.json @@ -0,0 +1,693 @@ +{ + "name": "product_translations", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "gettext-parser": "^7.0.1", + "glob": "^10.3.10", + "yaml": "^2.3.4" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "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/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "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/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "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/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "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/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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..3a098cade0 --- /dev/null +++ b/.github/workflows/product_translations/package.json @@ -0,0 +1,7 @@ +{ + "devDependencies": { + "gettext-parser": "^7.0.1", + "glob": "^10.3.10", + "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..552e70edda --- /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() + 1) + "-" + + 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-merge-po.yml b/.github/workflows/weblate-merge-po.yml index 57532ae0c4..7d13aeae97 100644 --- a/.github/workflows/weblate-merge-po.yml +++ b/.github/workflows/weblate-merge-po.yml @@ -95,7 +95,7 @@ jobs: web/share/update-manifest.py web/src/manifest.json # 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 commit -a -m "Update web 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 @@ -105,7 +105,7 @@ jobs: 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" + --title "Update web PO files" \ + --body "Updating the web translation files from the agama-weblate repository" env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/weblate-merge-products-po.yml b/.github/workflows/weblate-merge-products-po.yml new file mode 100644 index 0000000000..4d722e3230 --- /dev/null +++ b/.github/workflows/weblate-merge-products-po.yml @@ -0,0 +1,106 @@ +name: Weblate Merge Product PO + +on: + schedule: + # run every Monday at 2:45AM UTC + - cron: "45 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 npm-default + + - 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: Validate the product PO files + working-directory: ./agama-weblate + run: ls products/*.po | xargs -n1 msgfmt --check-format -o /dev/null + + - name: Install NPM packages + working-directory: ./agama/.github/workflows/product_translations + run: npm ci + + - name: Update product files with translations + env: + NODE_PATH: ./agama/.github/workflows/product_translations + run: | + ${NODE_PATH}/merge_po agama-weblate/products agama/products.d + + - name: Check changes + id: check_changes + working-directory: ./agama + run: | + git diff products.d > po.diff + + if [ -s po.diff ]; then + echo "Product 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 "products_updated=true" >> $GITHUB_OUTPUT + else + echo "Product files unchanged" + echo "products_updated=false" >> $GITHUB_OUTPUT + fi + + rm po.diff + + - name: Push updated product files + # run only when a PO file has been updated + if: steps.check_changes.outputs.products_updated == 'true' + working-directory: ./agama + run: | + # use a unique branch to avoid possible conflicts with already existing branches + git checkout -b "products_po_merge_${GITHUB_RUN_ID}" + git commit -a -m "Update translations in the product files"$'\n\n'"Agama-weblate commit: `git -C ../agama-weblate rev-parse HEAD`" + git push origin "products_po_merge_${GITHUB_RUN_ID}" + + - name: Create pull request + # run only when a PO file has been updated + if: steps.check_changes.outputs.products_updated == 'true' + working-directory: ./agama + run: | + gh pr create -B master -H "products_po_merge_${GITHUB_RUN_ID}" \ + --label translations --label bot \ + --title "Update translations in the product files" \ + --body "Updating the product translations from the agama-weblate repository" + env: + GH_TOKEN: ${{ github.token }} 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 diff --git a/README.md b/README.md index 73ff70dbc2..556d5642b0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ **Checks** -[![CI Status](https://github.com/openSUSE/agama/actions/workflows/ci.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/ci.yml) +[![CI - Rust](https://github.com/openSUSE/agama/actions/workflows/ci-rust.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/ci-rust.yml) +[![CI - Service](https://github.com/openSUSE/agama/actions/workflows/ci-service.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/ci-service.yml) +[![CI - Web](https://github.com/openSUSE/agama/actions/workflows/ci-web.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/ci-web.yml) [![CI - Rubocop](https://github.com/openSUSE/agama/actions/workflows/ci-rubocop.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/ci-rubocop.yml) [![CI - Documentation Check](https://github.com/openSUSE/agama/actions/workflows/ci-doc-check.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/ci-doc-check.yml) [![CI - Integration Tests](https://github.com/openSUSE/agama/actions/workflows/ci-integration-tests.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/ci-integration-tests.yml) @@ -11,6 +13,7 @@ [![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) +[![Weblate Merge Product PO](https://github.com/openSUSE/agama/actions/workflows/weblate-merge-products-po.yml/badge.svg)](https://github.com/openSUSE/agama/actions/workflows/weblate-merge-products-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)** @@ -23,8 +26,9 @@ [![OBS Staging/agama-cli](https://img.shields.io/obs/systemsmanagement:Agama:Staging/agama-cli/openSUSE_Tumbleweed/x86_64?label=Package%20agama-cli)](https://build.opensuse.org/package/show/systemsmanagement:Agama:Staging/agama-cli) [![OBS Staging/cockpit-agama](https://img.shields.io/obs/systemsmanagement:Agama:Staging/cockpit-agama/openSUSE_Tumbleweed/x86_64?label=Package%20cockpit-agama)](https://build.opensuse.org/package/show/systemsmanagement:Agama:Staging/cockpit-agama) [![OBS Staging/rubygem-agama](https://img.shields.io/obs/systemsmanagement:Agama:Staging/rubygem-agama/openSUSE_Tumbleweed/x86_64?label=Package%20rubygem-agama)](https://build.opensuse.org/package/show/systemsmanagement:Agama:Staging/rubygem-agama) +[![OBS Staging/agama-products-opensuse](https://img.shields.io/obs/systemsmanagement%3AAgama%3AStaging/agama-products-opensuse/openSUSE_Tumbleweed/x86_64?label=Package%20agama-products-opensuse)](https://build.opensuse.org/package/show/systemsmanagement:Agama:Staging/agama-products-opensuse) [![OBS Staging/cockpit-agama-playwright](https://img.shields.io/obs/systemsmanagement:Agama:Staging/cockpit-agama-playwright/openSUSE_Tumbleweed/x86_64?label=Package%20cockpit-agama-playwright)](https://build.opensuse.org/package/show/systemsmanagement:Agama:Staging/cockpit-agama-playwright) -[![OBS Staging/agama-live](https://img.shields.io/obs/systemsmanagement:Agama:Staging/agama-live:default/images/x86_64?label=Live%20ISO)](https://build.opensuse.org/package/show/systemsmanagement:Agama:Staging/agama-live) +[![OBS Staging/agama-live](https://img.shields.io/obs/systemsmanagement:Agama:Staging/agama-live:openSUSE/images/x86_64?label=Live%20ISO)](https://build.opensuse.org/package/show/systemsmanagement:Agama:Staging/agama-live) **[OBS systemsmanagement:Agama:Devel](https://build.opensuse.org/project/show/systemsmanagement:Agama:Devel)** @@ -34,7 +38,7 @@ [![OBS Devel/agama-cli](https://img.shields.io/obs/systemsmanagement:Agama:Devel/agama-cli/openSUSE_Tumbleweed/x86_64?label=Package%20agama-cli)](https://build.opensuse.org/package/show/systemsmanagement:Agama:Devel/agama-cli) [![OBS Devel/cockpit-agama](https://img.shields.io/obs/systemsmanagement:Agama:Devel/cockpit-agama/openSUSE_Tumbleweed/x86_64?label=Package%20cockpit-agama)](https://build.opensuse.org/package/show/systemsmanagement:Agama:Devel/cockpit-agama) [![OBS Devel/rubygem-agama](https://img.shields.io/obs/systemsmanagement:Agama:Devel/rubygem-agama/openSUSE_Tumbleweed/x86_64?label=Package%20rubygem-agama)](https://build.opensuse.org/package/show/systemsmanagement:Agama:Devel/rubygem-agama) -[![OBS Devel/agama-live](https://img.shields.io/obs/systemsmanagement:Agama:Devel/agama-live:default/images/x86_64?label=Live%20ISO)](https://build.opensuse.org/package/show/systemsmanagement:Agama:Devel/agama-live) +[![OBS Devel/agama-live](https://img.shields.io/obs/systemsmanagement:Agama:Devel/agama-live:openSUSE/images/x86_64?label=Live%20ISO)](https://build.opensuse.org/package/show/systemsmanagement:Agama:Devel/agama-live) # Agama: A Service-based Linux Installer diff --git a/products.d/ALP-Dolomite.yaml b/products.d/ALP-Dolomite.yaml index 3e3d9a7463..345c7a2461 100644 --- a/products.d/ALP-Dolomite.yaml +++ b/products.d/ALP-Dolomite.yaml @@ -1,8 +1,19 @@ id: ALP-Dolomite name: SUSE ALP Dolomite +# ------------------------------------------------------------------------------ +# WARNING: When changing the product description delete the translations located +# at the at translations/description key below to avoid using obsolete +# translations!! +# ------------------------------------------------------------------------------ description: 'SUSE ALP Dolomite is a minimum immutable OS core, focused on security to provide the bare minimum to run workloads and services as containers or virtual machines.' +# Do not manually change any translations! See README.md for more details. +translations: + description: + cs: SUSE ALP Dolomite je minimální neměnitelný základní OS, zaměřený na + bezpečnost pro poskytování úplného minima ke spuštění úloh a služeb v + kontejnerech nebo virtuálních strojích. software: installation_repositories: - url: https://updates.suse.com/SUSE/Products/ALP-Dolomite/1.0/x86_64/product/ diff --git a/products.d/README.md b/products.d/README.md new file mode 100644 index 0000000000..fd6f8149d0 --- /dev/null +++ b/products.d/README.md @@ -0,0 +1,17 @@ +# Product Definitions + +This directory contains product definitions for the Agama installer. + +## Contribution + +For updating the translations use the [Agama Weblate +project](https://l10n.opensuse.org/projects/agama/agama-products/). The changes +in the Weblate are automatically saved to the +[agama-weblate](https://github.com/openSUSE/agama-weblate/products) repository +and later a pull request with the changes is automatically created for merging +the changes here. + +Alternatively you can open a pull request against the +[agama-weblate](https://github.com/openSUSE/agama-weblate/products) GitHub +repository. But that requires manual approving and merging, prefer using the +Weblate tool, it merges automatically. diff --git a/products.d/leap16.yaml b/products.d/leap16.yaml index 3ae4dc0a00..b7913cfb05 100644 --- a/products.d/leap16.yaml +++ b/products.d/leap16.yaml @@ -1,7 +1,18 @@ id: Leap16 name: openSUSE Leap 16.0 archs: x86_64,aarch64 -description: '[Experimental project] openSUSE Leap 16 is built on top of the next generation Adaptable Linux Platform (ALP) from SUSE.' +# ------------------------------------------------------------------------------ +# WARNING: When changing the product description delete the translations located +# at the at translations/description key below to avoid using obsolete +# translations!! +# ------------------------------------------------------------------------------ +description: '[Experimental project] openSUSE Leap 16 is built on top of the + next generation Adaptable Linux Platform (ALP) from SUSE.' +# Do not manually change any translations! See README.md for more details. +translations: + description: + cs: "[Experimentální projekt] openSUSE Leap 16 je postaven na budoucí generaci + Adaptable Linux Platform (ALP) od SUSE." software: installation_repositories: - url: https://download.opensuse.org/repositories/openSUSE:/Leap:/16.0/images/repo/Leap-16.0-x86_64-Media1/ diff --git a/products.d/tumbleweed.yaml b/products.d/tumbleweed.yaml index 732b836815..e5c72cd2cb 100644 --- a/products.d/tumbleweed.yaml +++ b/products.d/tumbleweed.yaml @@ -1,9 +1,20 @@ id: Tumbleweed name: openSUSE Tumbleweed -description: 'The Tumbleweed distribution is a pure rolling release version - of openSUSE containing the latest "stable" versions of all software - instead of relying on rigid periodic release cycles. The project does - this for users that want the newest stable software.' +# ------------------------------------------------------------------------------ +# WARNING: When changing the product description delete the translations located +# at the at translations/description key below to avoid using obsolete +# translations!! +# ------------------------------------------------------------------------------ +description: 'The Tumbleweed distribution is a pure rolling release version of + openSUSE containing the latest "stable" versions of all software instead of + relying on rigid periodic release cycles. The project does this for users that + want the newest stable software.' +# Do not manually change any translations! See README.md for more details. +translations: + description: + cs: Tumbleweed je rolující verze distribuce openSUSE obsahující poslední + "stabilní" verze veškerého software namísto pevných pravidelných vydání. + Projekt je určen pro uživatele, kteří chtějí nejnovější stabilní software. software: installation_repositories: - url: https://download.opensuse.org/tumbleweed/repo/oss/ diff --git a/service/lib/agama/config_reader.rb b/service/lib/agama/config_reader.rb index da43bc024b..e1dde8ded5 100644 --- a/service/lib/agama/config_reader.rb +++ b/service/lib/agama/config_reader.rb @@ -148,7 +148,7 @@ def config_paths paths.uniq! { |f| File.basename(f) } # Sort files lexicographic paths.sort_by! { |f| File.basename(f) } - paths.prepend(default_path) + paths.prepend(default_path) if File.exist?(default_path) paths end diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb index aad196e3cb..7217d90811 100644 --- a/service/lib/agama/dbus/software/manager.rb +++ b/service/lib/agama/dbus/software/manager.rb @@ -149,6 +149,31 @@ def register_callbacks backend.on_issues_change { issues_properties_changed } end + # find translated product description if available + # @param data [Hash] product configuration from the YAML file + # @return [String,nil] Translated product description (if available) + # or the untranslated description, nil if not found + def localized_description(data) + translations = data["translations"]&.[]("description") + lang = ENV["LANG"] || "" + + # no translations or language not set, return untranslated value + return data["description"] if !translations.is_a?(Hash) || lang.empty? + + # remove the character encoding if present + lang = lang.split(".").first + # full matching (language + country) + return translations[lang] if translations[lang] + + # remove the country part + lang = lang.split("_").first + # partial match (just the language) + return translations[lang] if translations[lang] + + # fallback to original untranslated description + data["description"] + end + USER_SELECTED_PATTERN = 0 AUTO_SELECTED_PATTERN = 1 def compute_patterns diff --git a/service/lib/agama/ui_locale.rb b/service/lib/agama/ui_locale.rb index cc67ba9475..01c1b10564 100644 --- a/service/lib/agama/ui_locale.rb +++ b/service/lib/agama/ui_locale.rb @@ -47,7 +47,7 @@ def initialize(locale_client, &block) def change_locale(locale) # TODO: check if we can use UTF-8 everywhere including strange arch consoles Yast::WFM.SetLanguage(locale, "UTF-8") - # explicitelly set ENV to get localization also from libraries like libstorage + # explicitly set ENV to get localization also from libraries like libstorage ENV["LANG"] = locale + ".UTF-8" log.info "set yast language to #{locale}" # explicit call to textdomain to force fast gettext change of language ASAP diff --git a/service/test/agama/dbus/software/manager_test.rb b/service/test/agama/dbus/software/manager_test.rb index 925218b9d5..ea94877819 100644 --- a/service/test/agama/dbus/software/manager_test.rb +++ b/service/test/agama/dbus/software/manager_test.rb @@ -135,4 +135,68 @@ expect(installed).to eq(true) end end + + describe "#available_base_products" do + # testing product with translations + products = { + "Tumbleweed" => { + "name" => "openSUSE Tumbleweed", + "description" => "Original description", + "translations" => { + "description" => { + "cs" => "Czech translation", + "es" => "Spanish translation" + } + } + } + } + + it "returns product ID and name" do + expect(backend).to receive(:products).and_return(products) + + product = subject.available_base_products.first + expect(product[0]).to eq("Tumbleweed") + expect(product[1]).to eq("openSUSE Tumbleweed") + end + + it "returns untranslated description when the language is not set" do + allow(ENV).to receive(:[]).with("LANG").and_return(nil) + expect(backend).to receive(:products).and_return(products) + + product = subject.available_base_products.first + expect(product[2]["description"]).to eq("Original description") + end + + it "returns Czech translation if locale is \"cs_CZ.UTF-8\"" do + allow(ENV).to receive(:[]).with("LANG").and_return("cs_CZ.UTF-8") + expect(backend).to receive(:products).and_return(products) + + product = subject.available_base_products.first + expect(product[2]["description"]).to eq("Czech translation") + end + + it "returns Czech translation if locale is \"cs\"" do + allow(ENV).to receive(:[]).with("LANG").and_return("cs") + expect(backend).to receive(:products).and_return(products) + + product = subject.available_base_products.first + expect(product[2]["description"]).to eq("Czech translation") + end + + it "return untranslated description when translation is not available" do + allow(ENV).to receive(:[]).with("LANG").and_return("cs_CZ.UTF-8") + + # testing product without translations + untranslated = { + "Tumbleweed" => { + "name" => "openSUSE Tumbleweed", + "description" => "Original description" + } + } + expect(backend).to receive(:products).and_return(untranslated) + + product = subject.available_base_products.first + expect(product[2]["description"]).to eq("Original description") + end + end end diff --git a/web/po/README.md b/web/po/README.md new file mode 100644 index 0000000000..7a77b16c90 --- /dev/null +++ b/web/po/README.md @@ -0,0 +1,22 @@ +# Translations + +This directory contains translation files for the web frontend part of the Agama +installer. See more details in the main [i18n](../../doc/i18n.md) documentation. + +## Contribution + +:warning: *WARNING: Do not manually change any files here! The files are +automatically overwritten by the files from the +[agama-weblate](https://github.com/openSUSE/agama-weblate/tree/master/web) Git +repository! Your changes would be lost on the next synchronization!* :warning: + +For updating the translations use the [Agama Weblate +project](https://l10n.opensuse.org/projects/agama/agama-web/). The changes +in the Weblate are automatically saved to the agama-weblate repository +and later a pull request with the changes is automatically created for merging +the changes here. + +Alternatively you can open a pull request against the +[agama-weblate](https://github.com/openSUSE/agama-weblate/web) GitHub +repository. But that requires manual approving and merging, prefer using the +Weblate tool, it merges automatically. diff --git a/web/po/cs.po b/web/po/cs.po index d4a7d6ac3d..18fa976e5f 100644 --- a/web/po/cs.po +++ b/web/po/cs.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-10-29 02:08+0000\n" +"POT-Creation-Date: 2023-11-05 02:14+0000\n" "PO-Revision-Date: 2023-07-26 15:03+0000\n" "Last-Translator: Ladislav Slezák \n" "Language-Team: Czech \n" "Language-Team: Spanish \n" "Language-Team: French \n" @@ -72,20 +72,20 @@ msgid "Close" msgstr "Fermer" #. TRANSLATORS: button label -#: src/components/core/DBusError.jsx:40 +#: src/components/core/DBusError.jsx:41 msgid "Reload" msgstr "Recharger" #. TRANSLATORS: page title -#: src/components/core/DBusError.jsx:48 +#: src/components/core/DBusError.jsx:49 msgid "D-Bus Error" msgstr "Erreur D-Bus" -#: src/components/core/DBusError.jsx:54 +#: src/components/core/DBusError.jsx:55 msgid "Cannot connect to D-Bus" msgstr "Connexion au D-Bus impossible" -#: src/components/core/DBusError.jsx:59 +#: src/components/core/DBusError.jsx:60 msgid "" "Could not connect to the D-Bus service. Please, check whether it is running." msgstr "" @@ -340,8 +340,8 @@ msgstr "Popover basique" msgid "language" msgstr "langue" -#. TRANSLATORS: page section #. TRANSLATORS: page header +#. TRANSLATORS: page section #: src/components/l10n/L10nPage.jsx:84 #: src/components/overview/L10nSection.jsx:82 msgid "Localization" @@ -351,11 +351,6 @@ msgstr "Localisation" msgid "Display Language" msgstr "Langue d'affichage" -#: src/components/layout/Icon.jsx:157 -#, c-format -msgid "Icon %s not found!" -msgstr "Icône %s non trouvée!" - #: src/components/layout/Loading.jsx:30 msgid "Loading installation environment, please wait." msgstr "Chargement de l'environnement d'installation, veuillez patienter." @@ -396,8 +391,8 @@ msgstr "Adresses" msgid "Addresses data list" msgstr "Liste des données d'adresses" -#. TRANSLATORS: table header #. TRANSLATORS: input field for the iSCSI initiator name +#. TRANSLATORS: table header #: src/components/network/ConnectionsTable.jsx:57 #: src/components/network/ConnectionsTable.jsx:86 #: src/components/storage/ZFCPPage.jsx:362 @@ -531,8 +526,8 @@ msgstr "Aucune connexion WiFi trouvée" msgid "Connect to a Wi-Fi network" msgstr "Se connecter à un réseau Wi-Fi" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/network/NetworkPage.jsx:170 #: src/components/overview/NetworkSection.jsx:102 msgid "Network" @@ -614,8 +609,8 @@ msgstr "Connecté" msgid "Disconnecting" msgstr "Déconnexion" -#. TRANSLATORS: Wifi network status #. TRANSLATORS: iSCSI connection status +#. TRANSLATORS: Wifi network status #: src/components/network/WifiNetworkListItem.jsx:50 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" @@ -661,8 +656,8 @@ msgstr "Lecture des dépôts de logiciels" msgid "Refresh the repositories" msgstr "Rafraîchir les dépôts" -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/overview/SoftwareSection.jsx:145 #: src/components/software/SoftwarePage.jsx:81 msgid "Software" @@ -685,8 +680,8 @@ msgstr "" msgid "Probing storage devices" msgstr "Sonder les périphériques de stockage" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/overview/StorageSection.jsx:182 #: src/components/storage/ProposalPage.jsx:218 msgid "Storage" @@ -772,8 +767,13 @@ msgstr "non sélectionné" msgid "%d errors" msgstr "%d erreurs" +#: src/components/software/PatternSelector.jsx:210 +msgid "Software summary and filter options" +msgstr "Synthèse logiciel et options de filtrage" + #. TRANSLATORS: search field placeholder text -#: src/components/software/PatternSelector.jsx:214 +#: src/components/software/PatternSelector.jsx:215 +#: src/components/software/PatternSelector.jsx:216 msgid "Search" msgstr "Rechercher" @@ -1765,3 +1765,7 @@ msgstr "Utilisateur" #: src/components/users/UsersPage.jsx:34 msgid "Root authentication" msgstr "Authentification root" + +#, c-format +#~ msgid "Icon %s not found!" +#~ msgstr "Icône %s non trouvée!" diff --git a/web/po/ja.po b/web/po/ja.po index e88948c8b7..26db6fb39b 100644 --- a/web/po/ja.po +++ b/web/po/ja.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-10-29 02:08+0000\n" -"PO-Revision-Date: 2023-10-23 01:15+0000\n" +"POT-Creation-Date: 2023-11-05 02:14+0000\n" +"PO-Revision-Date: 2023-11-01 05:15+0000\n" "Last-Translator: Yasuhiko Kamata \n" "Language-Team: Japanese \n" @@ -70,20 +70,20 @@ msgid "Close" msgstr "閉じる" #. TRANSLATORS: button label -#: src/components/core/DBusError.jsx:40 +#: src/components/core/DBusError.jsx:41 msgid "Reload" msgstr "再読み込み" #. TRANSLATORS: page title -#: src/components/core/DBusError.jsx:48 +#: src/components/core/DBusError.jsx:49 msgid "D-Bus Error" msgstr "D-Bus エラー" -#: src/components/core/DBusError.jsx:54 +#: src/components/core/DBusError.jsx:55 msgid "Cannot connect to D-Bus" msgstr "D-Bus に接続できません" -#: src/components/core/DBusError.jsx:59 +#: src/components/core/DBusError.jsx:60 msgid "" "Could not connect to the D-Bus service. Please, check whether it is running." msgstr "" @@ -335,8 +335,8 @@ msgstr "基本ポップオーバー" msgid "language" msgstr "言語" -#. TRANSLATORS: page section #. TRANSLATORS: page header +#. TRANSLATORS: page section #: src/components/l10n/L10nPage.jsx:84 #: src/components/overview/L10nSection.jsx:82 msgid "Localization" @@ -346,11 +346,6 @@ msgstr "ローカライゼーション" msgid "Display Language" msgstr "表示言語" -#: src/components/layout/Icon.jsx:157 -#, c-format -msgid "Icon %s not found!" -msgstr "アイコン %s が見つかりませんでした!" - #: src/components/layout/Loading.jsx:30 msgid "Loading installation environment, please wait." msgstr "インストール環境を読み込んでいます。しばらくお待ちください。" @@ -391,8 +386,8 @@ msgstr "アドレス" msgid "Addresses data list" msgstr "アドレスデータの一覧" -#. TRANSLATORS: table header #. TRANSLATORS: input field for the iSCSI initiator name +#. TRANSLATORS: table header #: src/components/network/ConnectionsTable.jsx:57 #: src/components/network/ConnectionsTable.jsx:86 #: src/components/storage/ZFCPPage.jsx:362 @@ -527,8 +522,8 @@ msgstr "WiFi 接続が見つかりませんでした" msgid "Connect to a Wi-Fi network" msgstr "Wi-Fi ネットワークへの接続" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/network/NetworkPage.jsx:170 #: src/components/overview/NetworkSection.jsx:102 msgid "Network" @@ -610,8 +605,8 @@ msgstr "接続済み" msgid "Disconnecting" msgstr "切断中" -#. TRANSLATORS: Wifi network status #. TRANSLATORS: iSCSI connection status +#. TRANSLATORS: Wifi network status #: src/components/network/WifiNetworkListItem.jsx:50 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" @@ -656,8 +651,8 @@ msgstr "ソフトウエアリポジトリを読み込んでいます" msgid "Refresh the repositories" msgstr "リポジトリの更新" -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/overview/SoftwareSection.jsx:145 #: src/components/software/SoftwarePage.jsx:81 msgid "Software" @@ -679,8 +674,8 @@ msgstr "デバイス %s の内容を全て削除してインストールしま msgid "Probing storage devices" msgstr "ストレージデバイスを検出しています" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/overview/StorageSection.jsx:182 #: src/components/storage/ProposalPage.jsx:218 msgid "Storage" @@ -764,8 +759,13 @@ msgstr "未選択" msgid "%d errors" msgstr "%d 個のエラー" +#: src/components/software/PatternSelector.jsx:210 +msgid "Software summary and filter options" +msgstr "ソフトウエアの概要とフィルタのオプション" + #. TRANSLATORS: search field placeholder text -#: src/components/software/PatternSelector.jsx:214 +#: src/components/software/PatternSelector.jsx:215 +#: src/components/software/PatternSelector.jsx:216 msgid "Search" msgstr "検索" @@ -1750,6 +1750,10 @@ msgstr "ユーザ" msgid "Root authentication" msgstr "root の認証" +#, c-format +#~ msgid "Icon %s not found!" +#~ msgstr "アイコン %s が見つかりませんでした!" + #~ msgid "" #~ "Select the device for installing the system. All the file systems will be " #~ "created on the selected device." diff --git a/web/po/mk.po b/web/po/mk.po index 69891b3d01..80184f36df 100644 --- a/web/po/mk.po +++ b/web/po/mk.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-10-29 02:08+0000\n" +"POT-Creation-Date: 2023-11-05 02:14+0000\n" "PO-Revision-Date: 2023-10-21 23:15+0000\n" "Last-Translator: Kristijan Fremen Velkovski \n" "Language-Team: Macedonian \n" "Language-Team: Dutch \n" "Language-Team: Portuguese (Brazil) \n" "Language-Team: Russian , YEAR. +# Luna Jernberg , 2023. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-10-29 02:08+0000\n" -"PO-Revision-Date: 2023-10-18 18:15+0000\n" +"POT-Creation-Date: 2023-11-06 12:00+0000\n" +"PO-Revision-Date: 2023-11-06 12:01+0000\n" "Last-Translator: Luna Jernberg \n" "Language-Team: Swedish \n" @@ -71,20 +71,20 @@ msgid "Close" msgstr "Stäng" #. TRANSLATORS: button label -#: src/components/core/DBusError.jsx:40 +#: src/components/core/DBusError.jsx:41 msgid "Reload" msgstr "Ladda om" #. TRANSLATORS: page title -#: src/components/core/DBusError.jsx:48 +#: src/components/core/DBusError.jsx:49 msgid "D-Bus Error" msgstr "D-Bus fel" -#: src/components/core/DBusError.jsx:54 +#: src/components/core/DBusError.jsx:55 msgid "Cannot connect to D-Bus" msgstr "Kan inte ansluta till D-Bus" -#: src/components/core/DBusError.jsx:59 +#: src/components/core/DBusError.jsx:60 msgid "" "Could not connect to the D-Bus service. Please, check whether it is running." msgstr "" @@ -336,23 +336,17 @@ msgstr "Enkel popover" msgid "language" msgstr "språk" -#. TRANSLATORS: page section #. TRANSLATORS: page header +#. TRANSLATORS: page section #: src/components/l10n/L10nPage.jsx:84 #: src/components/overview/L10nSection.jsx:82 msgid "Localization" msgstr "Lokalisering" #: src/components/l10n/LanguageSwitcher.jsx:46 -#, fuzzy #| msgid "language" msgid "Display Language" -msgstr "språk" - -#: src/components/layout/Icon.jsx:157 -#, c-format -msgid "Icon %s not found!" -msgstr "Ikon %s hittades inte!" +msgstr "Visningsspråk" #: src/components/layout/Loading.jsx:30 msgid "Loading installation environment, please wait." @@ -394,8 +388,8 @@ msgstr "Adresser" msgid "Addresses data list" msgstr "Adresser data lista" -#. TRANSLATORS: table header #. TRANSLATORS: input field for the iSCSI initiator name +#. TRANSLATORS: table header #: src/components/network/ConnectionsTable.jsx:57 #: src/components/network/ConnectionsTable.jsx:86 #: src/components/storage/ZFCPPage.jsx:362 @@ -529,8 +523,8 @@ msgstr "Hittade inga WiFi-anslutningar" msgid "Connect to a Wi-Fi network" msgstr "Anslut till ett Wi-Fi nätverk" -#. TRANSLATORS: page title #. TRANSLATORS: page section title +#. TRANSLATORS: page title #: src/components/network/NetworkPage.jsx:170 #: src/components/overview/NetworkSection.jsx:102 msgid "Network" @@ -612,8 +606,8 @@ msgstr "Ansluten" msgid "Disconnecting" msgstr "Kopplar från" -#. TRANSLATORS: Wifi network status #. TRANSLATORS: iSCSI connection status +#. TRANSLATORS: Wifi network status #: src/components/network/WifiNetworkListItem.jsx:50 #: src/components/storage/iscsi/NodesPresenter.jsx:63 msgid "Disconnected" @@ -659,8 +653,8 @@ msgstr "Läser programvaruförråd" msgid "Refresh the repositories" msgstr "Uppdatera förråden" -#. TRANSLATORS: page title #. TRANSLATORS: page section +#. TRANSLATORS: page title #: src/components/overview/SoftwareSection.jsx:145 #: src/components/software/SoftwarePage.jsx:81 msgid "Software" @@ -682,8 +676,8 @@ msgstr "Installerar på enhet %s och raderar allt innehåll" msgid "Probing storage devices" msgstr "Undersöker lagringsenheter" -#. TRANSLATORS: page section title #. TRANSLATORS: page title +#. TRANSLATORS: page section title #: src/components/overview/StorageSection.jsx:182 #: src/components/storage/ProposalPage.jsx:218 msgid "Storage" @@ -769,8 +763,13 @@ msgstr "inte vald" msgid "%d errors" msgstr "%d fel" +#: src/components/software/PatternSelector.jsx:210 +msgid "Software summary and filter options" +msgstr "" + #. TRANSLATORS: search field placeholder text -#: src/components/software/PatternSelector.jsx:214 +#: src/components/software/PatternSelector.jsx:215 +#: src/components/software/PatternSelector.jsx:216 msgid "Search" msgstr "Sök" @@ -1758,6 +1757,10 @@ msgstr "Användare" msgid "Root authentication" msgstr "Rootautentisering" +#, c-format +#~ msgid "Icon %s not found!" +#~ msgstr "Ikon %s hittades inte!" + #~ msgid "" #~ "Select the device for installing the system. All the file systems will be " #~ "created on the selected device." diff --git a/web/po/uk.po b/web/po/uk.po index 562f244766..6c0254a32e 100644 --- a/web/po/uk.po +++ b/web/po/uk.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-10-29 02:08+0000\n" +"POT-Creation-Date: 2023-11-05 02:14+0000\n" "PO-Revision-Date: 2023-08-06 21:15+0000\n" "Last-Translator: Milachew \n" "Language-Team: Ukrainian