diff --git a/.gitignore b/.gitignore index 7c800d8dfd..1051ddacee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .log/ .yardoc/ +.vscode/tasks.json diff --git a/.yupdate.post b/.yupdate.post index 2d91b28549..c3560a7012 100755 --- a/.yupdate.post +++ b/.yupdate.post @@ -25,8 +25,8 @@ function restart_service() { if [ -n "$SERVICE_START" ]; then SERVICE_START_UNIX_TIME=$(date -d "$SERVICE_START" +"%s") - # find the date of the latest file in the gem - NEWEST_FILE_TIME=$(find /usr/lib*/ruby/gems/*/gems/$SERVICE_NAME-* -exec stat --format %Y "{}" \; | sort -nr | head -n 1) + # find the date of the latest file in the product configuration or in the gem + NEWEST_FILE_TIME=$(find /usr/share/agama/products.d /usr/lib*/ruby/gems/*/gems/$SERVICE_NAME-* -exec stat --format %Y "{}" \; | sort -nr | head -n 1) # when a file is newer than the start time then restart the service if [ -n "$NEWEST_FILE_TIME" ] && [ "$SERVICE_START_UNIX_TIME" -lt "$NEWEST_FILE_TIME" ]; then diff --git a/Rakefile b/Rakefile index 37ec973652..095acb3b1c 100644 --- a/Rakefile +++ b/Rakefile @@ -170,5 +170,21 @@ if ENV["YUPDATE_FORCE"] == "1" || File.exist?("/.packages.initrd") || live_iso? FileUtils.mkdir_p(File.join(destdir, "/usr/share")) FileUtils.cp_r("playwright/.", File.join(destdir, "/usr/share/agama-playwright")) end + + if ENV["YUPDATE_SKIP_PRODUCTS"] != "1" + files = Dir.glob("products.d/*.y{a}ml") + files.each do |f| + # the sources contain several products, update only the existing files + oldfile = File.join("/usr/share/agama/", f) + if File.exist?(oldfile) + target = File.join(destdir, "/usr/share/agama/", f) + FileUtils.mkdir_p(File.dirname(target)) + FileUtils.cp(f, target) + else + # if there is a new product file it needs to be copied manually + puts "Skipping product file: #{f}" + end + end + end end end diff --git a/doc/dbus/bus/org.opensuse.Agama1.Locale.bus.xml b/doc/dbus/bus/org.opensuse.Agama1.Locale.bus.xml index 38416bd6bc..8f4dec9054 100644 --- a/doc/dbus/bus/org.opensuse.Agama1.Locale.bus.xml +++ b/doc/dbus/bus/org.opensuse.Agama1.Locale.bus.xml @@ -38,39 +38,47 @@ - - + + - - - - - + + + - - + + - - diff --git a/doc/dbus/org.opensuse.Agama1.Locale.doc.xml b/doc/dbus/org.opensuse.Agama1.Locale.doc.xml index 8844528be1..3f0256a75e 100644 --- a/doc/dbus/org.opensuse.Agama1.Locale.doc.xml +++ b/doc/dbus/org.opensuse.Agama1.Locale.doc.xml @@ -1,47 +1,48 @@ - - + - - - + + - - - - - + + + - - + + - - - - diff --git a/doc/yaml_config.md b/doc/yaml_config.md index a98d48c929..c2a8e0de9a 100644 --- a/doc/yaml_config.md +++ b/doc/yaml_config.md @@ -38,6 +38,15 @@ Array of patterns that have to be selected. Array of patterns that should be selected but can be deselected or skipped if not available. +#### user\_patterns + +Array of patterns that are displayed in the pattern selector UI and user can +select them to install. + +If the list is empty then the pattern selector is not displayed. If the key is +not defined or the value is missing or is `null` then all available user visible +patterns are displayed. + ### security Options related to security diff --git a/products.d/ALP-Dolomite.yaml b/products.d/ALP-Dolomite.yaml index c2d4e7d039..d1cbea6f61 100644 --- a/products.d/ALP-Dolomite.yaml +++ b/products.d/ALP-Dolomite.yaml @@ -14,12 +14,17 @@ translations: 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. + sv: SUSE ALP Dolomite är en minimal oföränderlig OS-kärna, fokuserad på säkerhet + för att tillhandahålla det absoluta minimum för att köra + arbetsbelastningar och tjänster som behållare eller virtuella maskiner. software: mandatory_patterns: - alp_base_zypper - alp_cockpit - alp_hardware optional_patterns: null # no optional pattern shared + # no user selectable patterns, do not display the pattern selector + user_patterns: [] mandatory_packages: - package: ppc64-diag # Needed for hardware-based installations archs: ppc64 diff --git a/products.d/agama-products-opensuse.changes b/products.d/agama-products-opensuse.changes index f84460af9e..eb6cd2d50f 100644 --- a/products.d/agama-products-opensuse.changes +++ b/products.d/agama-products-opensuse.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Mon Dec 4 14:11:51 UTC 2023 - Ancor Gonzalez Sosa + +- Preliminary definitions of openSUSE MicroOS products +- Remove Leap 16.0 for now + ------------------------------------------------------------------- Mon Oct 30 14:38:51 UTC 2023 - Josef Reidinger diff --git a/products.d/agama-products-opensuse.spec b/products.d/agama-products-opensuse.spec index d2c23829f9..815663bc4a 100644 --- a/products.d/agama-products-opensuse.spec +++ b/products.d/agama-products-opensuse.spec @@ -52,8 +52,9 @@ install -m 0644 *.yaml %{buildroot}%{_datadir}/agama/products.d %files %dir %{_datadir}/agama %dir %{_datadir}/agama/products.d +%{_datadir}/agama/products.d/microos.yaml +%{_datadir}/agama/products.d/microos-desktop.yaml %{_datadir}/agama/products.d/tumbleweed.yaml -%{_datadir}/agama/products.d/leap16.yaml %files -n agama-products-ALP-Dolomite %dir %{_datadir}/agama diff --git a/products.d/leap16.yaml b/products.d/leap16.yaml deleted file mode 100644 index b7913cfb05..0000000000 --- a/products.d/leap16.yaml +++ /dev/null @@ -1,98 +0,0 @@ -id: Leap16 -name: openSUSE Leap 16.0 -archs: x86_64,aarch64 -# ------------------------------------------------------------------------------ -# 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/ - archs: x86_64 - - url: https://download.opensuse.org/repositories/openSUSE:/Leap:/16.0/images/repo/Leap-16.0-aarch64-Media1/ - archs: aarch64 - mandatory_patterns: - - alp_base - - alp_base_zypper - - alp_cockpit - - alp-container_runtime - - alp_defaults - optional_patterns: null # no optional pattern shared - mandatory_packages: null - optional_packages: null - base_product: Leap16 - -security: - lsm: selinux - available_lsms: - # apparmor: - # patterns: - # - apparmor - selinux: - patterns: - - alp_selinux - policy: enforcing - none: - patterns: null - -storage: - space_policy: delete - encryption: - method: luks2 - pbkd_function: pbkdf2 - tpm_luks_open: true - volumes: - - "/" - volume_templates: - - mount_path: "/" - filesystem: btrfs - btrfs: - snapshots: true - read_only: true - default_subvolume: "@" - subvolumes: - - path: root - - path: home - - path: opt - - path: srv - - path: boot/writable - - path: usr/local - - path: boot/grub2/arm64-efi - archs: aarch64 - - path: boot/grub2/i386-pc - archs: x86_64 - - path: boot/grub2/powerpc-ieee1275 - archs: ppc,!board_powernv - - path: boot/grub2/s390x-emu - archs: s390 - - path: boot/grub2/x86_64-efi - archs: x86_64 - - path: var - copy_on_write: false - size: - auto: false - min: 5 GiB - outline: - required: true - filesystems: - - btrfs - snapshots_configurable: false - - filesystem: xfs - size: - auto: false - outline: - required: false - filesystems: - - btrfs - - ext2 - - ext3 - - ext4 - - xfs diff --git a/products.d/microos-desktop.yaml b/products.d/microos-desktop.yaml new file mode 100644 index 0000000000..8a850ec72d --- /dev/null +++ b/products.d/microos-desktop.yaml @@ -0,0 +1,110 @@ +id: MicroOS-Desktop +name: openSUSE MicroOS Desktop +# ------------------------------------------------------------------------------ +# WARNING: When changing the product description delete the translations located +# at the at translations/description key below to avoid using obsolete +# translations!! +# ------------------------------------------------------------------------------ +description: 'A distribution for the desktop offering automatic updates and + rollback on top of the foundations of openSUSE MicroOS. Includes Podman + Container Runtime and allows to manage software using Gnome Software or + KDE Discover.' +# Do not manually change any translations! See README.md for more details. +translations: +software: + installation_repositories: + - url: https://download.opensuse.org/tumbleweed/repo/oss/ + archs: x86_64 + - url: https://download.opensuse.org/ports/aarch64/tumbleweed/repo/oss/ + archs: aarch64 + - url: https://download.opensuse.org/ports/zsystems/tumbleweed/repo/oss/ + archs: s390 + - url: https://download.opensuse.org/ports/ppc/tumbleweed/repo/oss/ + archs: ppc + - url: https://download.opensuse.org/tumbleweed/repo/non-oss/ + archs: x86_64 + # aarch64 does not have non-oss ports. Keep eye if it change + - url: https://download.opensuse.org/ports/zsystems/tumbleweed/repo/non-oss/ + archs: s390 + - url: https://download.opensuse.org/ports/ppc/tumbleweed/repo/non-oss/ + archs: ppc + - url: https://download.opensuse.org/update/tumbleweed/ + archs: x86_64 + - url: https://download.opensuse.org/ports/aarch64/update/tumbleweed/ + archs: aarch64 + - url: https://download.opensuse.org/ports/zsystems/update/tumbleweed/ + archs: s390 + - url: https://download.opensuse.org/ports/ppc/tumbleweed/repo/oss/ + archs: ppc + mandatory_patterns: + - microos_base + - microos_base_zypper + - microos_defaults + - microos_hardware + - container_runtime + - pattern: 32bit + archs: x86_64 + optional_patterns: + - microos_gnome_desktop + user_patterns: + - microos_gnome_desktop + - microos_kde_desktop + mandatory_packages: + - NetworkManager + optional_packages: null + base_product: MicroOS + +security: + lsm: selinux + available_lsms: + selinux: + patterns: + - microos_selinux + policy: enforcing + none: + patterns: null + +storage: + space_policy: delete + volumes: + - "/" + volume_templates: + - mount_path: "/" + filesystem: btrfs + btrfs: + snapshots: true + read_only: true + default_subvolume: "@" + subvolumes: + - path: home + - path: opt + - path: root + - path: srv + - path: usr/local + - path: boot/writable + # Unified var subvolume - https://lists.opensuse.org/opensuse-packaging/2017-11/msg00017.html + - path: var + copy_on_write: false + # Architecture specific subvolumes + - path: boot/grub2/arm64-efi + archs: aarch64 + - path: boot/grub2/arm-efi + archs: arm + - path: boot/grub2/i386-pc + archs: x86_64 + - path: boot/grub2/powerpc-ieee1275 + archs: ppc,!board_powernv + - path: boot/grub2/s390x-emu + archs: s390 + - path: boot/grub2/x86_64-efi + archs: x86_64 + - path: boot/grub2/riscv64-efi + archs: riscv64 + size: + auto: false + min: 5 GiB + outline: + required: true + snapshots_configurable: false + filesystems: + - btrfs diff --git a/products.d/microos.yaml b/products.d/microos.yaml new file mode 100644 index 0000000000..ec998f2eec --- /dev/null +++ b/products.d/microos.yaml @@ -0,0 +1,126 @@ +id: MicroOS +name: openSUSE MicroOS +# ------------------------------------------------------------------------------ +# WARNING: When changing the product description delete the translations located +# at the at translations/description key below to avoid using obsolete +# translations!! +# ------------------------------------------------------------------------------ +description: 'A quick, small distribution designed to host container workloads + with automated administration & patching. openSUSE MicroOS provides + transactional (atomic) updates upon a read-only btrfs root file system. + As rolling release distribution the software is always up-to-date.' +# Do not manually change any translations! See README.md for more details. +translations: +software: + installation_repositories: + - url: https://download.opensuse.org/tumbleweed/repo/oss/ + archs: x86_64 + - url: https://download.opensuse.org/ports/aarch64/tumbleweed/repo/oss/ + archs: aarch64 + - url: https://download.opensuse.org/ports/zsystems/tumbleweed/repo/oss/ + archs: s390 + - url: https://download.opensuse.org/ports/ppc/tumbleweed/repo/oss/ + archs: ppc + - url: https://download.opensuse.org/tumbleweed/repo/non-oss/ + archs: x86_64 + # aarch64 does not have non-oss ports. Keep eye if it change + - url: https://download.opensuse.org/ports/zsystems/tumbleweed/repo/non-oss/ + archs: s390 + - url: https://download.opensuse.org/ports/ppc/tumbleweed/repo/non-oss/ + archs: ppc + - url: https://download.opensuse.org/update/tumbleweed/ + archs: x86_64 + - url: https://download.opensuse.org/ports/aarch64/update/tumbleweed/ + archs: aarch64 + - url: https://download.opensuse.org/ports/zsystems/update/tumbleweed/ + archs: s390 + - url: https://download.opensuse.org/ports/ppc/tumbleweed/repo/oss/ + archs: ppc + mandatory_patterns: + - microos_base + - microos_base_zypper + - microos_defaults + - microos_hardware + - pattern: 32bit + archs: x86_64 + optional_patterns: null + user_patterns: + - container_runtime + - microos_ra_agent + - microos_ra_verifier + mandatory_packages: + - NetworkManager + optional_packages: null + base_product: MicroOS + +security: + lsm: selinux + available_lsms: + selinux: + patterns: + - microos_selinux + policy: enforcing + none: + patterns: null + +storage: + space_policy: delete + volumes: + - "/" + - "/var" + volume_templates: + - mount_path: "/" + filesystem: btrfs + btrfs: + snapshots: true + read_only: true + default_subvolume: "@" + subvolumes: + - path: home + - path: opt + - path: root + - path: srv + - path: usr/local + - path: boot/writable + # Unified var subvolume - https://lists.opensuse.org/opensuse-packaging/2017-11/msg00017.html + - path: var + copy_on_write: false + # Architecture specific subvolumes + - path: boot/grub2/arm64-efi + archs: aarch64 + - path: boot/grub2/arm-efi + archs: arm + - path: boot/grub2/i386-pc + archs: x86_64 + - path: boot/grub2/powerpc-ieee1275 + archs: ppc,!board_powernv + - path: boot/grub2/s390x-emu + archs: s390 + - path: boot/grub2/x86_64-efi + archs: x86_64 + - path: boot/grub2/riscv64-efi + archs: riscv64 + size: + auto: true + outline: + required: true + snapshots_configurable: false + filesystems: + - btrfs + auto_size: + base_min: 5 GiB + base_max: 25 GiB + max_fallback_for: + - "/var" + - mount_path: "/var" + filesystem: btrfs + mount_options: + - "x-initrd.mount" + - "nodatacow" + size: + auto: false + min: 5 GiB + outline: + required: false + filesystems: + - btrfs diff --git a/products.d/tumbleweed.yaml b/products.d/tumbleweed.yaml index e5c72cd2cb..fc8e934e5a 100644 --- a/products.d/tumbleweed.yaml +++ b/products.d/tumbleweed.yaml @@ -15,6 +15,10 @@ translations: 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. + sv: Tumbleweed-distributionen är en ren rullande version av openSUSE som + innehåller de senaste "stabila" versionerna av all programvara istället + för att förlita sig på stela periodiska utgivningscykler. Projektet gör + detta för användare som vill ha den senaste stabila programvaran. software: installation_repositories: - url: https://download.opensuse.org/tumbleweed/repo/oss/ @@ -43,6 +47,16 @@ software: mandatory_patterns: - enhanced_base # only pattern that is shared among all roles on TW optional_patterns: null # no optional pattern shared + user_patterns: + - basic_desktop + - xfce + - kde + - gnome + - yast2_basis + - yast2_desktop + - yast2_server + - multimedia + - office mandatory_packages: - NetworkManager optional_packages: null diff --git a/rust/Cargo.lock b/rust/Cargo.lock index bb72b0de76..cb533f2e9b 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -48,7 +48,11 @@ dependencies = [ "agama-locale-data", "anyhow", "cidr", + "gettext-rs", "log", + "macaddr", + "once_cell", + "regex", "serde", "serde_yaml", "simplelog", @@ -100,6 +104,7 @@ dependencies = [ "quick-xml", "regex", "serde", + "thiserror", ] [[package]] @@ -393,6 +398,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -893,6 +904,26 @@ dependencies = [ "wasi", ] +[[package]] +name = "gettext-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364" +dependencies = [ + "gettext-sys", + "locale_config", +] + +[[package]] +name = "gettext-sys" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d" +dependencies = [ + "cc", + "temp-dir", +] + [[package]] name = "gimli" version = "0.28.0" @@ -1081,6 +1112,19 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +[[package]] +name = "locale_config" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934" +dependencies = [ + "lazy_static", + "objc", + "objc-foundation", + "regex", + "winapi", +] + [[package]] name = "lock_api" version = "0.4.11" @@ -1100,6 +1144,21 @@ dependencies = [ "value-bag", ] +[[package]] +name = "macaddr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.6.4" @@ -1281,6 +1340,35 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.32.1" @@ -1831,6 +1919,12 @@ dependencies = [ "log", ] +[[package]] +name = "temp-dir" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab" + [[package]] name = "tempfile" version = "3.8.1" diff --git a/rust/agama-cli/src/logs.rs b/rust/agama-cli/src/logs.rs index 027da2c9a2..f6b6fcf404 100644 --- a/rust/agama-cli/src/logs.rs +++ b/rust/agama-cli/src/logs.rs @@ -112,7 +112,7 @@ const DEFAULT_PATHS: [&str; 14] = [ "/linuxrc.config", ]; -const DEFAULT_RESULT: &str = "/tmp/agama_logs"; +const DEFAULT_RESULT: &str = "/tmp/agama-logs"; // what compression is used by default: // (, ) const DEFAULT_COMPRESSION: (&str, &str) = ("bzip2", "tar.bz2"); @@ -398,7 +398,11 @@ fn store(options: LogOptions) -> Result<(), io::Error> { showln(verbose, "\t- proceeding output of commands"); // store it - showln(true, format!("Storing result in: \"{}\"", result).as_str()); + if verbose { + showln(true, format!("Storing result in: \"{}\"", result).as_str()); + } else { + showln(true, result.as_str()); + } for log in log_sources.iter() { show( diff --git a/rust/agama-dbus-server/Cargo.toml b/rust/agama-dbus-server/Cargo.toml index 1b28677bcf..e6e95c67a0 100644 --- a/rust/agama-dbus-server/Cargo.toml +++ b/rust/agama-dbus-server/Cargo.toml @@ -21,3 +21,7 @@ serde_yaml = "0.9.24" cidr = { version = "0.2.2", features = ["serde"] } tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } tokio-stream = "0.1.14" +gettext-rs = { version = "0.7.0", features = ["gettext-system"] } +regex = "1.10.2" +once_cell = "1.18.0" +macaddr = "1.0" diff --git a/rust/agama-dbus-server/src/error.rs b/rust/agama-dbus-server/src/error.rs index 81202acd14..926feab063 100644 --- a/rust/agama-dbus-server/src/error.rs +++ b/rust/agama-dbus-server/src/error.rs @@ -19,3 +19,9 @@ impl From for Error { Self::Anyhow(format!("{:#}", e)) } } + +impl From for zbus::fdo::Error { + fn from(value: Error) -> zbus::fdo::Error { + zbus::fdo::Error::Failed(format!("Localization error: {value}")) + } +} diff --git a/rust/agama-dbus-server/src/locale.rs b/rust/agama-dbus-server/src/locale.rs index 54e5bcb2cc..aabdfdc260 100644 --- a/rust/agama-dbus-server/src/locale.rs +++ b/rust/agama-dbus-server/src/locale.rs @@ -1,62 +1,52 @@ +pub mod helpers; +mod keyboard; +mod locale; +mod timezone; + use crate::error::Error; +use agama_locale_data::{KeymapId, LocaleCode}; use anyhow::Context; -use std::{fs::read_dir, process::Command}; +use keyboard::KeymapsDatabase; +use locale::LocalesDatabase; +use std::process::Command; +use timezone::TimezonesDatabase; use zbus::{dbus_interface, Connection}; pub struct Locale { + timezone: String, + timezones_db: TimezonesDatabase, locales: Vec, - keymap: String, - timezone_id: String, - supported_locales: Vec, - ui_locale: String, + locales_db: LocalesDatabase, + keymap: KeymapId, + keymaps_db: KeymapsDatabase, + ui_locale: LocaleCode, } #[dbus_interface(name = "org.opensuse.Agama1.Locale")] impl Locale { - /// Get labels for locales. The first pair is english language and territory - /// and second one is localized one to target language from locale. + /// Gets the supported locales information. /// - // Can be `async` as well. - // NOTE: check how often it is used and if often, it can be easily cached - fn labels_for_locales(&self) -> Result, Error> { - const DEFAULT_LANG: &str = "en"; - let mut res = Vec::with_capacity(self.supported_locales.len()); - let languages = agama_locale_data::get_languages()?; - let territories = agama_locale_data::get_territories()?; - for locale in self.supported_locales.as_slice() { - let (loc_language, loc_territory) = agama_locale_data::parse_locale(locale.as_str())?; - - let language = languages - .find_by_id(loc_language) - .context("language for passed locale not found")?; - let territory = territories - .find_by_id(loc_territory) - .context("territory for passed locale not found")?; - - let default_ret = ( - language - .names - .name_for(DEFAULT_LANG) - .context("missing default translation for language")?, - territory - .names - .name_for(DEFAULT_LANG) - .context("missing default translation for territory")?, - ); - let localized_ret = ( - language - .names - .name_for(language.id.as_str()) - .context("missing native label for language")?, - territory - .names - .name_for(language.id.as_str()) - .context("missing native label for territory")?, - ); - res.push((default_ret, localized_ret)); - } - - Ok(res) + /// Each element of the list has these parts: + /// + /// * The locale code (e.g., "es_ES.UTF-8"). + /// * The name of the language according to the language defined by the + /// UILocale property. + /// * The name of the territory according to the language defined by the + /// UILocale property. + fn list_locales(&self) -> Result, Error> { + let locales = self + .locales_db + .entries() + .iter() + .map(|l| { + ( + l.code.to_string(), + l.language.to_string(), + l.territory.to_string(), + ) + }) + .collect::>(); + Ok(locales) } #[dbus_interface(property)] @@ -67,7 +57,7 @@ impl Locale { #[dbus_interface(property)] fn set_locales(&mut self, locales: Vec) -> zbus::fdo::Result<()> { for loc in &locales { - if !self.supported_locales.contains(loc) { + if !self.locales_db.exists(loc.as_str()) { return Err(zbus::fdo::Error::Failed(format!( "Unsupported locale value '{loc}'" ))); @@ -77,119 +67,85 @@ impl Locale { Ok(()) } - #[dbus_interface(property)] - fn supported_locales(&self) -> Vec { - self.supported_locales.to_owned() - } - - #[dbus_interface(property)] - fn set_supported_locales(&mut self, locales: Vec) -> Result<(), zbus::fdo::Error> { - self.supported_locales = locales; - // TODO: handle if current selected locale contain something that is no longer supported - Ok(()) - } - #[dbus_interface(property, name = "UILocale")] - fn ui_locale(&self) -> &str { - &self.ui_locale + fn ui_locale(&self) -> String { + self.ui_locale.to_string() } #[dbus_interface(property, name = "UILocale")] - fn set_ui_locale(&mut self, locale: &str) { - self.ui_locale = locale.to_string(); + fn set_ui_locale(&mut self, locale: &str) -> zbus::fdo::Result<()> { + let locale: LocaleCode = locale + .try_into() + .map_err(|_e| zbus::fdo::Error::Failed(format!("Invalid locale value '{locale}'")))?; + helpers::set_service_locale(&locale); + Ok(self.translate(&locale)?) } - /// Gets list of locales available on system. + /// Returns a list of the supported keymaps. /// - /// # Examples + /// Each element of the list contains: /// - /// ``` - /// use agama_dbus_server::locale::Locale; - /// let locale = Locale::new(); - /// assert!(locale.list_ui_locales().unwrap().len() > 0); - /// ``` - #[dbus_interface(name = "ListUILocales")] - pub fn list_ui_locales(&self) -> Result, Error> { - // english is always available ui localization - let mut result = vec!["en".to_string()]; - const DIR: &str = "/usr/share/YaST2/locale/"; - let entries = read_dir(DIR); - if entries.is_err() { - // if dir is not there act like if it is empty - return Ok(result); - } - - for entry in entries.unwrap() { - let entry = entry.context("Failed to read entry in YaST2 locale dir")?; - let name = entry - .file_name() - .to_str() - .context("Non valid UTF entry found in YaST2 locale dir")? - .to_string(); - result.push(name) - } - - Ok(result) + /// * The keymap identifier (e.g., "es" or "es(ast)"). + /// * The name of the keyboard in language set by the UILocale property. + fn list_keymaps(&self) -> Result, Error> { + let keymaps = self + .keymaps_db + .entries() + .iter() + .map(|k| (k.id.to_string(), k.localized_description())) + .collect(); + Ok(keymaps) } - /* support only keymaps for console for now - fn list_x11_keyboards(&self) -> Result, Error> { - let keyboards = agama_locale_data::get_xkeyboards()?; - let ret = keyboards - .keyboard.iter() - .map(|k| (k.id.clone(), k.description.clone())) - .collect(); - Ok(ret) - } - - fn set_x11_keyboard(&mut self, keyboard: &str) { - self.keyboard_id = keyboard.to_string(); - } - */ - - #[dbus_interface(name = "ListVConsoleKeyboards")] - fn list_keyboards(&self) -> Result, Error> { - let res = agama_locale_data::get_key_maps()?; - Ok(res) + #[dbus_interface(property)] + fn keymap(&self) -> String { + self.keymap.to_string() } - #[dbus_interface(property, name = "VConsoleKeyboard")] - fn keymap(&self) -> &str { - self.keymap.as_str() - } + #[dbus_interface(property)] + fn set_keymap(&mut self, keymap_id: &str) -> Result<(), zbus::fdo::Error> { + let keymap_id: KeymapId = keymap_id + .parse() + .map_err(|_e| zbus::fdo::Error::InvalidArgs("Invalid keymap".to_string()))?; - #[dbus_interface(property, name = "VConsoleKeyboard")] - fn set_keymap(&mut self, keyboard: &str) -> Result<(), zbus::fdo::Error> { - let exist = agama_locale_data::get_key_maps() - .unwrap() - .iter() - .any(|k| k == keyboard); - if !exist { - return Err(zbus::fdo::Error::Failed( - "Invalid keyboard value".to_string(), - )); + if !self.keymaps_db.exists(&keymap_id) { + return Err(zbus::fdo::Error::Failed("Invalid keymap value".to_string())); } - self.keymap = keyboard.to_string(); + self.keymap = keymap_id; Ok(()) } - fn list_timezones(&self, locale: &str) -> Result, Error> { - let timezones = agama_locale_data::get_timezones(); - let localized = - agama_locale_data::get_timezone_parts()?.localize_timezones(locale, &timezones); - let ret = timezones.into_iter().zip(localized.into_iter()).collect(); - Ok(ret) + /// Returns a list of the supported timezones. + /// + /// Each element of the list contains: + /// + /// * The timezone identifier (e.g., "Europe/Berlin"). + /// * A list containing each part of the name in the language set by the + /// UILocale property. + fn list_timezones(&self) -> Result)>, Error> { + let timezones: Vec<_> = self + .timezones_db + .entries() + .iter() + .map(|tz| (tz.code.to_string(), tz.parts.clone())) + .collect(); + Ok(timezones) } #[dbus_interface(property)] fn timezone(&self) -> &str { - self.timezone_id.as_str() + self.timezone.as_str() } #[dbus_interface(property)] fn set_timezone(&mut self, timezone: &str) -> Result<(), zbus::fdo::Error> { - // NOTE: cannot use crate::Error as property expect this one - self.timezone_id = timezone.to_string(); + let timezone = timezone.to_string(); + if !self.timezones_db.exists(&timezone) { + return Err(zbus::fdo::Error::Failed(format!( + "Unsupported timezone value '{timezone}'" + ))); + } + self.timezone = timezone; Ok(()) } @@ -198,52 +154,78 @@ impl Locale { const ROOT: &str = "/mnt"; Command::new("/usr/bin/systemd-firstboot") .args([ - "root", + "--root", ROOT, + "--force", "--locale", self.locales.first().context("missing locale")?.as_str(), + "--keymap", + &self.keymap.to_string(), + "--timezone", + &self.timezone, ]) .status() .context("Failed to execute systemd-firstboot")?; - Command::new("/usr/bin/systemd-firstboot") - .args(["root", ROOT, "--keymap", self.keymap.as_str()]) - .status() - .context("Failed to execute systemd-firstboot")?; - Command::new("/usr/bin/systemd-firstboot") - .args(["root", ROOT, "--timezone", self.timezone_id.as_str()]) - .status() - .context("Failed to execute systemd-firstboot")?; Ok(()) } } impl Locale { - pub fn new() -> Self { - Self { - locales: vec!["en_US.UTF-8".to_string()], - keymap: "us".to_string(), - timezone_id: "America/Los_Angeles".to_string(), - supported_locales: vec!["en_US.UTF-8".to_string()], - ui_locale: "en".to_string(), - } - } -} - -impl Default for Locale { - fn default() -> Self { - Self::new() + pub fn new_with_locale(ui_locale: &LocaleCode) -> Result { + const DEFAULT_TIMEZONE: &str = "Europe/Berlin"; + + let locale = ui_locale.to_string(); + let mut locales_db = LocalesDatabase::new(); + locales_db.read(&locale)?; + + let default_locale = if locales_db.exists(locale.as_str()) { + ui_locale.to_string() + } else { + // TODO: handle the case where the database is empty (not expected!) + locales_db.entries().get(0).unwrap().code.to_string() + }; + + let mut timezones_db = TimezonesDatabase::new(); + timezones_db.read(&locale)?; + let mut default_timezone = DEFAULT_TIMEZONE.to_string(); + if !timezones_db.exists(&default_timezone) { + default_timezone = timezones_db.entries().get(0).unwrap().code.to_string(); + }; + + let mut keymaps_db = KeymapsDatabase::new(); + keymaps_db.read()?; + + let locale = Self { + keymap: "us".parse().unwrap(), + timezone: default_timezone, + locales: vec![default_locale], + locales_db, + timezones_db, + keymaps_db, + ui_locale: ui_locale.clone(), + }; + + Ok(locale) + } + + pub fn translate(&mut self, locale: &LocaleCode) -> Result<(), Error> { + self.timezones_db.read(&locale.language)?; + self.locales_db.read(&locale.language)?; + self.ui_locale = locale.clone(); + Ok(()) } } pub async fn export_dbus_objects( connection: &Connection, + locale: &LocaleCode, ) -> Result<(), Box> { const PATH: &str = "/org/opensuse/Agama1/Locale"; // When serving, request the service name _after_ exposing the main object - let locale = Locale::new(); - connection.object_server().at(PATH, locale).await?; + let locale_iface = Locale::new_with_locale(locale)?; + connection.object_server().at(PATH, locale_iface).await?; Ok(()) } diff --git a/rust/agama-dbus-server/src/locale/helpers.rs b/rust/agama-dbus-server/src/locale/helpers.rs new file mode 100644 index 0000000000..d9e65b6de7 --- /dev/null +++ b/rust/agama-dbus-server/src/locale/helpers.rs @@ -0,0 +1,30 @@ +//! Helpers functions +//! +//! FIXME: find a better place for the localization function + +use agama_locale_data::LocaleCode; +use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory}; +use std::env; + +/// Initializes the service locale. +/// +/// It returns the used locale. Defaults to `en_US.UTF-8`. +pub fn init_locale() -> Result> { + let lang = env::var("LANG").unwrap_or("en_US.UTF-8".to_string()); + let locale: LocaleCode = lang.as_str().try_into().unwrap_or_default(); + + set_service_locale(&locale); + textdomain("xkeyboard-config")?; + bind_textdomain_codeset("xkeyboard-config", "UTF-8")?; + Ok(locale) +} + +/// Sets the service locale. +/// +pub fn set_service_locale(locale: &LocaleCode) { + // Let's force the encoding to be 'UTF-8'. + let locale = format!("{}.UTF-8", locale.to_string()); + if setlocale(LocaleCategory::LcAll, locale).is_none() { + log::warn!("Could not set the locale"); + } +} diff --git a/rust/agama-dbus-server/src/locale/keyboard.rs b/rust/agama-dbus-server/src/locale/keyboard.rs new file mode 100644 index 0000000000..8a286bf4f6 --- /dev/null +++ b/rust/agama-dbus-server/src/locale/keyboard.rs @@ -0,0 +1,92 @@ +use agama_locale_data::{get_localectl_keymaps, keyboard::XkbConfigRegistry, KeymapId}; +use gettextrs::*; +use std::collections::HashMap; + +// Minimal representation of a keymap +pub struct Keymap { + pub id: KeymapId, + description: String, +} + +impl Keymap { + pub fn new(id: KeymapId, description: &str) -> Self { + Self { + id, + description: description.to_string(), + } + } + + pub fn localized_description(&self) -> String { + gettext(&self.description) + } +} + +/// Represents the keymaps database. +/// +/// The list of supported keymaps is read from `systemd-localed` and the +/// descriptions from the X Keyboard Configuraiton Database (see +/// `agama_locale_data::XkbConfigRegistry`). +#[derive(Default)] +pub struct KeymapsDatabase { + keymaps: Vec, +} + +impl KeymapsDatabase { + pub fn new() -> Self { + Self::default() + } + + /// Reads the list of keymaps. + pub fn read(&mut self) -> anyhow::Result<()> { + self.keymaps = get_keymaps()?; + Ok(()) + } + + pub fn exists(&self, id: &KeymapId) -> bool { + self.keymaps.iter().any(|k| &k.id == id) + } + + /// Returns the list of keymaps. + pub fn entries(&self) -> &Vec { + &self.keymaps + } +} + +/// Returns the list of keymaps to offer. +/// +/// It only includes the keyboards supported by `localectl` but getting +/// the description from the X Keyboard Configuration Database. +fn get_keymaps() -> anyhow::Result> { + let mut keymaps: Vec = vec![]; + let xkb_descriptions = get_keymap_descriptions(); + let keymap_ids = get_localectl_keymaps()?; + for keymap_id in keymap_ids { + let keymap_id_str = keymap_id.to_string(); + if let Some(description) = xkb_descriptions.get(&keymap_id_str) { + keymaps.push(Keymap::new(keymap_id, description)); + } else { + log::debug!("Keyboard '{}' not found in xkb database", keymap_id_str); + } + } + + Ok(keymaps) +} + +/// Returns a map of keymaps ids and its descriptions from the X Keyboard +/// Configuration Database. +fn get_keymap_descriptions() -> HashMap { + let layouts = XkbConfigRegistry::from_system().unwrap(); + let mut keymaps = HashMap::new(); + + for layout in layouts.layout_list.layouts { + let name = layout.config_item.name; + keymaps.insert(name.to_string(), layout.config_item.description.to_string()); + + for variant in layout.variants_list.variants { + let id = format!("{}({})", &name, &variant.config_item.name); + keymaps.insert(id, variant.config_item.description); + } + } + + keymaps +} diff --git a/rust/agama-dbus-server/src/locale/locale.rs b/rust/agama-dbus-server/src/locale/locale.rs new file mode 100644 index 0000000000..88a6b4c95e --- /dev/null +++ b/rust/agama-dbus-server/src/locale/locale.rs @@ -0,0 +1,139 @@ +//! This module provides support for reading the locales database. + +use crate::error::Error; +use agama_locale_data::{InvalidLocaleCode, LocaleCode}; +use anyhow::Context; +use std::process::Command; + +/// Represents a locale, including the localized language and territory. +#[derive(Debug)] +pub struct LocaleEntry { + /// The locale code (e.g., "es_ES.UTF-8"). + pub code: LocaleCode, + /// Localized language name (e.g., "Spanish", "Español", etc.) + pub language: String, + /// Localized territory name (e.g., "Spain", "España", etc.) + pub territory: String, +} + +/// Represents the locales database. +/// +/// The list of supported locales is read from `systemd-localed`. However, the +/// translations are obtained from the `agama_locale_data` crate. +#[derive(Default)] +pub struct LocalesDatabase { + known_locales: Vec, + locales: Vec, +} + +impl LocalesDatabase { + pub fn new() -> Self { + Self::default() + } + + /// Loads the list of locales. + /// + /// * `ui_language`: language to translate the descriptions (e.g., "en"). + pub fn read(&mut self, ui_language: &str) -> Result<(), Error> { + let result = Command::new("/usr/bin/localectl") + .args(["list-locales"]) + .output() + .context("Failed to get the list of locales")?; + let output = + String::from_utf8(result.stdout).context("Invalid UTF-8 sequence from list-locales")?; + self.known_locales = output + .lines() + .filter_map(|line| TryInto::::try_into(line).ok()) + .collect(); + self.locales = self.get_locales(&ui_language)?; + Ok(()) + } + + /// Determines whether a locale exists in the database. + pub fn exists(&self, locale: T) -> bool + where + T: TryInto, + T::Error: Into, + { + if let Ok(locale) = TryInto::::try_into(locale) { + return self.known_locales.contains(&locale); + } + + false + } + + /// Returns the list of locales. + pub fn entries(&self) -> &Vec { + &self.locales + } + + /// Gets the supported locales information. + /// + /// * `ui_language`: language to use in the translations. + fn get_locales(&self, ui_language: &str) -> Result, Error> { + const DEFAULT_LANG: &str = "en"; + let mut result = Vec::with_capacity(self.known_locales.len()); + let languages = agama_locale_data::get_languages()?; + let territories = agama_locale_data::get_territories()?; + for code in self.known_locales.as_slice() { + let language = languages + .find_by_id(&code.language) + .context("language not found")?; + + let names = &language.names; + let language_label = names + .name_for(&ui_language) + .or_else(|| names.name_for(DEFAULT_LANG)) + .unwrap_or(language.id.to_string()); + + let territory = territories + .find_by_id(&code.territory) + .context("territory not found")?; + + let names = &territory.names; + let territory_label = names + .name_for(&ui_language) + .or_else(|| names.name_for(DEFAULT_LANG)) + .unwrap_or(territory.id.to_string()); + + let entry = LocaleEntry { + code: code.clone(), + language: language_label, + territory: territory_label, + }; + result.push(entry) + } + + Ok(result) + } +} + +#[cfg(test)] +mod tests { + use super::LocalesDatabase; + use agama_locale_data::LocaleCode; + + #[ignore] + #[test] + fn test_read_locales() { + let mut db = LocalesDatabase::new(); + db.read("de").unwrap(); + let found_locales = db.entries(); + let spanish: LocaleCode = "es_ES".try_into().unwrap(); + let found = found_locales + .into_iter() + .find(|l| l.code == spanish) + .unwrap(); + assert_eq!(&found.language, "Spanisch"); + assert_eq!(&found.territory, "Spanien"); + } + + #[ignore] + #[test] + fn test_locale_exists() { + let mut db = LocalesDatabase::new(); + db.read("en").unwrap(); + assert!(db.exists("en_US")); + assert!(!db.exists("unknown_UNKNOWN")); + } +} diff --git a/rust/agama-dbus-server/src/locale/timezone.rs b/rust/agama-dbus-server/src/locale/timezone.rs new file mode 100644 index 0000000000..8b60197a45 --- /dev/null +++ b/rust/agama-dbus-server/src/locale/timezone.rs @@ -0,0 +1,102 @@ +//! This module provides support for reading the timezones database. + +use crate::error::Error; +use agama_locale_data::timezone_part::TimezoneIdParts; + +/// Represents a timezone, including each part as localized. +#[derive(Debug)] +pub struct TimezoneEntry { + /// Timezone identifier (e.g. "Atlantic/Canary"). + pub code: String, + /// Localized parts (e.g., "Atlántico", "Canarias"). + pub parts: Vec, +} + +#[derive(Default)] +pub struct TimezonesDatabase { + timezones: Vec, +} + +impl TimezonesDatabase { + pub fn new() -> Self { + Self::default() + } + + /// Initializes the list of known timezones. + /// + /// * `ui_language`: language to translate the descriptions (e.g., "en"). + pub fn read(&mut self, ui_language: &str) -> Result<(), Error> { + self.timezones = self.get_timezones(ui_language)?; + Ok(()) + } + + /// Determines whether a timezone exists in the database. + pub fn exists(&self, timezone: &String) -> bool { + self.timezones.iter().any(|t| &t.code == timezone) + } + + /// Returns the list of timezones. + pub fn entries(&self) -> &Vec { + &self.timezones + } + + /// Returns a list of the supported timezones. + /// + /// Each element of the list contains a timezone identifier and a vector + /// containing the translation of each part of the language. + /// + /// * `ui_language`: language to translate the descriptions (e.g., "en"). + fn get_timezones(&self, ui_language: &str) -> Result, Error> { + let timezones = agama_locale_data::get_timezones(); + let tz_parts = agama_locale_data::get_timezone_parts()?; + let ret = timezones + .into_iter() + .map(|tz| { + let parts = translate_parts(&tz, &ui_language, &tz_parts); + TimezoneEntry { code: tz, parts } + }) + .collect(); + Ok(ret) + } +} + +fn translate_parts(timezone: &str, ui_language: &str, tz_parts: &TimezoneIdParts) -> Vec { + timezone + .split("/") + .map(|part| { + tz_parts + .localize_part(part, &ui_language) + .unwrap_or(part.to_owned()) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::TimezonesDatabase; + + #[test] + fn test_read_timezones() { + let mut db = TimezonesDatabase::new(); + db.read("es").unwrap(); + let found_timezones = db.entries(); + dbg!(&found_timezones); + let found = found_timezones + .into_iter() + .find(|tz| tz.code == "Europe/Berlin") + .unwrap(); + assert_eq!(&found.code, "Europe/Berlin"); + assert_eq!( + found.parts, + vec!["Europa".to_string(), "Berlín".to_string()] + ) + } + + #[test] + fn test_timezone_exists() { + let mut db = TimezonesDatabase::new(); + db.read("es").unwrap(); + assert!(db.exists(&"Atlantic/Canary".to_string())); + assert!(!db.exists(&"Unknown/Unknown".to_string())); + } +} diff --git a/rust/agama-dbus-server/src/main.rs b/rust/agama-dbus-server/src/main.rs index cc849d0c1b..33c0c41db8 100644 --- a/rust/agama-dbus-server/src/main.rs +++ b/rust/agama-dbus-server/src/main.rs @@ -1,8 +1,8 @@ -use agama_dbus_server::{locale, network, questions}; +use agama_dbus_server::{locale, locale::helpers, network, questions}; use agama_lib::connection_to; use anyhow::Context; -use log::LevelFilter; +use log::{self, LevelFilter}; use std::future::pending; use tokio; @@ -11,6 +11,8 @@ const SERVICE_NAME: &str = "org.opensuse.Agama1"; #[tokio::main] async fn main() -> Result<(), Box> { + let locale = helpers::init_locale()?; + // be smart with logging and log directly to journal if connected to it if systemd_journal_logger::connected_to_journal() { // unwrap here is intentional as we are sure no other logger is active yet @@ -35,7 +37,7 @@ async fn main() -> Result<(), Box> { // When adding more services here, the order might be important. questions::export_dbus_objects(&connection).await?; log::info!("Started questions interface"); - locale::export_dbus_objects(&connection).await?; + locale::export_dbus_objects(&connection, &locale).await?; log::info!("Started locale interface"); network::export_dbus_objects(&connection).await?; log::info!("Started network interface"); diff --git a/rust/agama-dbus-server/src/network/dbus/interfaces.rs b/rust/agama-dbus-server/src/network/dbus/interfaces.rs index 682dbbfe10..23ebdbb784 100644 --- a/rust/agama-dbus-server/src/network/dbus/interfaces.rs +++ b/rust/agama-dbus-server/src/network/dbus/interfaces.rs @@ -6,11 +6,13 @@ use super::ObjectsRegistry; use crate::network::{ action::Action, error::NetworkStateError, - model::{Connection as NetworkConnection, Device as NetworkDevice, WirelessConnection}, + model::{ + Connection as NetworkConnection, Device as NetworkDevice, MacAddress, WirelessConnection, + }, }; use agama_lib::network::types::SSID; -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard}; use zbus::{ @@ -238,6 +240,19 @@ impl Connection { connection.set_interface(name); self.update_connection(connection).await } + + /// Custom mac-address + #[dbus_interface(property)] + pub async fn mac_address(&self) -> String { + self.get_connection().await.mac_address() + } + + #[dbus_interface(property)] + pub async fn set_mac_address(&mut self, mac_address: &str) -> zbus::fdo::Result<()> { + let mut connection = self.get_connection().await; + connection.set_mac_address(MacAddress::from_str(mac_address)?); + self.update_connection(connection).await + } } /// D-Bus interface for Match settings diff --git a/rust/agama-dbus-server/src/network/model.rs b/rust/agama-dbus-server/src/network/model.rs index 88a5416180..b413284999 100644 --- a/rust/agama-dbus-server/src/network/model.rs +++ b/rust/agama-dbus-server/src/network/model.rs @@ -221,6 +221,7 @@ pub enum Connection { Ethernet(EthernetConnection), Wireless(WirelessConnection), Loopback(LoopbackConnection), + Dummy(DummyConnection), } impl Connection { @@ -236,6 +237,7 @@ impl Connection { }), DeviceType::Loopback => Connection::Loopback(LoopbackConnection { base }), DeviceType::Ethernet => Connection::Ethernet(EthernetConnection { base }), + DeviceType::Dummy => Connection::Dummy(DummyConnection { base }), } } @@ -246,6 +248,7 @@ impl Connection { Connection::Ethernet(conn) => &conn.base, Connection::Wireless(conn) => &conn.base, Connection::Loopback(conn) => &conn.base, + Connection::Dummy(conn) => &conn.base, } } @@ -254,6 +257,7 @@ impl Connection { Connection::Ethernet(conn) => &mut conn.base, Connection::Wireless(conn) => &mut conn.base, Connection::Loopback(conn) => &mut conn.base, + Connection::Dummy(conn) => &mut conn.base, } } @@ -306,12 +310,25 @@ impl Connection { pub fn is_loopback(&self) -> bool { matches!(self, Connection::Loopback(_)) } + + pub fn is_ethernet(&self) -> bool { + matches!(self, Connection::Loopback(_)) || matches!(self, Connection::Ethernet(_)) + } + + pub fn mac_address(&self) -> String { + self.base().mac_address.to_string() + } + + pub fn set_mac_address(&mut self, mac_address: MacAddress) { + self.base_mut().mac_address = mac_address; + } } #[derive(Debug, Default, Clone)] pub struct BaseConnection { pub id: String, pub uuid: Uuid, + pub mac_address: MacAddress, pub ip_config: IpConfig, pub status: Status, pub interface: String, @@ -324,6 +341,59 @@ impl PartialEq for BaseConnection { } } +#[derive(Debug, Error)] +#[error("Invalid MAC address: {0}")] +pub struct InvalidMacAddress(String); + +#[derive(Debug, Default, Clone)] +pub enum MacAddress { + MacAddress(macaddr::MacAddr6), + Preserve, + Permanent, + Random, + Stable, + #[default] + Unset, +} + +impl FromStr for MacAddress { + type Err = InvalidMacAddress; + + fn from_str(s: &str) -> Result { + match s { + "preserve" => Ok(Self::Preserve), + "permanent" => Ok(Self::Permanent), + "random" => Ok(Self::Random), + "stable" => Ok(Self::Stable), + "" => Ok(Self::Unset), + _ => Ok(Self::MacAddress(match macaddr::MacAddr6::from_str(s) { + Ok(mac) => mac, + Err(e) => return Err(InvalidMacAddress(e.to_string())), + })), + } + } +} + +impl fmt::Display for MacAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let output = match &self { + Self::MacAddress(mac) => mac.to_string(), + Self::Preserve => "preserve".to_string(), + Self::Permanent => "permanent".to_string(), + Self::Random => "random".to_string(), + Self::Stable => "stable".to_string(), + Self::Unset => "".to_string(), + }; + write!(f, "{}", output) + } +} + +impl From for zbus::fdo::Error { + fn from(value: InvalidMacAddress) -> Self { + zbus::fdo::Error::Failed(value.to_string()) + } +} + #[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum Status { #[default] @@ -479,6 +549,11 @@ pub struct LoopbackConnection { pub base: BaseConnection, } +#[derive(Debug, Default, PartialEq, Clone)] +pub struct DummyConnection { + pub base: BaseConnection, +} + #[derive(Debug, Default, PartialEq, Clone)] pub struct WirelessConfig { pub mode: WirelessMode, diff --git a/rust/agama-dbus-server/src/network/nm/dbus.rs b/rust/agama-dbus-server/src/network/nm/dbus.rs index d601291e9f..11cc97c19a 100644 --- a/rust/agama-dbus-server/src/network/nm/dbus.rs +++ b/rust/agama-dbus-server/src/network/nm/dbus.rs @@ -17,6 +17,7 @@ const ETHERNET_KEY: &str = "802-3-ethernet"; const WIRELESS_KEY: &str = "802-11-wireless"; const WIRELESS_SECURITY_KEY: &str = "802-11-wireless-security"; const LOOPBACK_KEY: &str = "loopback"; +const DUMMY_KEY: &str = "dummy"; /// Converts a connection struct into a HashMap that can be sent over D-Bus. /// @@ -32,14 +33,22 @@ pub fn connection_to_dbus(conn: &Connection) -> NestedHash { result.insert("ipv6", ip_config_to_ipv6_dbus(conn.ip_config())); result.insert("match", match_config_to_dbus(conn.match_config())); - if let Connection::Wireless(wireless) = conn { - connection_dbus.insert("type", "802-11-wireless".into()); + if conn.is_ethernet() { + let ethernet_config = + HashMap::from([("assigned-mac-address", Value::new(conn.mac_address()))]); + result.insert(ETHERNET_KEY, ethernet_config); + } else if let Connection::Wireless(wireless) = conn { + connection_dbus.insert("type", WIRELESS_KEY.into()); let wireless_dbus = wireless_config_to_dbus(wireless); for (k, v) in wireless_dbus { result.insert(k, v); } } + if let Connection::Dummy(_) = conn { + connection_dbus.insert("type", DUMMY_KEY.into()); + } + result.insert("connection", connection_dbus); result } @@ -57,6 +66,10 @@ pub fn connection_from_dbus(conn: OwnedNestedHash) -> Option { })); } + if conn.get(DUMMY_KEY).is_some() { + return Some(Connection::Dummy(DummyConnection { base })); + }; + if conn.get(LOOPBACK_KEY).is_some() { return Some(Connection::Loopback(LoopbackConnection { base })); }; @@ -218,6 +231,10 @@ fn wireless_config_to_dbus(conn: &WirelessConnection) -> NestedHash { let wireless: HashMap<&str, zvariant::Value> = HashMap::from([ ("mode", Value::new(config.mode.to_string())), ("ssid", Value::new(config.ssid.to_vec())), + ( + "assigned-mac-address", + Value::new(conn.base.mac_address.to_string()), + ), ]); let mut security: HashMap<&str, zvariant::Value> = @@ -291,11 +308,31 @@ fn base_connection_from_dbus(conn: &OwnedNestedHash) -> Option { base_connection.match_config = match_config_from_dbus(match_config)?; } + if let Some(ethernet_config) = conn.get(ETHERNET_KEY) { + base_connection.mac_address = mac_address_from_dbus(ethernet_config)?; + } else if let Some(wireless_config) = conn.get(WIRELESS_KEY) { + base_connection.mac_address = mac_address_from_dbus(wireless_config)?; + } + base_connection.ip_config = ip_config_from_dbus(&conn)?; Some(base_connection) } +fn mac_address_from_dbus(config: &HashMap) -> Option { + if let Some(mac_address) = config.get("assigned-mac-address") { + match MacAddress::from_str(mac_address.downcast_ref::()?) { + Ok(mac) => Some(mac), + Err(e) => { + log::warn!("Couldn't parse MAC: {}", e); + None + } + } + } else { + Some(MacAddress::Unset) + } +} + fn match_config_from_dbus( match_config: &HashMap, ) -> Option { @@ -585,6 +622,8 @@ mod test { let match_config = connection.match_config(); assert_eq!(match_config.kernel, vec!["pci-0000:00:19.0"]); + assert_eq!(connection.mac_address(), "12:34:56:78:9A:BC"); + assert_eq!( ip_config.addresses, vec![ @@ -640,6 +679,10 @@ mod test { "ssid".to_string(), Value::new("agama".as_bytes()).to_owned(), ), + ( + "assigned-mac-address".to_string(), + Value::new("13:45:67:89:AB:CD").to_owned(), + ), ]); let security_section = @@ -652,6 +695,7 @@ mod test { ]); let connection = connection_from_dbus(dbus_conn).unwrap(); + assert_eq!(connection.mac_address(), "13:45:67:89:AB:CD".to_string()); assert!(matches!(connection, Connection::Wireless(_))); if let Connection::Wireless(connection) = connection { assert_eq!(connection.wireless.ssid, SSID(vec![97, 103, 97, 109, 97])); @@ -679,6 +723,12 @@ mod test { let wireless = wireless_dbus.get("802-11-wireless").unwrap(); let mode: &str = wireless.get("mode").unwrap().downcast_ref().unwrap(); assert_eq!(mode, "infrastructure"); + let mac_address: &str = wireless + .get("assigned-mac-address") + .unwrap() + .downcast_ref() + .unwrap(); + assert_eq!(mac_address, "FD:CB:A9:87:65:43"); let ssid: &zvariant::Array = wireless.get("ssid").unwrap().downcast_ref().unwrap(); let ssid: Vec = ssid @@ -804,19 +854,33 @@ mod test { Value::new("eth0".to_string()).to_owned(), ), ]); + let ethernet = HashMap::from([( + "assigned-mac-address".to_string(), + Value::new("12:34:56:78:9A:BC".to_string()).to_owned(), + )]); original.insert("connection".to_string(), connection); + original.insert(ETHERNET_KEY.to_string(), ethernet); let mut updated = Connection::Ethernet(EthernetConnection::default()); updated.set_interface(""); + updated.set_mac_address(MacAddress::Unset); let updated = connection_to_dbus(&updated); let merged = merge_dbus_connections(&original, &updated); let connection = merged.get("connection").unwrap(); assert_eq!(connection.get("interface-name"), None); + let ethernet = merged.get(ETHERNET_KEY).unwrap(); + assert_eq!(ethernet.get("assigned-mac-address"), Some(&Value::from(""))); } fn build_ethernet_section_from_dbus() -> HashMap { - HashMap::from([("auto-negotiate".to_string(), true.into())]) + HashMap::from([ + ("auto-negotiate".to_string(), true.into()), + ( + "assigned-mac-address".to_string(), + Value::new("12:34:56:78:9A:BC").to_owned(), + ), + ]) } fn build_base_connection() -> BaseConnection { @@ -840,9 +904,11 @@ mod test { }]), ..Default::default() }; + let mac_address = MacAddress::from_str("FD:CB:A9:87:65:43").unwrap(); BaseConnection { id: "agama".to_string(), ip_config, + mac_address, ..Default::default() } } @@ -859,6 +925,14 @@ mod test { let id: &str = connection_dbus.get("id").unwrap().downcast_ref().unwrap(); assert_eq!(id, "agama"); + let ethernet_connection = conn_dbus.get(ETHERNET_KEY).unwrap(); + let mac_address: &str = ethernet_connection + .get("assigned-mac-address") + .unwrap() + .downcast_ref() + .unwrap(); + assert_eq!(mac_address, "FD:CB:A9:87:65:43"); + let ipv4_dbus = conn_dbus.get("ipv4").unwrap(); let gateway4: &str = ipv4_dbus.get("gateway").unwrap().downcast_ref().unwrap(); assert_eq!(gateway4, "192.168.0.1"); diff --git a/rust/agama-dbus-server/src/network/nm/model.rs b/rust/agama-dbus-server/src/network/nm/model.rs index 93ce9f69c8..9175b8b342 100644 --- a/rust/agama-dbus-server/src/network/nm/model.rs +++ b/rust/agama-dbus-server/src/network/nm/model.rs @@ -83,6 +83,7 @@ impl TryFrom for DeviceType { NmDeviceType(0) => Ok(DeviceType::Loopback), NmDeviceType(1) => Ok(DeviceType::Ethernet), NmDeviceType(2) => Ok(DeviceType::Wireless), + NmDeviceType(3) => Ok(DeviceType::Dummy), NmDeviceType(_) => Err(NmError::UnsupportedDeviceType(value.into())), } } diff --git a/rust/agama-dbus-server/tests/network.rs b/rust/agama-dbus-server/tests/network.rs index 37d404210a..e4f8d4862e 100644 --- a/rust/agama-dbus-server/tests/network.rs +++ b/rust/agama-dbus-server/tests/network.rs @@ -62,6 +62,7 @@ async fn test_add_connection() -> Result<(), Box> { let addresses: Vec = vec!["192.168.0.2/24".parse()?, "::ffff:c0a8:7ac7/64".parse()?]; let wlan0 = settings::NetworkConnection { id: "wlan0".to_string(), + mac_address: Some("FD:CB:A9:87:65:43".to_string()), method4: Some("auto".to_string()), method6: Some("disabled".to_string()), addresses: addresses.clone(), @@ -80,6 +81,7 @@ async fn test_add_connection() -> Result<(), Box> { let conn = conns.first().unwrap(); assert_eq!(conn.id, "wlan0"); + assert_eq!(conn.mac_address, Some("FD:CB:A9:87:65:43".to_string())); assert_eq!(conn.device_type(), DeviceType::Wireless); assert_eq!(&conn.addresses, &addresses); let method4 = conn.method4.as_ref().unwrap(); diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 2d9cb33b0a..97e2a29293 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -36,6 +36,10 @@ "description": "The name of the network interface bound to this connection", "type": "string" }, + "mac-address": { + "description": "Custom mac-address (can also be 'preserve', 'permanent', 'random' or 'stable')", + "type": "string" + }, "method4": { "description": "IPv4 configuration method (e.g., 'auto')", "type": "string", diff --git a/rust/agama-lib/src/network/client.rs b/rust/agama-lib/src/network/client.rs index 485baee516..d8e3077216 100644 --- a/rust/agama-lib/src/network/client.rs +++ b/rust/agama-lib/src/network/client.rs @@ -67,6 +67,10 @@ impl<'a> NetworkClient<'a> { "" => None, value => Some(value.to_string()), }; + let mac_address = match connection_proxy.mac_address().await?.as_str() { + "" => None, + value => Some(value.to_string()), + }; let ip_proxy = IPProxy::builder(&self.connection) .path(path)? @@ -91,6 +95,7 @@ impl<'a> NetworkClient<'a> { addresses, nameservers, interface, + mac_address, ..Default::default() }) } @@ -189,6 +194,9 @@ impl<'a> NetworkClient<'a> { let interface = conn.interface.as_deref().unwrap_or(""); proxy.set_interface(interface).await?; + let mac_address = conn.mac_address.as_deref().unwrap_or(""); + proxy.set_mac_address(mac_address).await?; + self.update_ip_settings(path, conn).await?; if let Some(ref wireless) = conn.wireless { diff --git a/rust/agama-lib/src/network/proxies.rs b/rust/agama-lib/src/network/proxies.rs index 2b43124722..314e0c5b23 100644 --- a/rust/agama-lib/src/network/proxies.rs +++ b/rust/agama-lib/src/network/proxies.rs @@ -83,6 +83,10 @@ trait Connection { fn interface(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_interface(&self, interface: &str) -> zbus::Result<()>; + #[dbus_proxy(property)] + fn mac_address(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_mac_address(&self, mac_address: &str) -> zbus::Result<()>; } #[dbus_proxy( diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index cd344ef323..34be2f4090 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -69,6 +69,8 @@ pub struct NetworkConnection { pub interface: Option, #[serde(skip_serializing_if = "Option::is_none")] pub match_settings: Option, + #[serde(rename = "mac-address", skip_serializing_if = "Option::is_none")] + pub mac_address: Option, } impl NetworkConnection { diff --git a/rust/agama-lib/src/network/types.rs b/rust/agama-lib/src/network/types.rs index 5aa56c5145..10daa400c1 100644 --- a/rust/agama-lib/src/network/types.rs +++ b/rust/agama-lib/src/network/types.rs @@ -29,6 +29,7 @@ pub enum DeviceType { Loopback = 0, Ethernet = 1, Wireless = 2, + Dummy = 3, } #[derive(Debug, Error, PartialEq)] @@ -43,6 +44,7 @@ impl TryFrom for DeviceType { 0 => Ok(DeviceType::Loopback), 1 => Ok(DeviceType::Ethernet), 2 => Ok(DeviceType::Wireless), + 3 => Ok(DeviceType::Dummy), _ => Err(InvalidDeviceType(value)), } } diff --git a/rust/agama-lib/src/proxies.rs b/rust/agama-lib/src/proxies.rs index cf4b175e93..e72cdf7401 100644 --- a/rust/agama-lib/src/proxies.rs +++ b/rust/agama-lib/src/proxies.rs @@ -75,40 +75,43 @@ trait Manager { ) -> zbus::Result>>; } -#[dbus_proxy(interface = "org.opensuse.Agama1.Locale", assume_defaults = true)] +#[dbus_proxy( + interface = "org.opensuse.Agama1.Locale", + default_service = "org.opensuse.Agama1", + default_path = "/org/opensuse/Agama1/Locale" +)] trait Locale { /// Commit method fn commit(&self) -> zbus::Result<()>; - /// LabelsForLocales method - fn labels_for_locales(&self) -> zbus::Result>; + /// ListKeymaps method + fn list_keymaps(&self) -> zbus::Result>; + + /// ListLocales method + fn list_locales(&self) -> zbus::Result>; /// ListTimezones method - fn list_timezones(&self, locale: &str) -> zbus::Result>; + fn list_timezones(&self) -> zbus::Result)>>; - /// ListVConsoleKeyboards method - #[dbus_proxy(name = "ListVConsoleKeyboards")] - fn list_vconsole_keyboards(&self) -> zbus::Result>; + /// Keymap property + #[dbus_proxy(property)] + fn keymap(&self) -> zbus::Result; + fn set_keymap(&self, value: &str) -> zbus::Result<()>; /// Locales property #[dbus_proxy(property)] fn locales(&self) -> zbus::Result>; fn set_locales(&self, value: &[&str]) -> zbus::Result<()>; - /// SupportedLocales property - #[dbus_proxy(property)] - fn supported_locales(&self) -> zbus::Result>; - fn set_supported_locales(&self, value: &[&str]) -> zbus::Result<()>; - /// Timezone property #[dbus_proxy(property)] fn timezone(&self) -> zbus::Result; fn set_timezone(&self, value: &str) -> zbus::Result<()>; - /// VConsoleKeyboard property - #[dbus_proxy(property, name = "VConsoleKeyboard")] - fn vconsole_keyboard(&self) -> zbus::Result; - fn set_vconsole_keyboard(&self, value: &str) -> zbus::Result<()>; + /// UILocale property + #[dbus_proxy(property, name = "UILocale")] + fn uilocale(&self) -> zbus::Result; + fn set_uilocale(&self, value: &str) -> zbus::Result<()>; } #[dbus_proxy( diff --git a/rust/agama-locale-data/Cargo.toml b/rust/agama-locale-data/Cargo.toml index 46380041c5..ca9eda6ba3 100644 --- a/rust/agama-locale-data/Cargo.toml +++ b/rust/agama-locale-data/Cargo.toml @@ -12,3 +12,4 @@ quick-xml = { version = "0.28.2", features = ["serialize"] } flate2 = "1.0.25" chrono-tz = "0.8.2" regex = "1" +thiserror = "1.0.50" diff --git a/rust/agama-locale-data/src/keyboard.rs b/rust/agama-locale-data/src/keyboard.rs new file mode 100644 index 0000000000..879217dd7d --- /dev/null +++ b/rust/agama-locale-data/src/keyboard.rs @@ -0,0 +1,5 @@ +pub mod xkb_config_registry; +pub mod xkeyboard; + +pub use xkb_config_registry::XkbConfigRegistry; +pub use xkeyboard::XKeyboards; diff --git a/rust/agama-locale-data/src/keyboard/xkb_config_registry.rs b/rust/agama-locale-data/src/keyboard/xkb_config_registry.rs new file mode 100644 index 0000000000..a1bdd07cb6 --- /dev/null +++ b/rust/agama-locale-data/src/keyboard/xkb_config_registry.rs @@ -0,0 +1,70 @@ +//! This module aims to read the information in the X Keyboard Configuration Database. +//! +//! https://freedesktop.org/Software/XKeyboardConfig + +use quick_xml::de::from_str; +use serde::Deserialize; +use std::{error::Error, fs}; + +const DB_PATH: &'static str = "/usr/share/X11/xkb/rules/base.xml"; + +/// X Keyboard Configuration Database +#[derive(Deserialize, Debug)] +pub struct XkbConfigRegistry { + #[serde(rename = "layoutList")] + pub layout_list: LayoutList, +} + +impl XkbConfigRegistry { + /// Reads the database from the given file + /// + /// - `path`: database path. + pub fn from(path: &str) -> Result> { + let contents = fs::read_to_string(&path)?; + Ok(from_str(&contents)?) + } + + /// Reads the database from the default path. + pub fn from_system() -> Result> { + Self::from(DB_PATH) + } +} + +#[derive(Deserialize, Debug)] +pub struct LayoutList { + #[serde(rename = "layout")] + pub layouts: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct Layout { + #[serde(rename = "configItem")] + pub config_item: ConfigItem, + #[serde(rename = "variantList", default)] + pub variants_list: VariantList, +} + +#[derive(Deserialize, Debug)] +pub struct ConfigItem { + pub name: String, + #[serde(rename = "description")] + pub description: String, +} + +#[derive(Deserialize, Debug, Default)] +pub struct VariantList { + #[serde(rename = "variant", default)] + pub variants: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct Variant { + #[serde(rename = "configItem")] + pub config_item: VariantConfigItem, +} + +#[derive(Deserialize, Debug)] +pub struct VariantConfigItem { + pub name: String, + pub description: String, +} diff --git a/rust/agama-locale-data/src/xkeyboard.rs b/rust/agama-locale-data/src/keyboard/xkeyboard.rs similarity index 100% rename from rust/agama-locale-data/src/xkeyboard.rs rename to rust/agama-locale-data/src/keyboard/xkeyboard.rs diff --git a/rust/agama-locale-data/src/lib.rs b/rust/agama-locale-data/src/lib.rs index 0fe0803915..e1a48914d0 100644 --- a/rust/agama-locale-data/src/lib.rs +++ b/rust/agama-locale-data/src/lib.rs @@ -1,7 +1,6 @@ use anyhow::Context; use flate2::bufread::GzDecoder; use quick_xml::de::Deserializer; -use regex::Regex; use serde::Deserialize; use std::fs::File; use std::io::BufRead; @@ -9,12 +8,17 @@ use std::io::BufReader; use std::process::Command; pub mod deprecated_timezones; +pub mod keyboard; pub mod language; +mod locale; pub mod localization; pub mod ranked; pub mod territory; pub mod timezone_part; -pub mod xkeyboard; + +use keyboard::xkeyboard; + +pub use locale::{InvalidLocaleCode, KeymapId, LocaleCode}; fn file_reader(file_path: &str) -> anyhow::Result { let file = File::open(file_path) @@ -39,10 +43,13 @@ pub fn get_xkeyboards() -> anyhow::Result { /// Requires working localectl. /// /// ```no_run -/// let key_maps = agama_locale_data::get_key_maps().unwrap(); -/// assert!(key_maps.contains(&"us".to_string())) +/// use agama_locale_data::KeymapId; +/// +/// let key_maps = agama_locale_data::get_localectl_keymaps().unwrap(); +/// let us: KeymapId = "us".parse().unwrap(); +/// assert!(key_maps.contains(&us)); /// ``` -pub fn get_key_maps() -> anyhow::Result> { +pub fn get_localectl_keymaps() -> anyhow::Result> { const BINARY: &str = "/usr/bin/localectl"; let output = Command::new(BINARY) .arg("list-keymaps") @@ -50,31 +57,11 @@ pub fn get_key_maps() -> anyhow::Result> { .context("failed to execute localectl list-maps")? .stdout; let output = String::from_utf8(output).context("Strange localectl output formatting")?; - let ret = output.split('\n').map(|l| l.trim().to_string()).collect(); + let ret: Vec<_> = output.lines().flat_map(|l| l.parse().ok()).collect(); Ok(ret) } -/// Parses given locale to language and territory part -/// -/// /// ## Examples -/// -/// ``` -/// let result = agama_locale_data::parse_locale("en_US.UTF-8").unwrap(); -/// assert_eq!(result.0, "en"); -/// assert_eq!(result.1, "US") -/// ``` -pub fn parse_locale(locale: &str) -> anyhow::Result<(&str, &str)> { - let locale_regexp: Regex = Regex::new(r"^([[:alpha:]]+)_([[:alpha:]]+)").unwrap(); - let captures = locale_regexp - .captures(locale) - .context("Failed to parse locale")?; - Ok(( - captures.get(1).unwrap().as_str(), - captures.get(2).unwrap().as_str(), - )) -} - /// Returns struct which contain list of known languages pub fn get_languages() -> anyhow::Result { const FILE_PATH: &str = "/usr/share/langtable/data/languages.xml.gz"; diff --git a/rust/agama-locale-data/src/locale.rs b/rust/agama-locale-data/src/locale.rs new file mode 100644 index 0000000000..90551ce233 --- /dev/null +++ b/rust/agama-locale-data/src/locale.rs @@ -0,0 +1,157 @@ +//! Defines useful types to deal with localization values + +use regex::Regex; +use std::sync::OnceLock; +use std::{fmt::Display, str::FromStr}; +use thiserror::Error; + +#[derive(Clone, Debug, PartialEq)] +pub struct LocaleCode { + // ISO-639 + pub language: String, + // ISO-3166 + pub territory: String, + // encoding: String, +} + +impl Display for LocaleCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}_{}", &self.language, &self.territory) + } +} + +impl Default for LocaleCode { + fn default() -> Self { + Self { + language: "en".to_string(), + territory: "US".to_string(), + } + } +} + +#[derive(Error, Debug)] +#[error("Not a valid locale string: {0}")] +pub struct InvalidLocaleCode(String); + +impl TryFrom<&str> for LocaleCode { + type Error = InvalidLocaleCode; + + fn try_from(value: &str) -> Result { + let locale_regexp: Regex = Regex::new(r"^([[:alpha:]]+)_([[:alpha:]]+)").unwrap(); + let captures = locale_regexp + .captures(value) + .ok_or_else(|| InvalidLocaleCode(value.to_string()))?; + + Ok(Self { + language: captures.get(1).unwrap().as_str().to_string(), + territory: captures.get(2).unwrap().as_str().to_string(), + }) + } +} + +static KEYMAP_ID_REGEX: OnceLock = OnceLock::new(); + +/// Keymap layout identifier +/// +/// ``` +/// use agama_locale_data::KeymapId; +/// use std::str::FromStr; +/// +/// let id: KeymapId = "es(ast)".parse().unwrap(); +/// assert_eq!(&id.layout, "es"); +/// assert_eq!(id.variant.clone(), Some("ast".to_string())); +/// assert_eq!(id.dashed(), "es-ast".to_string()); +/// +/// let id_with_dashes: KeymapId = "es-ast".parse().unwrap(); +/// assert_eq!(id, id_with_dashes); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct KeymapId { + pub layout: String, + pub variant: Option, +} + +#[derive(Error, Debug)] +#[error("Invalid keymap ID: {0}")] +pub struct InvalidKeymap(String); + +impl KeymapId { + pub fn dashed(&self) -> String { + if let Some(variant) = &self.variant { + format!("{}-{}", &self.layout, variant) + } else { + self.layout.to_owned() + } + } +} + +impl Display for KeymapId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(variant) = &self.variant { + write!(f, "{}({})", &self.layout, variant) + } else { + write!(f, "{}", &self.layout) + } + } +} + +impl FromStr for KeymapId { + type Err = InvalidKeymap; + + fn from_str(s: &str) -> Result { + let re = KEYMAP_ID_REGEX + .get_or_init(|| Regex::new(r"(\w+)((\((?\w+)\)|-(?\w+)))?").unwrap()); + + if let Some(parts) = re.captures(s) { + let mut variant = None; + if let Some(var1) = parts.name("var1") { + variant = Some(var1.as_str().to_string()); + } + if let Some(var2) = parts.name("var2") { + variant = Some(var2.as_str().to_string()); + } + Ok(KeymapId { + layout: parts[1].to_string(), + variant, + }) + } else { + Err(InvalidKeymap(s.to_string())) + } + } +} + +#[cfg(test)] +mod test { + use super::KeymapId; + use std::str::FromStr; + + #[test] + fn test_parse_keymap_id() { + let keymap_id0 = KeymapId::from_str("es").unwrap(); + assert_eq!( + KeymapId { + layout: "es".to_string(), + variant: None + }, + keymap_id0 + ); + + let keymap_id1 = KeymapId::from_str("es(ast)").unwrap(); + assert_eq!( + KeymapId { + layout: "es".to_string(), + variant: Some("ast".to_string()) + }, + keymap_id1 + ); + + let keymap_id2 = KeymapId::from_str("es-ast").unwrap(); + assert_eq!( + KeymapId { + layout: "es".to_string(), + variant: Some("ast".to_string()) + }, + keymap_id2 + ); + } +} diff --git a/rust/agama-locale-data/src/timezone_part.rs b/rust/agama-locale-data/src/timezone_part.rs index c1390c3815..1f1d1223e6 100644 --- a/rust/agama-locale-data/src/timezone_part.rs +++ b/rust/agama-locale-data/src/timezone_part.rs @@ -20,6 +20,14 @@ pub struct TimezoneIdParts { } impl TimezoneIdParts { + // TODO: Implement a caching mechanism + pub fn localize_part(&self, part_id: &str, language: &str) -> Option { + self.timezone_part + .iter() + .find(|p| p.id == part_id) + .and_then(|p| p.names.name_for(language)) + } + /// Localized given list of timezones to given language /// # Examples /// diff --git a/rust/package/agama-cli.changes b/rust/package/agama-cli.changes index 0e95ea5b8c..0a6e8d573f 100644 --- a/rust/package/agama-cli.changes +++ b/rust/package/agama-cli.changes @@ -1,3 +1,42 @@ +------------------------------------------------------------------- +Tue Dec 5 11:18:41 UTC 2023 - Jorik Cronenberg + +- Add ability to assign a custom MAC address for network + connections (gh#openSUSE/agama#893) + +------------------------------------------------------------------- +Tue Dec 5 09:46:48 UTC 2023 - José Iván López González + +- Explicitly add dependencies instead of relying on the live ISO + to provide the required packages (gh#openSUSE/agama/911). + +------------------------------------------------------------------- +Sun Dec 3 15:53:34 UTC 2023 - Imobach Gonzalez Sosa + +- Use a single call to systemd-firstboot to write the localization + settings (gh#openSUSE/agama#903). + +------------------------------------------------------------------- +Sat Dec 2 18:05:54 UTC 2023 - Imobach Gonzalez Sosa + +- Version 6 + +------------------------------------------------------------------- +Wed Nov 29 11:19:51 UTC 2023 - Imobach Gonzalez Sosa + +- Rework the org.opensuse.Agama1.Locale interface + (gh#openSUSE/agama#881): + * Replace LabelsForLocales function with ListLocales. + * Add a ListKeymaps function. + * Extend the ListTimezone function to include the translation of + each part. + * Drop ListUILocales and ListVConsoleKeyboards functions. + * Remove the SupportedLocales and VConsoleKeyboard properties. + * Do not read the lists of locales, keymaps and timezones on + each request. + * Peform some validation when trying to change the Locales, + Keymap and Timezone properties. + ------------------------------------------------------------------- Thu Nov 16 11:06:30 UTC 2023 - Imobach Gonzalez Sosa diff --git a/rust/package/agama-cli.spec b/rust/package/agama-cli.spec index 9518cb35b3..1e200fa2b3 100644 --- a/rust/package/agama-cli.spec +++ b/rust/package/agama-cli.spec @@ -40,6 +40,8 @@ Requires: lshw # required by "agama logs store" Requires: bzip2 Requires: tar +# required for translating the keyboards descriptions +Requires: xkeyboard-config-lang %description Command line program to interact with the agama service. diff --git a/service/.rubocop.yml b/service/.rubocop.yml index ed2faf80db..137ea30537 100644 --- a/service/.rubocop.yml +++ b/service/.rubocop.yml @@ -24,3 +24,7 @@ Metrics/BlockLength: # assignment in method calls is used to document some params Lint/UselessAssignment: Enabled: false + +# be less strict +Metrics/AbcSize: + Max: 32 diff --git a/service/Gemfile.lock b/service/Gemfile.lock old mode 100644 new mode 100755 index d6f25b0609..9f6cb9c0d0 --- a/service/Gemfile.lock +++ b/service/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - agama (5) + agama (6) cfa (~> 1.0.2) cfa_grub2 (~> 2.0.0) cheetah (~> 1.0.0) @@ -73,4 +73,4 @@ DEPENDENCIES yard (~> 0.9.0) BUNDLED WITH - 2.3.26 + 2.4.22 diff --git a/service/lib/agama/config.rb b/service/lib/agama/config.rb index 0e524d648c..3b4c2ba811 100644 --- a/service/lib/agama/config.rb +++ b/service/lib/agama/config.rb @@ -168,12 +168,14 @@ def merge(config) # # @param keys [Array] Config data keys of the collection. # @param property [Symbol|String|nil] Property to retrieve of the elements. + # @param default [Object] The default value returned when the value is not + # found or is not an array # # @return [Array] - def arch_elements_from(*keys, property: nil) + def arch_elements_from(*keys, property: nil, default: []) keys.map!(&:to_s) elements = products.dig(*keys) - return [] unless elements.is_a?(Array) + return default unless elements.is_a?(Array) elements.map do |element| if !element.is_a?(Hash) diff --git a/service/lib/agama/dbus/clients/base.rb b/service/lib/agama/dbus/clients/base.rb index 9aa6b633e2..bb26770c42 100644 --- a/service/lib/agama/dbus/clients/base.rb +++ b/service/lib/agama/dbus/clients/base.rb @@ -27,6 +27,22 @@ module Agama module DBus module Clients # Base class for D-Bus clients + # + # The clients should be singleton because the #on_properties_change method + # will not work properly with several instances. Given a + # ::DBus::BusConnection object, ruby-dbus does not allow to register more + # than one callback for the same object. + # + # It causes the last instance to overwrite the callbacks from previous ones. + # + # @example Creating a new client + # require "singleton" + # + # class Locale < Base + # include Singleton + # + # # client methods + # end class Base # @!method service_name # Name of the D-Bus service diff --git a/service/lib/agama/dbus/clients/locale.rb b/service/lib/agama/dbus/clients/locale.rb index 7a3a37b359..1cc0cd797a 100644 --- a/service/lib/agama/dbus/clients/locale.rb +++ b/service/lib/agama/dbus/clients/locale.rb @@ -20,12 +20,14 @@ # find current contact information at www.suse.com. require "agama/dbus/clients/base" +require "singleton" module Agama module DBus module Clients # D-Bus client for locale configuration class Locale < Base + include Singleton INTERFACE_NAME = "org.opensuse.Agama1.Locale" def initialize @@ -39,13 +41,6 @@ def service_name @service_name ||= "org.opensuse.Agama1" end - # Sets the supported locales. It can differs per product. - # - # @param locales [Array] - def supported_locales=(locales) - dbus_object.supported_locales = locales - end - def ui_locale dbus_object[INTERFACE_NAME]["UILocale"] end @@ -54,10 +49,6 @@ def ui_locale=(locale) dbus_object[INTERFACE_NAME]["UILocale"] = locale end - def available_ui_locales - dbus_object.ListUILocales - end - # Finishes the language installation def finish dbus_object.Commit diff --git a/service/lib/agama/dbus/manager.rb b/service/lib/agama/dbus/manager.rb index 1a759ce672..d1f129d18e 100644 --- a/service/lib/agama/dbus/manager.rb +++ b/service/lib/agama/dbus/manager.rb @@ -60,7 +60,7 @@ def initialize(backend, logger) dbus_method(:Probe, "") { config_phase } dbus_method(:Commit, "") { install_phase } dbus_method(:CanInstall, "out result:b") { can_install? } - dbus_method(:CollectLogs, "out tarball_filesystem_path:s, in user:s") { |u| collect_logs(u) } + dbus_method(:CollectLogs, "out tarball_filesystem_path:s") { collect_logs } dbus_method(:Finish, "") { finish_phase } dbus_reader :installation_phases, "aa{sv}" dbus_reader :current_installation_phase, "u" @@ -92,8 +92,8 @@ def can_install? end # Collects the YaST logs - def collect_logs(user) - backend.collect_logs(user) + def collect_logs + backend.collect_logs end # Last action for the installer diff --git a/service/lib/agama/dbus/manager_service.rb b/service/lib/agama/dbus/manager_service.rb index 4c73938a0f..6351ef4031 100644 --- a/service/lib/agama/dbus/manager_service.rb +++ b/service/lib/agama/dbus/manager_service.rb @@ -73,7 +73,7 @@ def start setup_cockpit export # We need locale for data from users - locale_client = Clients::Locale.new + locale_client = Clients::Locale.instance # TODO: test if we need to pass block with additional actions @ui_locale = UILocale.new(locale_client) manager.on_progress_change { dispatch } # make single thread more responsive diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb index aad196e3cb..a22729c3eb 100644 --- a/service/lib/agama/dbus/software/manager.rb +++ b/service/lib/agama/dbus/software/manager.rb @@ -132,9 +132,9 @@ def finish # Registers callback to be called def register_callbacks - lang_client = Agama::DBus::Clients::Locale.new - lang_client.on_language_selected do |language_ids| + Agama::DBus::Clients::Locale.instance.on_language_selected do |language_ids| backend.languages = language_ids + probe end nm_client = Agama::DBus::Clients::Network.new diff --git a/service/lib/agama/dbus/software_service.rb b/service/lib/agama/dbus/software_service.rb index 3f357bb000..245f2f6a97 100644 --- a/service/lib/agama/dbus/software_service.rb +++ b/service/lib/agama/dbus/software_service.rb @@ -58,8 +58,7 @@ def start # for some reason the the "export" method must be called before # registering the language change callback to work properly export - locale_client = Clients::Locale.new - @ui_locale = UILocale.new(locale_client) do |locale| + @ui_locale = UILocale.new(Clients::Locale.instance) do |locale| # set the locale in the Language module, when changing the repository # (product) it calls Pkg.SetTextLocale(Language.language) internally Yast::Language.Set(locale) diff --git a/service/lib/agama/dbus/storage_service.rb b/service/lib/agama/dbus/storage_service.rb index 773e37a3e9..37da268335 100644 --- a/service/lib/agama/dbus/storage_service.rb +++ b/service/lib/agama/dbus/storage_service.rb @@ -53,9 +53,8 @@ def bus # Starts storage service. It does more then just #export method. def start export - locale_client = Clients::Locale.new # TODO: test if we need to pass block with additional actions - @ui_locale = UILocale.new(locale_client) + @ui_locale = UILocale.new(Clients::Locale.instance) end # Exports the storage proposal object through the D-Bus service diff --git a/service/lib/agama/dbus/y2dir/modules/InstFunctions.rb b/service/lib/agama/dbus/y2dir/modules/InstFunctions.rb new file mode 100644 index 0000000000..a1d5a21cc4 --- /dev/null +++ b/service/lib/agama/dbus/y2dir/modules/InstFunctions.rb @@ -0,0 +1,58 @@ +# Copyright (c) [2022-2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast" + +# :nodoc: +module Yast + # Replacement for the Yast::Package module + # + # @see https://github.com/yast/yast-installation/blob/279b7d108eab24082237cf5e3f02a31f58fef8da/src/modules/InstFunctions.rb + class InstFunctionsClass < Module + def main + puts "Loading mocked module #{__FILE__}" + end + + # @see https://github.com/yast/yast-installation/blob/279b7d108eab24082237cf5e3f02a31f58fef8da/src/modules/InstFunctions.rb#L56 + def ignored_features + [] + end + + # @see https://github.com/yast/yast-installation/blob/279b7d108eab24082237cf5e3f02a31f58fef8da/src/modules/InstFunctions.rb#L83 + def reset_ignored_features; end + + # @see https://github.com/yast/yast-installation/blob/279b7d108eab24082237cf5e3f02a31f58fef8da/src/modules/InstFunctions.rb#L91 + def feature_ignored?(_feature_name) + false + end + + # @see https://github.com/yast/yast-installation/blob/279b7d108eab24082237cf5e3f02a31f58fef8da/src/modules/InstFunctions.rb#L107 + def second_stage_required? + false + end + + # @see https://github.com/yast/yast-installation/blob/279b7d108eab24082237cf5e3f02a31f58fef8da/src/modules/InstFunctions.rb#L137 + def self_update_explicitly_enabled? + false + end + end + + InstFunctions = InstFunctionsClass.new + InstFunctions.main +end diff --git a/service/lib/agama/dbus/y2dir/storage/modules/InstFunctions.rb b/service/lib/agama/dbus/y2dir/storage/modules/InstFunctions.rb new file mode 120000 index 0000000000..42c5d80092 --- /dev/null +++ b/service/lib/agama/dbus/y2dir/storage/modules/InstFunctions.rb @@ -0,0 +1 @@ +../../modules/InstFunctions.rb \ No newline at end of file diff --git a/service/lib/agama/manager.rb b/service/lib/agama/manager.rb index b11b5960cb..48579520c6 100644 --- a/service/lib/agama/manager.rb +++ b/service/lib/agama/manager.rb @@ -152,7 +152,7 @@ def proxy # # @return [DBus::Clients::Locale] def language - @language ||= DBus::Clients::Locale.new + DBus::Clients::Locale.instance end # Users client @@ -205,18 +205,21 @@ def valid? # Collects the logs and stores them into an archive # - # @param user [String] local username who will own archive + # @param path [String] directory where to store logs + # # @return [String] path to created archive - def collect_logs(user) - output = Yast::Execute.locally!("save_y2logs", stderr: :capture) - path = output[/^.* (\/tmp\/y2log-\S*)/, 1] - Yast::Execute.locally!("chown", "#{user}:", path) + def collect_logs(path: nil) + opt = "-d #{path}" unless path.nil? || path.empty? - path + `agama logs store #{opt}`.strip end # Whatever has to be done at the end of installation def finish_installation + logs = collect_logs(path: "/tmp/var/logs/") + + logger.info("Installation logs stored in #{logs}") + cmd = if iguana? "/usr/bin/agamactl -k" else diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb index be47087609..9c2aeb8cf9 100644 --- a/service/lib/agama/software/manager.rb +++ b/service/lib/agama/software/manager.rb @@ -214,6 +214,11 @@ def patterns(filtered) patterns = Y2Packager::Resolvable.find({ kind: :pattern }, preload) patterns = patterns.select(&:user_visible) if filtered + # only display the configured patterns + if product.user_patterns && filtered + patterns.select! { |p| product.user_patterns.include?(p.name) } + end + patterns end diff --git a/service/lib/agama/software/product.rb b/service/lib/agama/software/product.rb index 24bb8c0b2b..9e1179ce3a 100644 --- a/service/lib/agama/software/product.rb +++ b/service/lib/agama/software/product.rb @@ -73,6 +73,11 @@ class Product # @return [Array] attr_accessor :optional_patterns + # Optional user selectable patterns + # + # @return [Array] + attr_accessor :user_patterns + # Product translations. # # @example @@ -94,6 +99,8 @@ def initialize(id) @optional_packages = [] @mandatory_patterns = [] @optional_patterns = [] + # nil = display all visible patterns, [] = display no patterns + @user_patterns = nil @translations = {} end diff --git a/service/lib/agama/software/product_builder.rb b/service/lib/agama/software/product_builder.rb index b4ecfeddf8..c048be304e 100644 --- a/service/lib/agama/software/product_builder.rb +++ b/service/lib/agama/software/product_builder.rb @@ -47,6 +47,7 @@ def build product.optional_packages = data[:optional_packages] product.mandatory_patterns = data[:mandatory_patterns] product.optional_patterns = data[:optional_patterns] + product.user_patterns = data[:user_patterns] product.translations = attrs["translations"] || {} end end @@ -79,6 +80,9 @@ def product_data_from_config(id) ), optional_patterns: config.arch_elements_from( id, "software", "optional_patterns", property: :pattern + ), + user_patterns: config.arch_elements_from( + id, "software", "user_patterns", property: :pattern, default: nil ) } end diff --git a/service/lib/agama/software/proposal.rb b/service/lib/agama/software/proposal.rb index 383e1509d5..c899a1e82f 100644 --- a/service/lib/agama/software/proposal.rb +++ b/service/lib/agama/software/proposal.rb @@ -56,7 +56,7 @@ class Proposal attr_accessor :base_product # @return [Array] List of languages to install - attr_accessor :languages + attr_reader :languages # Constructor # @@ -114,6 +114,13 @@ def valid? !(proposal.nil? || errors?) end + # Sets the languages to install + # + # @param [Array] value Languages in xx_XX format (e.g., "en_US"). + def languages=(value) + @languages = value.map { |l| l.split(".").first }.compact + end + private # @return [Logger] @@ -129,8 +136,12 @@ def initialize_target Yast::Pkg.TargetFinish # ensure that previous target is closed Yast::Pkg.TargetInitialize(Yast::Installation.destdir) Yast::Pkg.TargetLoad - Yast::Pkg.SetAdditionalLocales(languages) - Yast::Pkg.SetSolverFlags("ignoreAlreadyRecommended" => false, "onlyRequires" => true) + + preferred, *additional = languages + Yast::Pkg.SetPackageLocale(preferred) if preferred + Yast::Pkg.SetAdditionalLocales(additional) + + Yast::Pkg.SetSolverFlags("ignoreAlreadyRecommended" => false, "onlyRequires" => false) end # Selects the base product diff --git a/service/package/gem2rpm.yml b/service/package/gem2rpm.yml index f708e08f4d..51654f6372 100644 --- a/service/package/gem2rpm.yml +++ b/service/package/gem2rpm.yml @@ -18,25 +18,52 @@ %global rb_build_versions %{rb_default_ruby} BuildRequires: dbus-1-common Requires: dbus-1-common - Requires: snapper + Requires: suseconnect-ruby-bindings + # YaST dependencies + Requires: autoyast2-installation + # ArchFilter + Requires: yast2 >= 4.5.20 Requires: yast2-bootloader Requires: yast2-country Requires: yast2-hardware-detection Requires: yast2-installation + Requires: yast2-iscsi-client >= 4.5.7 Requires: yast2-network Requires: yast2-proxy # ProposalSettings#swap_reuse Requires: yast2-storage-ng >= 5.0.3 - Requires: open-iscsi - Requires: yast2-iscsi-client >= 4.5.7 Requires: yast2-users - # required for registration - Requires: suseconnect-ruby-bindings - # yast2 with ArchFilter - Requires: yast2 >= 4.5.20 %ifarch s390 s390x Requires: yast2-s390 >= 4.6.4 + Requires: yast2-reipl + Requires: yast2-cio %endif + # Storage dependencies + Requires: bcache-tools + Requires: btrfsprogs + Requires: cryptsetup + Requires: dmraid + Requires: dosfstools + Requires: e2fsprogs + Requires: exfat-utils + Requires: f2fs-tools + Requires: fcoe-utils + Requires: fde-tools + Requires: jfsutils + Requires: libstorage-ng-lang + Requires: lvm2 + Requires: mdadm + Requires: multipath-tools + Requires: nilfs-utils + Requires: nfs-client + Requires: ntfs-3g + Requires: ntfsprogs + Requires: nvme-cli + Requires: open-iscsi + Requires: quota + Requires: snapper + Requires: udftools + Requires: xfsprogs :filelist: "%{_datadir}/dbus-1/agama.conf\n %dir %{_datadir}/dbus-1/agama-services\n %{_datadir}/dbus-1/agama-services/org.opensuse.Agama*.service\n diff --git a/service/package/rubygem-agama.changes b/service/package/rubygem-agama.changes index 2c9d28c3a7..2f9e627bd5 100644 --- a/service/package/rubygem-agama.changes +++ b/service/package/rubygem-agama.changes @@ -1,3 +1,32 @@ +------------------------------------------------------------------- +Tue Dec 5 09:49:10 UTC 2023 - José Iván López González + +- Explicitly add dependencies instead of relying on the live ISO + to provide the required packages (gh#openSUSE/agama/911). + +------------------------------------------------------------------- +Sun Dec 3 15:45:22 UTC 2023 - Imobach Gonzalez Sosa + +- Redefine the InstFunctions module to avoid calling code that + causes unwanted side effects, like resetting the timezone + (gh#openSUSE/agama#903). + +------------------------------------------------------------------- +Sat Dec 2 18:05:37 UTC 2023 - Imobach Gonzalez Sosa + +- Version 6 + +------------------------------------------------------------------- +Wed Nov 29 11:26:39 UTC 2023 - Imobach Gonzalez Sosa + +- Update the software proposal when the locale changes + (gh#openSUSE/agama#881). + +------------------------------------------------------------------- +Fri Nov 24 14:50:22 UTC 2023 - Imobach Gonzalez Sosa + +- Install recommended packages (gh#openSUSE/agama#889). + ------------------------------------------------------------------- Thu Nov 16 16:27:37 UTC 2023 - Ladislav Slezák diff --git a/service/test/agama/dbus/clients/locale_test.rb b/service/test/agama/dbus/clients/locale_test.rb index 431c08515f..c454bc0f68 100644 --- a/service/test/agama/dbus/clients/locale_test.rb +++ b/service/test/agama/dbus/clients/locale_test.rb @@ -39,17 +39,7 @@ let(:dbus_object) { instance_double(DBus::ProxyObject) } let(:lang_iface) { instance_double(DBus::ProxyObjectInterface) } - subject { described_class.new } - - describe "#supported_locales=" do - # Using partial double because methods are dynamically added to the proxy object - let(:dbus_object) { double(DBus::ProxyObject) } - - it "calls the D-Bus object" do - expect(dbus_object).to receive(:supported_locales=).with(["no", "se"]) - subject.supported_locales = ["no", "se"] - end - end + subject { described_class.instance } describe "#finish" do let(:dbus_object) { double(DBus::ProxyObject) } diff --git a/service/test/agama/dbus/manager_service_test.rb b/service/test/agama/dbus/manager_service_test.rb index 8de02632b6..6149a198cf 100644 --- a/service/test/agama/dbus/manager_service_test.rb +++ b/service/test/agama/dbus/manager_service_test.rb @@ -48,7 +48,7 @@ .and_return(object_server) allow(Agama::Manager).to receive(:new).with(config, logger).and_return(manager) allow(Agama::CockpitManager).to receive(:new).and_return(cockpit) - allow(Agama::DBus::Clients::Locale).to receive(:new).and_return(locale_client) + allow(Agama::DBus::Clients::Locale).to receive(:instance).and_return(locale_client) allow(Agama::DBus::Manager).to receive(:new).with(manager, logger).and_return(manager_obj) allow(Agama::DBus::Users).to receive(:new).and_return(users_obj) end diff --git a/service/test/agama/dbus/software/manager_test.rb b/service/test/agama/dbus/software/manager_test.rb index 925218b9d5..ead7fcdc49 100644 --- a/service/test/agama/dbus/software/manager_test.rb +++ b/service/test/agama/dbus/software/manager_test.rb @@ -52,7 +52,7 @@ let(:issues_interface) { Agama::DBus::Interfaces::Issues::ISSUES_INTERFACE } before do - allow(Agama::DBus::Clients::Locale).to receive(:new).and_return(locale_client) + allow(Agama::DBus::Clients::Locale).to receive(:instance).and_return(locale_client) allow(Agama::DBus::Clients::Network).to receive(:new).and_return(network_client) allow(backend).to receive(:probe) allow(backend).to receive(:propose) diff --git a/service/test/agama/manager_test.rb b/service/test/agama/manager_test.rb index 9e465df938..b9e70a30fd 100644 --- a/service/test/agama/manager_test.rb +++ b/service/test/agama/manager_test.rb @@ -65,7 +65,7 @@ before do allow(Agama::Network).to receive(:new).and_return(network) allow(Agama::ProxySetup).to receive(:instance).and_return(proxy) - allow(Agama::DBus::Clients::Locale).to receive(:new).and_return(locale) + allow(Agama::DBus::Clients::Locale).to receive(:instance).and_return(locale) allow(Agama::DBus::Clients::Software).to receive(:new).and_return(software) allow(Agama::DBus::Clients::Storage).to receive(:new).and_return(storage) allow(Agama::Users).to receive(:new).and_return(users) @@ -214,13 +214,12 @@ describe "#collect_logs" do it "collects the logs and returns the path to the archive" do - expect(Yast::Execute).to receive(:locally!) - .with("save_y2logs", stderr: :capture) - .and_return("Saving YaST logs to /tmp/y2log-hWBn95.tar.xz") - expect(Yast::Execute).to receive(:locally!) - .with("chown", "ytm:", /y2log-hWBn95/) + # %x returns the command output including trailing \n + expect(subject).to receive(:`) + .with("agama logs store ") + .and_return("/tmp/y2log-hWBn95.tar.xz\n") - path = subject.collect_logs("ytm") + path = subject.collect_logs expect(path).to eq("/tmp/y2log-hWBn95.tar.xz") end end diff --git a/service/test/agama/software/manager_test.rb b/service/test/agama/software/manager_test.rb index f899ab64b3..1603656c4f 100644 --- a/service/test/agama/software/manager_test.rb +++ b/service/test/agama/software/manager_test.rb @@ -229,12 +229,70 @@ describe "#products" do it "returns the list of known products" do products = subject.products - expect(products.size).to eq(3) + expect(products.size).to eq(4) expect(products).to all(be_a(Agama::Software::Product)) expect(products).to contain_exactly( an_object_having_attributes(id: "ALP-Dolomite"), an_object_having_attributes(id: "Tumbleweed"), - an_object_having_attributes(id: "Leap16") + an_object_having_attributes(id: "MicroOS"), + an_object_having_attributes(id: "MicroOS-Desktop") + ) + end + end + + describe "#patterns" do + it "returns only the specified patterns" do + expect(Y2Packager::Resolvable).to receive(:find).and_return( + [ + double( + arch: "x86_64", + category: "Base Technologies", + description: "YaST tools for installing your system.", + icon: "./yast", + kind: :pattern, + name: "yast2_install_wf", + order: "1240", + source: 0, + summary: "YaST Installation Packages", + user_visible: false, + version: "20220411-1.4" + ), + double( + arch: "x86_64", + category: "Base Technologies", + description: "YaST tools for basic system administration.", + icon: "./yast", + kind: :pattern, + name: "yast2_basis", + order: "1220", + source: 0, + summary: "YaST Base Utilities", + user_visible: true, + version: "20220411-1.4" + ), + double( + arch: "noarch", + category: "Graphical Environments", + description: + "Packages providing the Plasma desktop environment and " \ + "applications from KDE.", + icon: "./pattern-kde", + kind: :pattern, + name: "kde", + order: "1110", + source: 0, + summary: "KDE Applications and Plasma 5 Desktop", + user_visible: true, + version: "20230801-1.1" + ) + ] + ) + + allow(subject.product).to receive(:user_patterns).and_return(["kde"]) + patterns = subject.patterns(true) + + expect(patterns).to contain_exactly( + an_object_having_attributes(name: "kde") ) end end diff --git a/service/test/agama/software/proposal_test.rb b/service/test/agama/software/proposal_test.rb index 8dfdf1cb94..1c3c176589 100644 --- a/service/test/agama/software/proposal_test.rb +++ b/service/test/agama/software/proposal_test.rb @@ -59,8 +59,9 @@ end it "selects the language packages" do + expect(Yast::Pkg).to receive(:SetPackageLocale).with("cs_CZ") expect(Yast::Pkg).to receive(:SetAdditionalLocales).with(["de_DE"]) - subject.languages = ["de_DE"] + subject.languages = ["cs_CZ", "de_DE"] subject.calculate end @@ -162,4 +163,11 @@ end end end + + describe "#languages" do + it "sets the languages to install removing the encoding" do + subject.languages = ["es_ES.UTF-8", "en_US"] + expect(subject.languages).to eq(["es_ES", "en_US"]) + end + end end diff --git a/setup-service.sh b/setup-services.sh similarity index 52% rename from setup-service.sh rename to setup-services.sh index 97c3dcd8af..cc32797f70 100755 --- a/setup-service.sh +++ b/setup-services.sh @@ -1,13 +1,11 @@ #!/bin/sh -x -# Using a git checkout in the current directory, -# set up the service (backend) part of agama -# so that it can be used by -# - the web frontend (as set up by setup.sh) +# Using a git checkout in the current directory and set up the services, so that it can be used by: +# - the web frontend (as set up by setup-web.sh) # - the CLI # or both -# Exit on error; unset variables are an error +# Exit on error; unset variables are an error. set -eu MYDIR=$(realpath $(dirname $0)) @@ -32,26 +30,89 @@ sudosed() { sed -e "$1" "$2" | $SUDO tee "$3" > /dev/null } -# - Install RPM dependencies - -# this repo can be removed once python-language-data reaches Factory -test -f /etc/zypp/repos.d/d_l_python.repo || \ - $SUDO zypper --non-interactive \ - addrepo https://download.opensuse.org/repositories/devel:/languages:/python/openSUSE_Tumbleweed/ d_l_python -$SUDO zypper --non-interactive --gpg-auto-import-keys install gcc gcc-c++ make openssl-devel ruby-devel \ - python-langtable-data git augeas-devel jemalloc-devel awk suseconnect-ruby-bindings || exit 1 - -# only install cargo if it is not available (avoid conflicts with rustup) -which cargo || $SUDO zypper --non-interactive install cargo - -# - Install service rubygem dependencies +# if agama is already running -> stop it +$SUDO systemctl list-unit-files agama.service &>/dev/null && $SUDO systemctl stop agama.service + +# Ruby services + +# Packages required for Ruby development (i.e., bundle install). +$SUDO zypper --non-interactive --gpg-auto-import-keys install \ + gcc \ + gcc-c++ \ + make \ + openssl-devel \ + ruby-devel \ + augeas-devel || exit 1 + +# Packages required by Agama Ruby services (see ./service/package/gem2rpm.yml). +# TODO extract list from gem2rpm.yml +$SUDO zypper --non-interactive --gpg-auto-import-keys install \ + dbus-1-common \ + suseconnect-ruby-bindings \ + autoyast2-installation \ + yast2 \ + yast2-bootloader \ + yast2-country \ + yast2-hardware-detection \ + yast2-installation \ + yast2-iscsi-client \ + yast2-network \ + yast2-proxy \ + yast2-storage-ng \ + yast2-users \ + bcache-tools \ + btrfsprogs \ + cryptsetup \ + dmraid \ + dosfstools \ + e2fsprogs \ + exfat-utils \ + f2fs-tools \ + fcoe-utils \ + fde-tools \ + jfsutils \ + libstorage-ng-lang \ + lvm2 \ + mdadm \ + multipath-tools \ + nilfs-utils \ + nfs-client \ + ntfs-3g \ + ntfsprogs \ + nvme-cli \ + open-iscsi \ + quota \ + snapper \ + udftools \ + xfsprogs || exit 1 + +# Install s390 packages (do not exit on failure). +$SUDO zypper --non-interactive --gpg-auto-import-keys install \ + yast2-s390 \ + yast2-reipl \ + yast2-cio + +# Rubygem dependencies ( cd $MYDIR/service bundle config set --local path 'vendor/bundle' bundle install ) -# - build also rust service +# Rust service, CLI and auto-installation. + +# Only install cargo if it is not available (avoid conflicts with rustup) +which cargo || $SUDO zypper --non-interactive install cargo + +# Packages required by Rust code (see ./rust/package/agama-cli.spec) +$SUDO zypper --non-interactive install \ + bzip2 \ + jsonnet \ + lshw \ + python-langtable-data \ + tar \ + xkeyboard-config-lang || exit 1 + ( cd $MYDIR/rust cargo build @@ -70,7 +131,7 @@ $SUDO cp -v $MYDIR/service/share/dbus.conf /usr/share/dbus-1/agama.conf # cleanup previous installation [[ -d $DBUSDIR ]] && $SUDO rm -r $DBUSDIR - # create services + # create services $SUDO mkdir -p $DBUSDIR for SVC in org.opensuse.Agama*.service; do sudosed "s@\(Exec\)=/usr/bin/@\1=$MYDIR/service/bin/@" $SVC $DBUSDIR/$SVC diff --git a/setup-web.sh b/setup-web.sh new file mode 100755 index 0000000000..7e07af57a5 --- /dev/null +++ b/setup-web.sh @@ -0,0 +1,29 @@ +#!/bin/sh -x + +# Exit on error; unset variables are an error. +set -eu + +MYDIR=$(realpath $(dirname $0)) + +# Helper: +# Ensure root privileges for the installation. +# In a testing container, we are root but there is no sudo. +if [ $(id --user) != 0 ]; then + SUDO=sudo + if [ $($SUDO id --user) != 0 ]; then + echo "We are not root and cannot sudo, cannot continue." + exit 1 + fi +else + SUDO="" +fi + +$SUDO zypper --non-interactive --gpg-auto-import-keys install \ + make \ + 'npm>=18' \ + cockpit || exit 1 + +$SUDO systemctl start cockpit + +cd web; make devel-install; cd - +$SUDO ln -snf `pwd`/web/dist /usr/share/cockpit/agama diff --git a/setup.sh b/setup.sh index 2f4bcaeb24..16d446444b 100755 --- a/setup.sh +++ b/setup.sh @@ -1,9 +1,9 @@ #!/bin/sh -# This script sets up the development environment without installing any -# package. This script is supposed to run within a repository clone. +# This script sets up the development environment without installing Agama packages. This script is +# supposed to run within a repository clone. -# Exit on error; unset variables are an error +# Exit on error; unset variables are an error. set -eu MYDIR=$(realpath $(dirname $0)) @@ -21,24 +21,23 @@ else SUDO="" fi -# Backend setup +# Services setup +if ! $MYDIR/setup-services.sh; then + echo "Services setup failed." + echo "Agama services are NOT running." -$MYDIR/setup-service.sh + exit 2 +fi; -# Install Frontend dependencies +# Web setup +if ! $MYDIR/setup-web.sh; then + echo "Web client setup failed." + echo "Agama web client is NOT running." -$SUDO zypper --non-interactive --gpg-auto-import-keys install \ - make git 'npm>=18' cockpit + exit 3 +fi; -# Web Frontend - -$SUDO systemctl start cockpit - -# set up the web UI -cd web; make devel-install; cd - -$SUDO ln -snf `pwd`/web/dist /usr/share/cockpit/agama - -# Start the installer +# Start the installer. echo echo "D-Bus will start the services, see journalctl for their logs." echo "To start the services manually, logging to the terminal:" diff --git a/web/cspell.json b/web/cspell.json index fa98f5ff09..428bf4ef41 100644 --- a/web/cspell.json +++ b/web/cspell.json @@ -43,6 +43,8 @@ "ipaddr", "iscsi", "jdoe", + "keymap", + "keymaps", "libyui", "lldp", "localdomain", diff --git a/web/package-lock.json b/web/package-lock.json index fed95a9d64..a587f2ad6c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -104,9 +104,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", - "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", + "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", "dev": true }, "node_modules/@ampproject/remapping": { @@ -123,12 +123,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -136,30 +136,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", - "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", - "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", + "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.3", + "@babel/helpers": "^7.23.5", + "@babel/parser": "^7.23.5", "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -193,12 +193,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.3.tgz", - "integrity": "sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", + "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", "dev": true, "dependencies": { - "@babel/types": "^7.23.3", + "@babel/types": "^7.23.5", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -248,17 +248,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.5.tgz", + "integrity": "sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" @@ -472,9 +472,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -490,9 +490,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, "engines": { "node": ">=6.9.0" @@ -513,23 +513,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.2.tgz", - "integrity": "sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", + "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", "dev": true, "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", @@ -541,9 +541,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz", - "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", + "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -921,9 +921,9 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.3.tgz", - "integrity": "sha512-59GsVNavGxAXCDDbakWSMJhajASb4kBCqDjqJsv+p5nKdbz7istmZ3HrX3L2LuiI80+zsOADCvooqQH3qGCucQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.4.tgz", + "integrity": "sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -971,9 +971,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.3.tgz", - "integrity": "sha512-QPZxHrThbQia7UdvfpaRRlq/J9ciz1J4go0k+lPBXbgaNeY7IQrBj/9ceWjvMMI07/ZBzHl/F0R/2K0qH7jCVw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1002,9 +1002,9 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.3.tgz", - "integrity": "sha512-PENDVxdr7ZxKPyi5Ffc0LjXdnJyrJxyqF5T5YjlVg4a0VFfQHW0r8iAtRiDXkfHlu1wwcvdtnndGYIeJLSuRMQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.22.15", @@ -1019,9 +1019,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.3.tgz", - "integrity": "sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz", + "integrity": "sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -1104,9 +1104,9 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.3.tgz", - "integrity": "sha512-vTG+cTGxPFou12Rj7ll+eD5yWeNl5/8xvQvF08y5Gv3v4mZQoyFf8/n9zg4q5vvCWt5jmgymfzMAldO7orBn7A==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1136,9 +1136,9 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.3.tgz", - "integrity": "sha512-yCLhW34wpJWRdTxxWtFZASJisihrfyMOTOQexhVzA78jlU+dH7Dw+zQgcPepQ5F3C6bAIiblZZ+qBggJdHiBAg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1184,9 +1184,9 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.3.tgz", - "integrity": "sha512-H9Ej2OiISIZowZHaBwF0tsJOih1PftXJtE8EWqlEIwpc7LMTGq0rPOrywKLQ4nefzx8/HMR0D3JGXoMHYvhi0A==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1215,9 +1215,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.3.tgz", - "integrity": "sha512-+pD5ZbxofyOygEp+zZAfujY2ShNCXRpDRIPOiBmTO693hhyOEteZgl876Xs9SAHPQpcV0vz8LvA/T+w8AzyX8A==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1344,9 +1344,9 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.3.tgz", - "integrity": "sha512-xzg24Lnld4DYIdysyf07zJ1P+iIfJpxtVFOzX4g+bsJ3Ng5Le7rXx9KwqKzuyaUeRnt+I1EICwQITqc0E2PmpA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1360,9 +1360,9 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.3.tgz", - "integrity": "sha512-s9GO7fIBi/BLsZ0v3Rftr6Oe4t0ctJ8h4CCXfPoEJwmvAPMyNrfkOOJzm6b9PX9YXcCJWWQd/sBF/N26eBiMVw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1376,9 +1376,9 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.3.tgz", - "integrity": "sha512-VxHt0ANkDmu8TANdE9Kc0rndo/ccsmfe2Cx2y5sI4hu3AukHQ5wAu4cM7j3ba8B9548ijVyclBU+nuDQftZsog==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", + "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", "dev": true, "dependencies": { "@babel/compat-data": "^7.23.3", @@ -1411,9 +1411,9 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.3.tgz", - "integrity": "sha512-LxYSb0iLjUamfm7f1D7GpiS4j0UAC8AOiehnsGAP8BEsIX8EOi3qV6bbctw8M7ZvLtcoZfZX5Z7rN9PlWk0m5A==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1427,9 +1427,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.3.tgz", - "integrity": "sha512-zvL8vIfIUgMccIAK1lxjvNv572JHFJIKb4MWBz5OGdBQA0fB0Xluix5rmOby48exiJc987neOmP/m9Fnpkz3Tg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1475,9 +1475,9 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.3.tgz", - "integrity": "sha512-a5m2oLNFyje2e/rGKjVfAELTVI5mbA0FeZpBnkOWWV7eSmKQ+T/XW0Vf+29ScLzSxX+rnsarvU0oie/4m6hkxA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -1538,16 +1538,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.15.tgz", - "integrity": "sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", + "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/types": "^7.22.15" + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/types": "^7.23.4" }, "engines": { "node": ">=6.9.0" @@ -1695,13 +1695,13 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.3.tgz", - "integrity": "sha512-ogV0yWnq38CFwH20l2Afz0dfKuZBx9o/Y2Rmh5vuSS0YD1hswgEgTfyTzuSrT2q9btmHRSqYoSfwFUVaC1M1Jw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.5.tgz", + "integrity": "sha512-2fMkXEJkrmwgu2Bsv1Saxgj30IXZdJ+84lQcKKI7sm719oXs0BBw2ZENKdJdR1PjWndgLCEBNXJOri0fk7RYQA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-create-class-features-plugin": "^7.23.5", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-typescript": "^7.23.3" }, @@ -1776,15 +1776,15 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.3.tgz", - "integrity": "sha512-ovzGc2uuyNfNAs/jyjIGxS8arOHS5FENZaNn4rtE7UdKMMkqHCvboHfcuhWLZNX5cB44QfcGNWjaevxMzzMf+Q==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.5.tgz", + "integrity": "sha512-0d/uxVD6tFGWXGDSfyMD1p2otoaKmu6+GD+NfAx0tMaH+dxORnp7T9TaVQ6mKyya7iBtCIVxHjWT7MuzzM9z+A==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.3", + "@babel/compat-data": "^7.23.5", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", + "@babel/helper-validator-option": "^7.23.5", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3", @@ -1808,25 +1808,25 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.4", "@babel/plugin-transform-async-to-generator": "^7.23.3", "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.3", - "@babel/plugin-transform-classes": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.5", "@babel/plugin-transform-computed-properties": "^7.23.3", "@babel/plugin-transform-destructuring": "^7.23.3", "@babel/plugin-transform-dotall-regex": "^7.23.3", "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", "@babel/plugin-transform-for-of": "^7.23.3", "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", "@babel/plugin-transform-member-expression-literals": "^7.23.3", "@babel/plugin-transform-modules-amd": "^7.23.3", "@babel/plugin-transform-modules-commonjs": "^7.23.3", @@ -1834,15 +1834,15 @@ "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.3", - "@babel/plugin-transform-numeric-separator": "^7.23.3", - "@babel/plugin-transform-object-rest-spread": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.23.4", "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.3", - "@babel/plugin-transform-optional-chaining": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", "@babel/plugin-transform-parameters": "^7.23.3", "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", "@babel/plugin-transform-property-literals": "^7.23.3", "@babel/plugin-transform-regenerator": "^7.23.3", "@babel/plugin-transform-reserved-words": "^7.23.3", @@ -1929,9 +1929,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", + "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1955,19 +1955,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.3.tgz", - "integrity": "sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", + "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/parser": "^7.23.5", + "@babel/types": "^7.23.5", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1976,12 +1976,12 @@ } }, "node_modules/@babel/types": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.3.tgz", - "integrity": "sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", + "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, @@ -1996,16 +1996,16 @@ "dev": true }, "node_modules/@cspell/cspell-bundled-dicts": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.0.0.tgz", - "integrity": "sha512-Phbb1ij1TQQuqxuuvxf5P6fvV9U+EVoATNLmDqFHvRZfUyuhgbJuCMzIPeBx4GfTTDWlPs51FYRvZ/Q8xBHsyA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-8.1.0.tgz", + "integrity": "sha512-o/R/kR1QO9SQV2hUroaguTlHD6MDDtrVY6Xj5eG0loM7T0Pm3TEdlGYQ0LP6O9/CfUiHTntIFUM+PJ999+LuHQ==", "dev": true, "dependencies": { "@cspell/dict-ada": "^4.0.2", "@cspell/dict-aws": "^4.0.0", "@cspell/dict-bash": "^4.1.2", - "@cspell/dict-companies": "^3.0.27", - "@cspell/dict-cpp": "^5.0.9", + "@cspell/dict-companies": "^3.0.28", + "@cspell/dict-cpp": "^5.0.10", "@cspell/dict-cryptocurrencies": "^4.0.0", "@cspell/dict-csharp": "^4.0.2", "@cspell/dict-css": "^4.0.12", @@ -2014,16 +2014,16 @@ "@cspell/dict-docker": "^1.1.7", "@cspell/dict-dotnet": "^5.0.0", "@cspell/dict-elixir": "^4.0.3", - "@cspell/dict-en_us": "^4.3.11", + "@cspell/dict-en_us": "^4.3.12", "@cspell/dict-en-common-misspellings": "^1.0.2", "@cspell/dict-en-gb": "1.1.33", - "@cspell/dict-filetypes": "^3.0.2", + "@cspell/dict-filetypes": "^3.0.3", "@cspell/dict-fonts": "^4.0.0", "@cspell/dict-fsharp": "^1.0.1", "@cspell/dict-fullstack": "^3.1.5", "@cspell/dict-gaming-terms": "^1.0.4", "@cspell/dict-git": "^2.0.0", - "@cspell/dict-golang": "^6.0.4", + "@cspell/dict-golang": "^6.0.5", "@cspell/dict-haskell": "^4.0.1", "@cspell/dict-html": "^4.0.5", "@cspell/dict-html-symbol-entities": "^4.0.0", @@ -2034,7 +2034,7 @@ "@cspell/dict-lua": "^4.0.2", "@cspell/dict-makefile": "^1.0.0", "@cspell/dict-node": "^4.0.3", - "@cspell/dict-npm": "^5.0.12", + "@cspell/dict-npm": "^5.0.13", "@cspell/dict-php": "^4.0.4", "@cspell/dict-powershell": "^5.0.2", "@cspell/dict-public-licenses": "^2.0.5", @@ -2043,7 +2043,7 @@ "@cspell/dict-ruby": "^5.0.1", "@cspell/dict-rust": "^4.0.1", "@cspell/dict-scala": "^5.0.0", - "@cspell/dict-software-terms": "^3.3.9", + "@cspell/dict-software-terms": "^3.3.11", "@cspell/dict-sql": "^2.1.2", "@cspell/dict-svelte": "^1.0.2", "@cspell/dict-swift": "^2.0.1", @@ -2055,51 +2055,51 @@ } }, "node_modules/@cspell/cspell-json-reporter": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.0.0.tgz", - "integrity": "sha512-1ltK5N4xMGWjDSIkU+GJd3rXV8buXgO/lAgnpM1RhKWqAmG+u0k6pnhk2vIo/4qZQpgfK0l3J3h/Ky2FcE95vA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-8.1.0.tgz", + "integrity": "sha512-Iss9dq5XBc5wYADv/Z59W4DgRQYs8BSHNVD6+LbQctuqmeJAte426/oi4x0Y76AJtEe0N6BZouj8HXykovwP5w==", "dev": true, "dependencies": { - "@cspell/cspell-types": "8.0.0" + "@cspell/cspell-types": "8.1.0" }, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-pipe": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.0.0.tgz", - "integrity": "sha512-1MH+9q3AmbzwK1BYhSGla8e4MAAYzzPApGvv8eyv0rWDmgmDTkGqJPTTvYj1wFvll5ximQ5OolpPQGv3JoWvtQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-8.1.0.tgz", + "integrity": "sha512-HDNX7MFAPAJ9acyYBa1bG+P4WiHHMFNYeywYBf3h6ScVhHobAqnhqS6b8R7MVhVRivwnKIQPG3zK7UpcwfyRcw==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-resolver": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.0.0.tgz", - "integrity": "sha512-gtALHFLT2vSZ7BZlIg26AY3W9gkiqxPGE75iypWz06JHJs05ngnAR+h6VOu0+rmgx98hNfzPPEh4g+Tjm8Ma0A==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-8.1.0.tgz", + "integrity": "sha512-nlppKh2o6g0zz+oIQ/dZB+oFQFf8lvn3mJKBpDwoeQY7/o9ZORPibXjtqXM83OhhdpoUVuk+3RFMsnFBBffa2Q==", "dev": true, "dependencies": { - "global-dirs": "^3.0.1" + "global-directory": "^4.0.1" }, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-service-bus": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-8.0.0.tgz", - "integrity": "sha512-1EYhIHoZnhxpfEp6Bno6yVWYBuYfaQrwIfeDMntnezUcSmi7RyroQEcp5U7sLv69vhRD2c81o7r8iUaAbPSmIg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-8.1.0.tgz", + "integrity": "sha512-9Enayhkef732f15kHgiUe4QKyJgKk1dcZ4EFq4eyzUUDFF/eBv6qTQo5k2juUhPIjaKosqqMBHg4ffXcpkhr+Q==", "dev": true, "engines": { "node": ">=18" } }, "node_modules/@cspell/cspell-types": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.0.0.tgz", - "integrity": "sha512-dPdxQI8dLJoJEjylaPYfCJNnm2XNMYPuowHE2FMcsnFR9hEchQAhnKVc/aD63IUYnUtUrPxPlUJdoAoj569e+g==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-8.1.0.tgz", + "integrity": "sha512-1SxBjQdZtVjrTs3Ftw5I3nNpuDjdpsFMvfbbt6EnxqMpmZiUwkqxLCKla0pEy5R9CZcFFlntlOTMTmNsIkgmWg==", "dev": true, "engines": { "node": ">=18" @@ -2124,15 +2124,15 @@ "dev": true }, "node_modules/@cspell/dict-companies": { - "version": "3.0.27", - "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.0.27.tgz", - "integrity": "sha512-gaPR/luf+4oKGyxvW4GbxGGPdHiC5kj/QefnmQqrLFrLiCSXMZg5/NL+Lr4E5lcHsd35meX61svITQAvsT7lyQ==", + "version": "3.0.28", + "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.0.28.tgz", + "integrity": "sha512-UinHkMYB/1pUkLKm1PGIm9PBFYxeAa6YvbB1Rq/RAAlrs0WDwiDBr3BAYdxydukG1IqqwT5z9WtU+8D/yV/5lw==", "dev": true }, "node_modules/@cspell/dict-cpp": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-5.0.9.tgz", - "integrity": "sha512-ql9WPNp8c+fhdpVpjpZEUWmxBHJXs9CJuiVVfW/iwv5AX7VuMHyEwid+9/6nA8qnCxkUQ5pW83Ums1lLjn8ScA==", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-5.0.10.tgz", + "integrity": "sha512-WCRuDrkFdpmeIR6uXQYKU9loMQKNFS4bUhtHdv5fu4qVyJSh3k/kgmtTm1h1BDTj8EwPRc/RGxS+9Z3b2mnabA==", "dev": true }, "node_modules/@cspell/dict-cryptocurrencies": { @@ -2190,9 +2190,9 @@ "dev": true }, "node_modules/@cspell/dict-en_us": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.3.11.tgz", - "integrity": "sha512-GhdavZFlS2YbUNcRtPbgJ9j6aUyq116LmDQ2/Q5SpQxJ5/6vVs8Yj5WxV1JD+Zh/Zim1NJDcneTOuLsUGi+Czw==", + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.3.12.tgz", + "integrity": "sha512-1bsUxFjgxF30FTzcU5uvmCvH3lyqVKR9dbwsJhomBlUM97f0edrd6590SiYBXDm7ruE68m3lJd4vs0Ev2D6FtQ==", "dev": true }, "node_modules/@cspell/dict-en-common-misspellings": { @@ -2208,9 +2208,9 @@ "dev": true }, "node_modules/@cspell/dict-filetypes": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.2.tgz", - "integrity": "sha512-StoC0wPmFNav6F6P8/FYFN1BpZfPgOmktb8gQ9wTauelWofPeBW+A0t5ncZt9hXHtnbGDA98v4ukacV+ucbnUg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.3.tgz", + "integrity": "sha512-J9UP+qwwBLfOQ8Qg9tAsKtSY/WWmjj21uj6zXTI9hRLD1eG1uUOLcfVovAmtmVqUWziPSKMr87F6SXI3xmJXgw==", "dev": true }, "node_modules/@cspell/dict-fonts": { @@ -2244,9 +2244,9 @@ "dev": true }, "node_modules/@cspell/dict-golang": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@cspell/dict-golang/-/dict-golang-6.0.4.tgz", - "integrity": "sha512-jOfewPEyN6U9Q80okE3b1PTYBfqZgHh7w4o271GSuAX+VKJ1lUDhdR4bPKRxSDdO5jHArw2u5C8nH2CWGuygbQ==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@cspell/dict-golang/-/dict-golang-6.0.5.tgz", + "integrity": "sha512-w4mEqGz4/wV+BBljLxduFNkMrd3rstBNDXmoX5kD4UTzIb4Sy0QybWCtg2iVT+R0KWiRRA56QKOvBsgXiddksA==", "dev": true }, "node_modules/@cspell/dict-haskell": { @@ -2310,9 +2310,9 @@ "dev": true }, "node_modules/@cspell/dict-npm": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.0.12.tgz", - "integrity": "sha512-T/+WeQmtbxo7ad6hrdI8URptYstKJP+kXyWJZfuVJJGWJQ7yubxrI5Z5AfM+Dh/ff4xHmdzapxD9adaEQ727uw==", + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.0.13.tgz", + "integrity": "sha512-uPb3DlQA/FvlmzT5RjZoy7fy91mxMRZW1B+K3atVM5A/cmP1QlDaSW/iCtde5kHET1MOV7uxz+vy0Yha2OI5pQ==", "dev": true }, "node_modules/@cspell/dict-php": { @@ -2367,9 +2367,9 @@ "dev": true }, "node_modules/@cspell/dict-software-terms": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-3.3.9.tgz", - "integrity": "sha512-/O3EWe0SIznx18S7J3GAXPDe7sexn3uTsf4IlnGYK9WY6ZRuEywkXCB+5/USLTGf4+QC05pkHofphdvVSifDyA==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-3.3.11.tgz", + "integrity": "sha512-a2Zml4G47dbQ6GDdN7+YlIWs3nFnIcJkZOLT88m/LzxjApiF7AOZLqQiKwow03hyvGSuZy8itgQZmQHoPlw2vQ==", "dev": true }, "node_modules/@cspell/dict-sql": { @@ -2403,21 +2403,21 @@ "dev": true }, "node_modules/@cspell/dynamic-import": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-8.0.0.tgz", - "integrity": "sha512-HNkCepopgiEGuI1QGA6ob4+ayvoSMxvAqetLxP0u1sZzc50LH2DEWwotcNrpVdzZOtERHvIBcGaQKIBEx8pPRQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-8.1.0.tgz", + "integrity": "sha512-TJ1OnP0ubdVr5YTMU15rVs8R6ROuPvP/Z5lY2gtHscEsf9tZxvIt3924uMc9fTJXgNsITNWSoCzgwJYcDvGM6A==", "dev": true, "dependencies": { - "import-meta-resolve": "^3.1.1" + "import-meta-resolve": "^4.0.0" }, "engines": { "node": ">=18.0" } }, "node_modules/@cspell/strong-weak-map": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-8.0.0.tgz", - "integrity": "sha512-fRlqPSdpdub52vFtulDgLPzGPGe75I04ScId1zOO9ABP7/ro8VmaG//m1k7hsPkm6h7FG4jWympoA3aXDAcXaA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-8.1.0.tgz", + "integrity": "sha512-yBc3ejGpx3QLbfS+Sec8ycS+lKuou5rnnpfz3aVBCnNHUPozosFuNYPFB6Iah2CBY6v6rkDCkIp5vnp1IwQzdA==", "dev": true, "engines": { "node": ">=18" @@ -2555,9 +2555,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -2645,9 +2645,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3572,9 +3572,9 @@ } }, "node_modules/@jsdoc/salty": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz", - "integrity": "sha512-aA+awb5yoml8TQ3CzI5Ue7sM3VMRC4l1zJJW4fgZ8OCL1wshJZhNzaf0PL85DSnOUw6QuFgeHGD/eq/xwwAF2g==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.7.tgz", + "integrity": "sha512-mh8LbS9d4Jq84KLw8pzho7XC2q2/IJGiJss3xwRoLD1A+EE16SjN4PfaG4jRCzKegTFLlN0Zd8SdUPE6XdoPFg==", "dev": true, "dependencies": { "lodash": "^4.17.21" @@ -3590,9 +3590,9 @@ "dev": true }, "node_modules/@material-symbols/svg-400": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@material-symbols/svg-400/-/svg-400-0.14.0.tgz", - "integrity": "sha512-sbK2DZAIcLderTE1uSLvRFjoDzdiPrtmQg/NJAaKzKqxWjvhqMZjbrd5mAdzHPlTkuzM548foNXs0gcaREONSg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-400/-/svg-400-0.14.1.tgz", + "integrity": "sha512-ZI3fbSZ0PCT0XEsSTICY5qfvoxEQmgYrN+C49m116f1e7u7XEMz9VV6TwWmfR0blTcByRVMoYFO3kLl1McVQDQ==" }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", @@ -3747,9 +3747,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", - "integrity": "sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.1.tgz", + "integrity": "sha512-so+DHzZKsoOcoXrILB4rqDkMDy7NLMErRdOxvzvOKb507YINKUP4Di+shbTZDhSE/pBZ+vr7XGIpcOO0VLSA+Q==", "engines": { "node": ">=14.0.0" } @@ -4125,9 +4125,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.1.4.tgz", - "integrity": "sha512-wpoYrCYwSZ5/AxcrjLxJmCU6I5QAJXslEeSiMQqaWmP2Kzpd1LvF/qxmAIW2qposULGWq2gw30GgVNFLSc2Jnw==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.1.5.tgz", + "integrity": "sha512-3y04JLW+EceVPy2Em3VwNr95dOKqA8DhR0RJHhHKDZNYXcVXnEK7WIrpj4eYU8SVt/qYZ2aRWt/WgQ+grNES8g==", "dev": true, "dependencies": { "@adobe/css-tools": "^4.3.1", @@ -4233,9 +4233,9 @@ } }, "node_modules/@testing-library/react": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.1.0.tgz", - "integrity": "sha512-hcvfZEEyO0xQoZeHmUbuMs7APJCGELpilL7bY+BaJaMP57aWc6q1etFwScnoZDheYjk4ESdlzPdQ33IbsKAK/A==", + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.1.2.tgz", + "integrity": "sha512-z4p7DVBTPjKM5qDZ0t5ZjzkpSNb+fZy1u6bzO7kk8oeGagpPCAtgh4cx1syrfp7a+QWkM021jGqjJaxJJnXAZg==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -4288,9 +4288,9 @@ "dev": true }, "node_modules/@types/babel__core": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.4.tgz", - "integrity": "sha512-mLnSC22IC4vcWiuObSRjrLd9XcBTGf59vUSoq2jkQDJ/QQ8PMI9rSuzE+aEV8karUMbskw07bKYoUJCKTUaygg==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "dependencies": { "@babel/parser": "^7.20.7", @@ -4357,9 +4357,9 @@ } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.3.tgz", - "integrity": "sha512-6mfQ6iNvhSKCZJoY6sIG3m0pKkdUcweVNOLuBBKvoWGzl2yRxOJcYOTRyLKt3nxXvBLJWa6QkW//tgbIwJehmA==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, "dependencies": { "@types/express-serve-static-core": "*", @@ -4367,9 +4367,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.44.7", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz", - "integrity": "sha512-f5ORu2hcBbKei97U73mf+l9t4zTGl74IqZ0GQk4oVea/VS8tQZYkUveSYojk+frraAVYId0V2WC9O4PTNru2FQ==", + "version": "8.44.8", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.8.tgz", + "integrity": "sha512-4K8GavROwhrYl2QXDXm0Rv9epkA8GBFu0EI+XrrnnuCl7u8CWBRusX7fXJfanhZTDWSAL24gDI/UqXyUM0Injw==", "dev": true, "dependencies": { "@types/estree": "*", @@ -4471,9 +4471,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.8", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", - "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", + "version": "29.5.10", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", + "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -4570,18 +4570,18 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", - "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "version": "20.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", + "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/node-forge": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.9.tgz", - "integrity": "sha512-meK88cx/sTalPSLSoCzkiUB4VPIFHmxtXm5FaaqRDqBX2i/Sy8bJ4odsan0b20RBjPh06dAQ+OTTdnyQyhJZyQ==", + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz", + "integrity": "sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==", "dev": true, "dependencies": { "@types/node": "*" @@ -4594,9 +4594,9 @@ "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.10", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz", - "integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==", + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", "dev": true }, "node_modules/@types/qs": { @@ -4612,9 +4612,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.37", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.37.tgz", - "integrity": "sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==", + "version": "18.2.41", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.41.tgz", + "integrity": "sha512-CwOGr/PiLiNBxEBqpJ7fO3kocP/2SSuC9fpH5K7tusrg4xPSRT/193rzolYwQnTN02We/ATXKnb6GqA5w4fRxw==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -4623,9 +4623,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.15", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.15.tgz", - "integrity": "sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==", + "version": "18.2.17", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", + "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", "dev": true, "dependencies": { "@types/react": "*" @@ -4638,15 +4638,15 @@ "dev": true }, "node_modules/@types/scheduler": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.6.tgz", - "integrity": "sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", "dev": true }, "node_modules/@types/semver": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", - "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/send": { @@ -4701,18 +4701,18 @@ "dev": true }, "node_modules/@types/ws": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", - "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/yargs": { - "version": "17.0.31", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.31.tgz", - "integrity": "sha512-bocYSx4DI8TmdlvxqGpVNXOgCNR1Jj0gNPhhAY+iz1rgKDAaYrAYdFYnhDV1IFuiuVc9HkOwyDcFxaTElF3/wg==", + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, "dependencies": { "@types/yargs-parser": "*" @@ -4725,16 +4725,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.11.0.tgz", - "integrity": "sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/type-utils": "6.11.0", - "@typescript-eslint/utils": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -4793,15 +4793,15 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.11.0.tgz", - "integrity": "sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/typescript-estree": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4" }, "engines": { @@ -4821,13 +4821,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz", - "integrity": "sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0" + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -4838,13 +4838,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz", - "integrity": "sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.11.0", - "@typescript-eslint/utils": "6.11.0", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -4865,9 +4865,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.11.0.tgz", - "integrity": "sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -4878,13 +4878,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz", - "integrity": "sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4938,17 +4938,17 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.11.0.tgz", - "integrity": "sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/typescript-estree": "6.11.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", "semver": "^7.5.4" }, "engines": { @@ -4996,12 +4996,12 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz", - "integrity": "sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", + "@typescript-eslint/types": "6.13.1", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -5236,6 +5236,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true }, "node_modules/accepts": { @@ -5997,9 +5998,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "funding": [ { @@ -6016,9 +6017,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -6206,9 +6207,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001562", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001562.tgz", - "integrity": "sha512-kfte3Hym//51EdX4239i+Rmp20EsLIYGdPkERegTgU19hQWCRhsRFGKHTliUlsry53tv17K7n077Kqa0WJU4ng==", + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", "dev": true, "funding": [ { @@ -6357,9 +6358,9 @@ "dev": true }, "node_modules/clean-css": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", - "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "dev": true, "dependencies": { "source-map": "~0.6.0" @@ -6784,9 +6785,9 @@ } }, "node_modules/core-js-compat": { - "version": "3.33.2", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.2.tgz", - "integrity": "sha512-axfo+wxFVxnqf8RvxTzoAlzW4gRoacrHeoFlc9n0x50+7BEyZL/Rt3hicaED1/CEd7I6tPCPVUYcJwCMO5XUYw==", + "version": "3.33.3", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.3.tgz", + "integrity": "sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==", "dev": true, "dependencies": { "browserslist": "^4.22.1" @@ -6797,9 +6798,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.33.2", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.33.2.tgz", - "integrity": "sha512-a8zeCdyVk7uF2elKIGz67AjcXOxjRbwOLz8SbklEso1V+2DoW4OkAMZN9S9GBgvZIaqQi/OemFX4OiSoQEmg1Q==", + "version": "3.33.3", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.33.3.tgz", + "integrity": "sha512-taJ00IDOP+XYQEA2dAe4ESkmHt1fL8wzYDo3mRWQey8uO9UojlBFMneA65kMyxfYP7106c6LzWaq7/haDT6BCQ==", "dev": true, "hasInstallScript": true, "funding": { @@ -6990,25 +6991,25 @@ } }, "node_modules/cspell": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.0.0.tgz", - "integrity": "sha512-Nayy25Dh+GAlDFDpVZaQhmidP947rpj1Pn9lmZ3nUFjD9W/yj0h0vrjMLMN4dbonddkmKh4t51C+7NuMP405hg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cspell/-/cspell-8.1.0.tgz", + "integrity": "sha512-oxQLyhW3yIAfvDdtoobvriWqfWVqOBo1o+WWRxlDyJdKDBH6my++p6KU3ZjxcJb7VG+CRLGfU7zASWwTPxMXRA==", "dev": true, "dependencies": { - "@cspell/cspell-json-reporter": "8.0.0", - "@cspell/cspell-pipe": "8.0.0", - "@cspell/cspell-types": "8.0.0", - "@cspell/dynamic-import": "8.0.0", + "@cspell/cspell-json-reporter": "8.1.0", + "@cspell/cspell-pipe": "8.1.0", + "@cspell/cspell-types": "8.1.0", + "@cspell/dynamic-import": "8.1.0", "chalk": "^5.3.0", "chalk-template": "^1.1.0", "commander": "^11.1.0", - "cspell-gitignore": "8.0.0", - "cspell-glob": "8.0.0", - "cspell-io": "8.0.0", - "cspell-lib": "8.0.0", + "cspell-gitignore": "8.1.0", + "cspell-glob": "8.1.0", + "cspell-io": "8.1.0", + "cspell-lib": "8.1.0", "fast-glob": "^3.3.2", "fast-json-stable-stringify": "^2.1.0", - "file-entry-cache": "^7.0.1", + "file-entry-cache": "^7.0.2", "get-stdin": "^9.0.0", "semver": "^7.5.4", "strip-ansi": "^7.1.0", @@ -7025,36 +7026,44 @@ "url": "https://github.com/streetsidesoftware/cspell?sponsor=1" } }, + "node_modules/cspell-config-lib": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-8.1.0.tgz", + "integrity": "sha512-mIv8etMAp05OapdxJQt0nkfzclMti8AfACPryWnVePrwB89A2KjErHYBa7hX6gn20B4K+KgD7ckPcOi6L8vLYA==", + "dev": true, + "dependencies": { + "@cspell/cspell-types": "8.1.0", + "comment-json": "^4.2.3", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/cspell-dictionary": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-8.0.0.tgz", - "integrity": "sha512-R/AzUj7W7F4O4fAOL8jvIiUqPYGy6jIBlDkxO9SZe/A6D2kOICZZzGSXMZ0M7OKYqxc6cioQUMKOJsLkDXfDXw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-8.1.0.tgz", + "integrity": "sha512-nwvlPiM7jsZThZ2bUS2CYzqwAbxWC4OL5GozQfbGEwW/8unNhifBpJzlOZuzLyX4Vu94ETExeIc625wBqPWjVA==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "8.0.0", - "@cspell/cspell-types": "8.0.0", - "cspell-trie-lib": "8.0.0", - "fast-equals": "^4.0.3", + "@cspell/cspell-pipe": "8.1.0", + "@cspell/cspell-types": "8.1.0", + "cspell-trie-lib": "8.1.0", + "fast-equals": "^5.0.1", "gensequence": "^6.0.0" }, "engines": { "node": ">=18" } }, - "node_modules/cspell-dictionary/node_modules/fast-equals": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", - "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", - "dev": true - }, "node_modules/cspell-gitignore": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.0.0.tgz", - "integrity": "sha512-Uv+ENdUm+EXwQuG9187lKmE1t8b2KW+6VaQHP7r01WiuhkwhfzmWA7C30iXVcwRcsMw07wKiWvMEtG6Zlzi6lQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-8.1.0.tgz", + "integrity": "sha512-upMIEjbBz1g92Vt80h2hMMRZ9057iAmCWxi05l0WrwGrtc3CGsA8gQQIFIbVZ0x86Sbmv1cBZms1Y/hKWPWuvg==", "dev": true, "dependencies": { - "cspell-glob": "8.0.0", - "find-up": "^5.0.0" + "cspell-glob": "8.1.0", + "find-up-simple": "^1.0.0" }, "bin": { "cspell-gitignore": "bin.mjs" @@ -7064,9 +7073,9 @@ } }, "node_modules/cspell-glob": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.0.0.tgz", - "integrity": "sha512-wOkRA1OTIPhyN7a+k9Qq45yFXM+tBFi9DS5ObiLv6t6VTBIeMQpwRK0KLViHmjTgiA6eWx53Dnr+DZfxcAkcZA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-8.1.0.tgz", + "integrity": "sha512-onPRqJqPZaaUQ1CKeuh2fJJ9UjIBicRq6Ffd6bqWCu7IdwfEBPtjWa/nlEjCVp1CMRwhS3Y0zG3jHkKLydsR4Q==", "dev": true, "dependencies": { "micromatch": "^4.0.5" @@ -7076,13 +7085,13 @@ } }, "node_modules/cspell-grammar": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.0.0.tgz", - "integrity": "sha512-uxpRvbBxOih6SjFQvKTBPTA+YyqYM5UFTNTFuRnA6g6WZeg+NJaTkbQrTgXja4B2r8MJ6XU22YrKTtHNNcP7bQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-8.1.0.tgz", + "integrity": "sha512-E28SDJYOOuHk8eBtMSIGyCu8qiKb/H4LX1J/kw8+eV0RLvnllmq2FAYFBk8jtu4uW49TW5n/eLg7J2TvPONYAA==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "8.0.0", - "@cspell/cspell-types": "8.0.0" + "@cspell/cspell-pipe": "8.1.0", + "@cspell/cspell-types": "8.1.0" }, "bin": { "cspell-grammar": "bin.mjs" @@ -7092,40 +7101,39 @@ } }, "node_modules/cspell-io": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.0.0.tgz", - "integrity": "sha512-NVdVmQd7SU/nxYwWtO/6gzux/kp1Dt36zKds0+QHZhQ18JJjXduF5e+WUttqKi2oj/vvmjiG4HGFKQVDBcBz3w==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-8.1.0.tgz", + "integrity": "sha512-oPRMS/XUWcdZXMj6Zhs65mgOVyRZajAhHLm18o6cPLOGUD0770oMqi8ZNKj7LuvubkyP/NL0m4AEcWwvmz/Cbw==", "dev": true, "dependencies": { - "@cspell/cspell-service-bus": "8.0.0" + "@cspell/cspell-service-bus": "8.1.0" }, "engines": { "node": ">=18" } }, "node_modules/cspell-lib": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-8.0.0.tgz", - "integrity": "sha512-X/BzUjrzHOx7YlhvSph/OlMu1RmCTnybeZvIE67d1Pd7wT1TmZhFTnmvruUhoHxWEudOEe4HjzuNL9ph6Aw+aA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-8.1.0.tgz", + "integrity": "sha512-tatdY9teElqqPtKHAY1osOhV68h/f3x+4Niw7rV12OXmJ9El1lPka59bVTV401fODWRoF3WWJXUpTg012zhdrQ==", "dev": true, "dependencies": { - "@cspell/cspell-bundled-dicts": "8.0.0", - "@cspell/cspell-pipe": "8.0.0", - "@cspell/cspell-resolver": "8.0.0", - "@cspell/cspell-types": "8.0.0", - "@cspell/dynamic-import": "8.0.0", - "@cspell/strong-weak-map": "8.0.0", + "@cspell/cspell-bundled-dicts": "8.1.0", + "@cspell/cspell-pipe": "8.1.0", + "@cspell/cspell-resolver": "8.1.0", + "@cspell/cspell-types": "8.1.0", + "@cspell/dynamic-import": "8.1.0", + "@cspell/strong-weak-map": "8.1.0", "clear-module": "^4.1.2", "comment-json": "^4.2.3", "configstore": "^6.0.0", - "cosmiconfig": "8.0.0", - "cspell-dictionary": "8.0.0", - "cspell-glob": "8.0.0", - "cspell-grammar": "8.0.0", - "cspell-io": "8.0.0", - "cspell-trie-lib": "8.0.0", + "cspell-config-lib": "8.1.0", + "cspell-dictionary": "8.1.0", + "cspell-glob": "8.1.0", + "cspell-grammar": "8.1.0", + "cspell-io": "8.1.0", + "cspell-trie-lib": "8.1.0", "fast-equals": "^5.0.1", - "find-up": "^6.3.0", "gensequence": "^6.0.0", "import-fresh": "^3.3.0", "resolve-from": "^5.0.0", @@ -7136,129 +7144,14 @@ "node": ">=18" } }, - "node_modules/cspell-lib/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/cspell-lib/node_modules/cosmiconfig": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.0.0.tgz", - "integrity": "sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==", - "dev": true, - "dependencies": { - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/cspell-lib/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cspell-lib/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/cspell-lib/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cspell-lib/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cspell-lib/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cspell-lib/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/cspell-lib/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cspell-trie-lib": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.0.0.tgz", - "integrity": "sha512-0rC5e1C0uM78uuS+lC1T18EojWZyNvq4bPOPCisnwuhuWrAfCqrFrX/qDNslWk3VTOPbsEMlFj6OnIGQnfwSKg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-8.1.0.tgz", + "integrity": "sha512-OF5ZNuGPIGg2CCMdMeAgd1I2iVDjoelpMjVDyqpuNu+RVpAkmNRqMFDBlsnJPWCCeOLn7blWPMBZW2KXctsm3Q==", "dev": true, "dependencies": { - "@cspell/cspell-pipe": "8.0.0", - "@cspell/cspell-types": "8.0.0", + "@cspell/cspell-pipe": "8.1.0", + "@cspell/cspell-types": "8.1.0", "gensequence": "^6.0.0" }, "engines": { @@ -7964,6 +7857,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", "dev": true, "dependencies": { "webidl-conversions": "^7.0.0" @@ -8033,9 +7927,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.582", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.582.tgz", - "integrity": "sha512-89o0MGoocwYbzqUUjc+VNpeOFSOK9nIdC5wY4N+PVUarUK0MtjyTjks75AZS2bW4Kl8MdewdFsWaH0jLy+JNoA==", + "version": "1.4.601", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.601.tgz", + "integrity": "sha512-SpwUMDWe9tQu8JX5QCO1+p/hChAi9AE9UpoC3rcHVc+gdCGlbT3SGb5I1klgb952HRIyvt9wZhSz9bNBYz9swA==", "dev": true }, "node_modules/emittery": { @@ -8304,15 +8198,15 @@ } }, "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -8358,6 +8252,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-compat-utils": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz", + "integrity": "sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, "node_modules/eslint-config-standard": { "version": "17.1.0", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", @@ -8508,13 +8414,14 @@ } }, "node_modules/eslint-plugin-es-x": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.3.0.tgz", - "integrity": "sha512-W9zIs+k00I/I13+Bdkl/zG1MEO07G97XjUSQuH117w620SJ6bHtLUmoMvkGA2oYnI/gNdr+G7BONLyYnFaLLEQ==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz", + "integrity": "sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.1.2", - "@eslint-community/regexpp": "^4.6.0" + "@eslint-community/regexpp": "^4.6.0", + "eslint-compat-utils": "^0.1.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -9390,12 +9297,12 @@ } }, "node_modules/file-entry-cache": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-7.0.1.tgz", - "integrity": "sha512-uLfFktPmRetVCbHe5UPuekWrQ6hENufnA46qEGbfACkK5drjTTdQYUragRgMjHldcbYG+nslUerqMPjbBSHXjQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-7.0.2.tgz", + "integrity": "sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==", "dev": true, "dependencies": { - "flat-cache": "^3.1.1" + "flat-cache": "^3.2.0" }, "engines": { "node": ">=12.0.0" @@ -9489,6 +9396,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-up-simple": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz", + "integrity": "sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -9822,16 +9741,16 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", "dev": true, "dependencies": { - "ini": "2.0.0" + "ini": "4.1.1" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10439,9 +10358,9 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "dev": true, "engines": { "node": ">= 4" @@ -10583,9 +10502,9 @@ } }, "node_modules/import-meta-resolve": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-3.1.1.tgz", - "integrity": "sha512-qeywsE/KC3w9Fd2ORrRDUw6nS/nLwZpXgfrOc2IILvZYnCaEMd+D56Vfg9k4G29gIeVi3XKql1RQatME8iYsiw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", + "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", "dev": true, "funding": { "type": "github", @@ -10627,12 +10546,12 @@ "dev": true }, "node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", "dev": true, "engines": { - "node": ">=10" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/internal-slot": { @@ -14059,9 +13978,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/node-watch": { @@ -14203,13 +14122,13 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -14705,9 +14624,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", + "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "dev": true, "funding": [ { @@ -14724,7 +14643,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -15581,11 +15500,11 @@ } }, "node_modules/react-router": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.18.0.tgz", - "integrity": "sha512-vk2y7Dsy8wI02eRRaRmOs9g2o+aE72YCx5q9VasT1N9v+lrdB79tIqrjMfByHiY5+6aYkH2rUa5X839nwWGPDg==", + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.20.1.tgz", + "integrity": "sha512-ccvLrB4QeT5DlaxSFFYi/KR8UMQ4fcD8zBcR71Zp1kaYTC5oJKYAp1cbavzGrogwxca+ubjkd7XjFZKBW8CxPA==", "dependencies": { - "@remix-run/router": "1.11.0" + "@remix-run/router": "1.13.1" }, "engines": { "node": ">=14.0.0" @@ -15595,12 +15514,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.18.0.tgz", - "integrity": "sha512-Ubrue4+Ercc/BoDkFQfc6og5zRQ4A8YxSO3Knsne+eRbZ+IepAsK249XBH/XaFuOYOYr3L3r13CXTLvYt5JDjw==", + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.20.1.tgz", + "integrity": "sha512-npzfPWcxfQN35psS7rJgi/EW0Gx6EsNjfdJSAk73U/HqMEJZ2k/8puxfwHFgDQhBGmS3+sjnGbMdMSV45axPQw==", "dependencies": { - "@remix-run/router": "1.11.0", - "react-router": "6.18.0" + "@remix-run/router": "1.13.1", + "react-router": "6.20.1" }, "engines": { "node": ">=14.0.0" @@ -17267,15 +17186,16 @@ "dev": true }, "node_modules/svgo": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.3.tgz", - "integrity": "sha512-X4UZvLhOglD5Xrp834HzGHf8RKUW0Ahigg/08yRO1no9t2NxffOkMiQ0WmaMIbaGlVTlSst2zWANsdhz5ybXgA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.5.tgz", + "integrity": "sha512-HQKHEo73pMNOlDlBcLgZRcHW2+1wo7bFYayAXkGN0l/2+h68KjlfZyMRhdhaGvoHV2eApOovl12zoFz42sT6rQ==", "dev": true, "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.2.1", + "css-what": "^6.1.0", "csso": "5.0.5", "picocolors": "^1.0.0" }, @@ -17936,9 +17856,9 @@ } }, "node_modules/typedoc": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.3.tgz", - "integrity": "sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==", + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.4.tgz", + "integrity": "sha512-Du9ImmpBCw54bX275yJrxPVnjdIyJO/84co0/L9mwe0R3G4FSR6rQ09AlXVRvZEGMUg09+z/usc8mgygQ1aidA==", "dev": true, "dependencies": { "lunr": "^2.3.9", @@ -17953,7 +17873,7 @@ "node": ">= 16" }, "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x" + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x" } }, "node_modules/typedoc-plugin-missing-exports": { @@ -17990,9 +17910,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -18182,9 +18102,9 @@ } }, "node_modules/v8-to-istanbul": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", - "integrity": "sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", @@ -18846,6 +18766,15 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "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" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/web/package/cockpit-agama.changes b/web/package/cockpit-agama.changes index 37e83816a0..972aa9b9b6 100644 --- a/web/package/cockpit-agama.changes +++ b/web/package/cockpit-agama.changes @@ -1,3 +1,38 @@ +------------------------------------------------------------------- +Sat Dec 2 18:06:02 UTC 2023 - Imobach Gonzalez Sosa + +- Version 6 + +------------------------------------------------------------------- +Thu Nov 30 22:39:57 UTC 2023 - David Diaz + +- UI: make selectors more compact (gh#openSUSE/agama#898). + +------------------------------------------------------------------- +Thu Nov 30 15:19:38 UTC 2023 - José Iván López González + +- Allow selecting the storage policy to make free space for the + installation (gh#openSUSE/agama#883). + +------------------------------------------------------------------- +Wed Nov 29 14:15:16 UTC 2023 - José Iván López González + +- Allow selecting language, keymap and timezone for the target + system (gh#openSUSE/agama#881). + +------------------------------------------------------------------- +Wed Nov 29 13:01:04 UTC 2023 - David Diaz + +- UI: improve the look and feel by fine tunning the sections spacing, + alignment, and icon sizes (gh#openSUSE/agama#892). + +------------------------------------------------------------------- +Tue Nov 21 15:21:06 UTC 2023 - David Diaz + +- UI: Do not crash when clicking the install button. It started + failing after removing core-js dependency (gh#openSUSE/agama#880 + and related to gh#openSUSE/agama#866). + ------------------------------------------------------------------- Fri Nov 17 13:27:22 UTC 2023 - David Diaz diff --git a/web/po/cs.po b/web/po/cs.po index 18fa976e5f..b1241fde2f 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-11-05 02:14+0000\n" +"POT-Creation-Date: 2023-12-03 02:17+0000\n" "PO-Revision-Date: 2023-07-26 15:03+0000\n" "Last-Translator: Ladislav Slezák \n" "Language-Team: Czech =2 && n<=4) ? 1 : 2;\n" "X-Generator: Weblate 4.9.1\n" -#: src/App.jsx:112 +#: src/App.jsx:110 msgid "Diagnostic tools" msgstr "" @@ -64,6 +64,7 @@ msgstr "" #: src/components/core/About.jsx:71 src/components/core/FileViewer.jsx:80 #: src/components/core/Sidebar.jsx:177 src/components/core/Terminal.jsx:48 #: src/components/network/WifiSelector.jsx:151 +#: src/components/product/ProductPage.jsx:239 msgid "Close" msgstr "Zavřít" @@ -132,18 +133,23 @@ msgid "" msgstr "" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:97 -#: src/components/storage/ProposalSettingsSection.jsx:166 -#: src/components/storage/ProposalSettingsSection.jsx:359 -#: src/components/storage/ProposalSettingsSection.jsx:504 +#: src/components/core/InstallButton.jsx:97 src/components/l10n/L10nPage.jsx:75 +#: src/components/l10n/L10nPage.jsx:191 src/components/l10n/L10nPage.jsx:304 +#: src/components/product/ProductPage.jsx:71 +#: src/components/product/ProductPage.jsx:140 +#: src/components/product/ProductPage.jsx:207 +#: src/components/storage/ProposalSettingsSection.jsx:170 +#: src/components/storage/ProposalSettingsSection.jsx:363 +#: src/components/storage/ProposalSettingsSection.jsx:508 +#: src/components/storage/ProposalSettingsSection.jsx:604 #: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:282 +#: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:513 msgid "Accept" msgstr "" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:145 +#: src/components/core/InstallButton.jsx:148 msgid "Install" msgstr "" @@ -193,10 +199,40 @@ msgstr "" msgid "There are new issues" msgstr "" -#: src/components/core/IssuesPage.jsx:115 +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:92 +#: src/components/overview/ProductSection.jsx:71 +#: src/components/product/ProductPage.jsx:434 +msgid "Product" +msgstr "" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section title +#: src/components/core/IssuesPage.jsx:100 +#: src/components/overview/StorageSection.jsx:208 +#: src/components/storage/ProposalPage.jsx:218 +msgid "Storage" +msgstr "" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:108 +#: src/components/overview/SoftwareSection.jsx:141 +#: src/components/software/SoftwarePage.jsx:81 +msgid "Software" +msgstr "" + +#: src/components/core/IssuesPage.jsx:129 msgid "No issues found. Everything looks ok." msgstr "" +#. TRANSLATORS: search field placeholder text +#: src/components/core/ListSearch.jsx:50 +#: src/components/software/PatternSelector.jsx:220 +#: src/components/software/PatternSelector.jsx:221 +msgid "Search" +msgstr "" + #: src/components/core/LogsButton.jsx:98 msgid "Collecting logs..." msgstr "" @@ -256,12 +292,11 @@ msgstr "" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 #: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:309 +#: src/components/storage/ProposalVolumes.jsx:310 msgid "Actions" msgstr "" #: src/components/core/SectionSkeleton.jsx:29 -#: src/components/core/SectionSkeleton.jsx:34 msgid "Waiting" msgstr "" @@ -318,22 +353,121 @@ msgstr[2] "" msgid "Basic popover" msgstr "" -#: src/components/l10n/L10nPage.jsx:72 -#: src/components/l10n/LanguageSwitcher.jsx:50 -msgid "language" +#. TRANSLATORS: placeholder text for search input in the keyboard selector. +#: src/components/l10n/KeymapSelector.jsx:82 +msgid "Filter by description or keymap code" +msgstr "" + +#: src/components/l10n/KeymapSelector.jsx:89 +msgid "Available keymaps" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:66 src/components/l10n/L10nPage.jsx:140 +msgid "Select time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:67 +#, c-format +msgid "%s will use the selected time zone." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:128 +msgid "Time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:134 +msgid "Change time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:139 +msgid "Time zone not selected yet" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:182 src/components/l10n/L10nPage.jsx:258 +msgid "Select language" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:183 +#, c-format +msgid "%s will use the selected language." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:246 +msgid "Language" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:252 +msgid "Change language" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:257 +msgid "Language not selected yet" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:295 src/components/l10n/L10nPage.jsx:369 +msgid "Select keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:296 +#, c-format +msgid "%s will use the selected keyboard." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:357 +msgid "Keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:363 +msgid "Change keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:368 +msgid "Keyboard not selected yet" msgstr "" -#. TRANSLATORS: page header #. TRANSLATORS: page section -#: src/components/l10n/L10nPage.jsx:84 -#: src/components/overview/L10nSection.jsx:82 +#. TRANSLATORS: page title +#: src/components/l10n/L10nPage.jsx:385 +#: src/components/overview/L10nSection.jsx:52 msgid "Localization" msgstr "" +#: src/components/l10n/L10nPage.jsx:387 +#: src/components/product/ProductPage.jsx:434 +#: src/components/software/SoftwarePage.jsx:81 +#: src/components/storage/DASDPage.jsx:187 +#: src/components/storage/ISCSIPage.jsx:39 +#: src/components/storage/ProposalPage.jsx:218 +#: src/components/storage/ZFCPPage.jsx:736 +#: src/components/users/UsersPage.jsx:30 +msgid "Back" +msgstr "" + #: src/components/l10n/LanguageSwitcher.jsx:46 msgid "Display Language" msgstr "" +#: src/components/l10n/LanguageSwitcher.jsx:50 +msgid "language" +msgstr "" + +#: src/components/l10n/LocaleSelector.jsx:82 +msgid "Filter by language, territory or locale code" +msgstr "" + +#: src/components/l10n/LocaleSelector.jsx:89 +msgid "Available locales" +msgstr "" + +#. TRANSLATORS: placeholder text for search input in the timezone selector. +#: src/components/l10n/TimezoneSelector.jsx:102 +msgid "Filter by territory, time zone code or UTC offset" +msgstr "" + +#: src/components/l10n/TimezoneSelector.jsx:109 +msgid "Available time zones" +msgstr "" + #: src/components/layout/Loading.jsx:30 msgid "Loading installation environment, please wait." msgstr "" @@ -394,7 +528,7 @@ msgid "IP addresses" msgstr "" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:233 +#: src/components/storage/ProposalVolumes.jsx:234 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:170 @@ -535,6 +669,8 @@ msgid "WPA & WPA2 Personal" msgstr "" #: src/components/network/WifiConnectionForm.jsx:91 +#: src/components/product/ProductPage.jsx:128 +#: src/components/product/ProductPage.jsx:194 #: src/components/storage/ZFCPDiskForm.jsx:112 #: src/components/storage/iscsi/DiscoverForm.jsx:108 #: src/components/storage/iscsi/LoginForm.jsx:72 @@ -607,9 +743,9 @@ msgstr "" msgid "Forget network" msgstr "" -#. TRANSLATORS: %s will be replaced by a language name and code, -#. example: "English (en_US.UTF-8)" -#: src/components/overview/L10nSection.jsx:70 +#. TRANSLATORS: %s will be replaced by a language name and territory, example: +#. "English (United States)". +#: src/components/overview/L10nSection.jsx:34 #, c-format msgid "The system will use %s as its default language." msgstr "" @@ -628,43 +764,61 @@ msgstr[0] "" msgstr[1] "" msgstr[2] "" +#. TRANSLATORS: page title +#: src/components/overview/Overview.jsx:47 +msgid "Installation Summary" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/overview/ProductSection.jsx:48 +#, c-format +msgid "%s (registered)" +msgstr "" + #: src/components/overview/SoftwareSection.jsx:37 msgid "Reading software repositories" msgstr "" #. TRANSLATORS: clickable link label -#: src/components/overview/SoftwareSection.jsx:135 +#: src/components/overview/SoftwareSection.jsx:131 msgid "Refresh the repositories" msgstr "" -#. TRANSLATORS: page section -#. TRANSLATORS: page title -#: src/components/overview/SoftwareSection.jsx:145 -#: src/components/software/SoftwarePage.jsx:81 -msgid "Software" +#: src/components/overview/StorageSection.jsx:42 +#: src/components/storage/ProposalSettingsSection.jsx:126 +msgid "No device selected yet" msgstr "" -#: src/components/overview/StorageSection.jsx:36 -#: src/components/storage/ProposalSettingsSection.jsx:122 -msgid "No device selected yet" +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:52 +#, c-format +msgid "Install using device %s shrinking existing partitions as needed" msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:44 +#: src/components/overview/StorageSection.jsx:56 +#, c-format +msgid "Install using device %s without modifying existing partitions" +msgstr "" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:60 #, c-format msgid "Install using device %s and deleting all its content" msgstr "" -#: src/components/overview/StorageSection.jsx:57 -msgid "Probing storage devices" +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:66 +#, c-format +msgid "Install using device %s" msgstr "" -#. TRANSLATORS: page title -#. TRANSLATORS: page section title -#: src/components/overview/StorageSection.jsx:182 -#: src/components/storage/ProposalPage.jsx:218 -msgid "Storage" +#: src/components/overview/StorageSection.jsx:83 +msgid "Probing storage devices" msgstr "" #. TRANSLATORS: %s will be replaced by the user name @@ -700,6 +854,96 @@ msgstr "" msgid "Users" msgstr "" +#: src/components/product/ProductPage.jsx:63 +#: src/components/product/ProductSelectionPage.jsx:73 +msgid "Choose a product" +msgstr "" + +#: src/components/product/ProductPage.jsx:122 +#, c-format +msgid "Register %s" +msgstr "" + +#: src/components/product/ProductPage.jsx:188 +#, c-format +msgid "Deregister %s" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:202 +#, c-format +msgid "Do you want to deregister %s?" +msgstr "" + +#: src/components/product/ProductPage.jsx:227 +msgid "Registered warning" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:232 +#, c-format +msgid "The product %s must be deregistered before selecting a new product." +msgstr "" + +#: src/components/product/ProductPage.jsx:263 +msgid "Change product" +msgstr "" + +#: src/components/product/ProductPage.jsx:305 +msgid "Register" +msgstr "" + +#: src/components/product/ProductPage.jsx:337 +msgid "Deregister product" +msgstr "" + +#: src/components/product/ProductPage.jsx:370 +msgid "Code:" +msgstr "" + +#: src/components/product/ProductPage.jsx:374 +msgid "Email:" +msgstr "" + +#. TRANSLATORS: section title. +#: src/components/product/ProductPage.jsx:390 +msgid "Registration" +msgstr "" + +#: src/components/product/ProductPage.jsx:399 +msgid "This product requires registration." +msgstr "" + +#: src/components/product/ProductPage.jsx:405 +msgid "This product does not require registration." +msgstr "" + +#: src/components/product/ProductRegistrationForm.jsx:63 +msgid "Registration code" +msgstr "" + +#: src/components/product/ProductRegistrationForm.jsx:66 +msgid "Email" +msgstr "" + +#: src/components/product/ProductSelectionPage.jsx:58 +msgid "Loading available products, please wait..." +msgstr "" + +#. TRANSLATORS: page header +#: src/components/product/ProductSelectionPage.jsx:64 +msgid "Product selection" +msgstr "" + +#. TRANSLATORS: button label +#: src/components/product/ProductSelectionPage.jsx:69 +msgid "Select" +msgstr "" + +#: src/components/product/ProductSelector.jsx:29 +msgid "No products available for selection" +msgstr "" + #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 msgid "Question" @@ -719,10 +963,6 @@ msgstr "" msgid "Encryption Password" msgstr "" -#: src/components/software/ChangeProductLink.jsx:36 -msgid "Change product" -msgstr "" - #. TRANSLATORS: pattern status, selected to install (by user) #: src/components/software/PatternItem.jsx:63 msgid "selected" @@ -740,46 +980,13 @@ msgstr "" #. TRANSLATORS: error summary, always plural, %d is replaced by number of errors (2 or more) #. if there is just a single error then the error is displayed directly instead of this summary -#: src/components/software/PatternSelector.jsx:206 +#: src/components/software/PatternSelector.jsx:207 #, c-format msgid "%d errors" msgstr "" -#: src/components/software/PatternSelector.jsx:210 -msgid "Software summary and filter options" -msgstr "" - -#. TRANSLATORS: search field placeholder text #: src/components/software/PatternSelector.jsx:215 -#: src/components/software/PatternSelector.jsx:216 -msgid "Search" -msgstr "" - -#: src/components/software/ProductSelectionPage.jsx:69 -msgid "Loading available products, please wait..." -msgstr "" - -#. TRANSLATORS: page header -#: src/components/software/ProductSelectionPage.jsx:94 -msgid "Product selection" -msgstr "" - -#. TRANSLATORS: button label -#: src/components/software/ProductSelectionPage.jsx:99 -msgid "Select" -msgstr "" - -#: src/components/software/ProductSelectionPage.jsx:104 -msgid "Choose a product" -msgstr "" - -#: src/components/software/SoftwarePage.jsx:81 -#: src/components/storage/DASDPage.jsx:187 -#: src/components/storage/ISCSIPage.jsx:39 -#: src/components/storage/ProposalPage.jsx:218 -#: src/components/storage/ZFCPPage.jsx:736 -#: src/components/users/UsersPage.jsx:30 -msgid "Back" +msgid "Software summary and filter options" msgstr "" #. TRANSLATORS: %s will be replaced by the estimated installation size, @@ -959,65 +1166,80 @@ msgstr "" msgid "iSCSI" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:135 +#: src/components/storage/ProposalSettingsSection.jsx:139 msgid "Select the device for installing the system." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:140 #: src/components/storage/ProposalSettingsSection.jsx:144 -#: src/components/storage/ProposalSettingsSection.jsx:240 +#: src/components/storage/ProposalSettingsSection.jsx:148 +#: src/components/storage/ProposalSettingsSection.jsx:244 msgid "Installation device" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:150 +#: src/components/storage/ProposalSettingsSection.jsx:154 msgid "No devices found" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:237 +#: src/components/storage/ProposalSettingsSection.jsx:241 msgid "Devices for creating the volume group" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:246 +#: src/components/storage/ProposalSettingsSection.jsx:250 msgid "Custom devices" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:310 +#: src/components/storage/ProposalSettingsSection.jsx:314 msgid "" "Configuration of the system volume group. All the file systems will be " "created in a logical volume of the system volume group." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:316 +#: src/components/storage/ProposalSettingsSection.jsx:320 msgid "Configure the LVM settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:321 -#: src/components/storage/ProposalSettingsSection.jsx:341 +#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:345 msgid "LVM settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:334 +#: src/components/storage/ProposalSettingsSection.jsx:338 msgid "Use logical volume management (LVM)" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:342 +#: src/components/storage/ProposalSettingsSection.jsx:346 msgid "System Volume Group" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:470 +#: src/components/storage/ProposalSettingsSection.jsx:474 msgid "Change encryption password" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:475 -#: src/components/storage/ProposalSettingsSection.jsx:496 +#: src/components/storage/ProposalSettingsSection.jsx:479 +#: src/components/storage/ProposalSettingsSection.jsx:500 msgid "Encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:489 +#: src/components/storage/ProposalSettingsSection.jsx:493 msgid "Use encryption" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:557 +#: src/components/storage/ProposalSettingsSection.jsx:578 +msgid "" +"Select how to make free space in the disks selected for allocating the " +"file systems." +msgstr "" + +#. TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" +#: src/components/storage/ProposalSettingsSection.jsx:584 +msgid "Find space" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:588 +msgid "Space Policy" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:662 msgid "Settings" msgstr "" @@ -1070,48 +1292,48 @@ msgid "partition" msgstr "" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:215 +#: src/components/storage/ProposalVolumes.jsx:216 msgid "encrypted" msgstr "" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:217 +#: src/components/storage/ProposalVolumes.jsx:218 msgid "with snapshots" msgstr "" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:219 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "transactional" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:228 +#: src/components/storage/ProposalVolumes.jsx:229 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:274 +#: src/components/storage/ProposalVolumes.jsx:275 msgid "Edit file system" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:306 +#: src/components/storage/ProposalVolumes.jsx:307 #: src/components/storage/VolumeForm.jsx:500 msgid "Mount point" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:307 +#: src/components/storage/ProposalVolumes.jsx:308 msgid "Details" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:309 #: src/components/storage/VolumeForm.jsx:517 msgid "Size" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:344 +#: src/components/storage/ProposalVolumes.jsx:345 msgid "Table with mount points" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:407 +#: src/components/storage/ProposalVolumes.jsx:408 msgid "File systems to create in your system" msgstr "" @@ -1320,64 +1542,64 @@ msgid "Storage zFCP" msgstr "" #. TRANSLATORS: multipath device type -#: src/components/storage/device-utils.jsx:98 +#: src/components/storage/device-utils.jsx:97 msgid "Multipath" msgstr "" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/device-utils.jsx:103 +#: src/components/storage/device-utils.jsx:102 #, c-format msgid "DASD %s" msgstr "" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/device-utils.jsx:108 +#: src/components/storage/device-utils.jsx:107 #, c-format msgid "Software %s" msgstr "" -#: src/components/storage/device-utils.jsx:113 +#: src/components/storage/device-utils.jsx:112 msgid "SD Card" msgstr "" #. TRANSLATORS: %s is replaced by the device transport name, e.g. USB, SATA, SCSI... -#: src/components/storage/device-utils.jsx:115 +#: src/components/storage/device-utils.jsx:114 #, c-format msgid "Transport %s" msgstr "" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:134 +#: src/components/storage/device-utils.jsx:133 #, c-format msgid "Members: %s" msgstr "" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:143 +#: src/components/storage/device-utils.jsx:142 #, c-format msgid "Devices: %s" msgstr "" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/device-utils.jsx:152 +#: src/components/storage/device-utils.jsx:151 #, c-format msgid "Wires: %s" msgstr "" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/device-utils.jsx:176 +#: src/components/storage/device-utils.jsx:175 #, c-format msgid "%s with %d partitions" msgstr "" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/device-utils.jsx:200 +#: src/components/storage/device-utils.jsx:199 msgid "No content found" msgstr "" -#: src/components/storage/device-utils.jsx:271 +#: src/components/storage/device-utils.jsx:270 msgid "Available devices" msgstr "" @@ -1553,6 +1775,71 @@ msgstr "" msgid "Targets" msgstr "" +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:59 +msgid "Delete current content" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:63 +msgid "Shrink existing partitions" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:67 +msgid "Use available space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:79 +msgid "All partitions will be removed and any data in the disks will be lost." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:82 +msgid "" +"The data is kept, but the current partitions will be resized as needed to " +"make enough space." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:85 +msgid "" +"The data is kept and existing partitions will not be modified. Only the " +"space that is not assigned to any partition will be used." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:112 +msgid "Select a mechanism to make space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:137 +#, c-format +msgid "deleting all content of the installation device" +msgid_plural "deleting all content of the %d selected disks" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: src/components/storage/space-policy-utils.jsx:147 +#, c-format +msgid "shrinking partitions of the installation device" +msgid_plural "shrinking partitions of the %d selected disks" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence +#. would read as "Find space without modifying any partition". +#: src/components/storage/space-policy-utils.jsx:155 +msgid "without modifying any partition" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:170 +#, c-format +msgid "This will only affect the installation device" +msgid_plural "This will affect the %d disks selected for installation" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + #: src/components/storage/utils.js:44 msgid "KiB" msgstr "" diff --git a/web/po/de.po b/web/po/de.po index ca1e44c6e6..2f9ad13487 100644 --- a/web/po/de.po +++ b/web/po/de.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-11-05 02:14+0000\n" +"POT-Creation-Date: 2023-12-03 02:17+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Automatically generated\n" "Language-Team: none\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -#: src/App.jsx:112 +#: src/App.jsx:110 msgid "Diagnostic tools" msgstr "" @@ -61,6 +61,7 @@ msgstr "" #: src/components/core/About.jsx:71 src/components/core/FileViewer.jsx:80 #: src/components/core/Sidebar.jsx:177 src/components/core/Terminal.jsx:48 #: src/components/network/WifiSelector.jsx:151 +#: src/components/product/ProductPage.jsx:239 msgid "Close" msgstr "" @@ -129,18 +130,23 @@ msgid "" msgstr "" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:97 -#: src/components/storage/ProposalSettingsSection.jsx:166 -#: src/components/storage/ProposalSettingsSection.jsx:359 -#: src/components/storage/ProposalSettingsSection.jsx:504 +#: src/components/core/InstallButton.jsx:97 src/components/l10n/L10nPage.jsx:75 +#: src/components/l10n/L10nPage.jsx:191 src/components/l10n/L10nPage.jsx:304 +#: src/components/product/ProductPage.jsx:71 +#: src/components/product/ProductPage.jsx:140 +#: src/components/product/ProductPage.jsx:207 +#: src/components/storage/ProposalSettingsSection.jsx:170 +#: src/components/storage/ProposalSettingsSection.jsx:363 +#: src/components/storage/ProposalSettingsSection.jsx:508 +#: src/components/storage/ProposalSettingsSection.jsx:604 #: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:282 +#: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:513 msgid "Accept" msgstr "" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:145 +#: src/components/core/InstallButton.jsx:148 msgid "Install" msgstr "" @@ -190,10 +196,40 @@ msgstr "" msgid "There are new issues" msgstr "" -#: src/components/core/IssuesPage.jsx:115 +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:92 +#: src/components/overview/ProductSection.jsx:71 +#: src/components/product/ProductPage.jsx:434 +msgid "Product" +msgstr "" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section title +#: src/components/core/IssuesPage.jsx:100 +#: src/components/overview/StorageSection.jsx:208 +#: src/components/storage/ProposalPage.jsx:218 +msgid "Storage" +msgstr "" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:108 +#: src/components/overview/SoftwareSection.jsx:141 +#: src/components/software/SoftwarePage.jsx:81 +msgid "Software" +msgstr "" + +#: src/components/core/IssuesPage.jsx:129 msgid "No issues found. Everything looks ok." msgstr "" +#. TRANSLATORS: search field placeholder text +#: src/components/core/ListSearch.jsx:50 +#: src/components/software/PatternSelector.jsx:220 +#: src/components/software/PatternSelector.jsx:221 +msgid "Search" +msgstr "" + #: src/components/core/LogsButton.jsx:98 msgid "Collecting logs..." msgstr "" @@ -253,12 +289,11 @@ msgstr "" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 #: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:309 +#: src/components/storage/ProposalVolumes.jsx:310 msgid "Actions" msgstr "" #: src/components/core/SectionSkeleton.jsx:29 -#: src/components/core/SectionSkeleton.jsx:34 msgid "Waiting" msgstr "" @@ -314,22 +349,121 @@ msgstr[1] "" msgid "Basic popover" msgstr "" -#: src/components/l10n/L10nPage.jsx:72 -#: src/components/l10n/LanguageSwitcher.jsx:50 -msgid "language" +#. TRANSLATORS: placeholder text for search input in the keyboard selector. +#: src/components/l10n/KeymapSelector.jsx:82 +msgid "Filter by description or keymap code" +msgstr "" + +#: src/components/l10n/KeymapSelector.jsx:89 +msgid "Available keymaps" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:66 src/components/l10n/L10nPage.jsx:140 +msgid "Select time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:67 +#, c-format +msgid "%s will use the selected time zone." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:128 +msgid "Time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:134 +msgid "Change time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:139 +msgid "Time zone not selected yet" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:182 src/components/l10n/L10nPage.jsx:258 +msgid "Select language" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:183 +#, c-format +msgid "%s will use the selected language." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:246 +msgid "Language" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:252 +msgid "Change language" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:257 +msgid "Language not selected yet" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:295 src/components/l10n/L10nPage.jsx:369 +msgid "Select keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:296 +#, c-format +msgid "%s will use the selected keyboard." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:357 +msgid "Keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:363 +msgid "Change keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:368 +msgid "Keyboard not selected yet" msgstr "" -#. TRANSLATORS: page header #. TRANSLATORS: page section -#: src/components/l10n/L10nPage.jsx:84 -#: src/components/overview/L10nSection.jsx:82 +#. TRANSLATORS: page title +#: src/components/l10n/L10nPage.jsx:385 +#: src/components/overview/L10nSection.jsx:52 msgid "Localization" msgstr "" +#: src/components/l10n/L10nPage.jsx:387 +#: src/components/product/ProductPage.jsx:434 +#: src/components/software/SoftwarePage.jsx:81 +#: src/components/storage/DASDPage.jsx:187 +#: src/components/storage/ISCSIPage.jsx:39 +#: src/components/storage/ProposalPage.jsx:218 +#: src/components/storage/ZFCPPage.jsx:736 +#: src/components/users/UsersPage.jsx:30 +msgid "Back" +msgstr "" + #: src/components/l10n/LanguageSwitcher.jsx:46 msgid "Display Language" msgstr "" +#: src/components/l10n/LanguageSwitcher.jsx:50 +msgid "language" +msgstr "" + +#: src/components/l10n/LocaleSelector.jsx:82 +msgid "Filter by language, territory or locale code" +msgstr "" + +#: src/components/l10n/LocaleSelector.jsx:89 +msgid "Available locales" +msgstr "" + +#. TRANSLATORS: placeholder text for search input in the timezone selector. +#: src/components/l10n/TimezoneSelector.jsx:102 +msgid "Filter by territory, time zone code or UTC offset" +msgstr "" + +#: src/components/l10n/TimezoneSelector.jsx:109 +msgid "Available time zones" +msgstr "" + #: src/components/layout/Loading.jsx:30 msgid "Loading installation environment, please wait." msgstr "" @@ -390,7 +524,7 @@ msgid "IP addresses" msgstr "" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:233 +#: src/components/storage/ProposalVolumes.jsx:234 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:170 @@ -531,6 +665,8 @@ msgid "WPA & WPA2 Personal" msgstr "" #: src/components/network/WifiConnectionForm.jsx:91 +#: src/components/product/ProductPage.jsx:128 +#: src/components/product/ProductPage.jsx:194 #: src/components/storage/ZFCPDiskForm.jsx:112 #: src/components/storage/iscsi/DiscoverForm.jsx:108 #: src/components/storage/iscsi/LoginForm.jsx:72 @@ -603,9 +739,9 @@ msgstr "" msgid "Forget network" msgstr "" -#. TRANSLATORS: %s will be replaced by a language name and code, -#. example: "English (en_US.UTF-8)" -#: src/components/overview/L10nSection.jsx:70 +#. TRANSLATORS: %s will be replaced by a language name and territory, example: +#. "English (United States)". +#: src/components/overview/L10nSection.jsx:34 #, c-format msgid "The system will use %s as its default language." msgstr "" @@ -623,43 +759,61 @@ msgid_plural "%d connections set:" msgstr[0] "" msgstr[1] "" +#. TRANSLATORS: page title +#: src/components/overview/Overview.jsx:47 +msgid "Installation Summary" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/overview/ProductSection.jsx:48 +#, c-format +msgid "%s (registered)" +msgstr "" + #: src/components/overview/SoftwareSection.jsx:37 msgid "Reading software repositories" msgstr "" #. TRANSLATORS: clickable link label -#: src/components/overview/SoftwareSection.jsx:135 +#: src/components/overview/SoftwareSection.jsx:131 msgid "Refresh the repositories" msgstr "" -#. TRANSLATORS: page section -#. TRANSLATORS: page title -#: src/components/overview/SoftwareSection.jsx:145 -#: src/components/software/SoftwarePage.jsx:81 -msgid "Software" +#: src/components/overview/StorageSection.jsx:42 +#: src/components/storage/ProposalSettingsSection.jsx:126 +msgid "No device selected yet" msgstr "" -#: src/components/overview/StorageSection.jsx:36 -#: src/components/storage/ProposalSettingsSection.jsx:122 -msgid "No device selected yet" +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:52 +#, c-format +msgid "Install using device %s shrinking existing partitions as needed" msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:44 +#: src/components/overview/StorageSection.jsx:56 +#, c-format +msgid "Install using device %s without modifying existing partitions" +msgstr "" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:60 #, c-format msgid "Install using device %s and deleting all its content" msgstr "" -#: src/components/overview/StorageSection.jsx:57 -msgid "Probing storage devices" +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:66 +#, c-format +msgid "Install using device %s" msgstr "" -#. TRANSLATORS: page title -#. TRANSLATORS: page section title -#: src/components/overview/StorageSection.jsx:182 -#: src/components/storage/ProposalPage.jsx:218 -msgid "Storage" +#: src/components/overview/StorageSection.jsx:83 +msgid "Probing storage devices" msgstr "" #. TRANSLATORS: %s will be replaced by the user name @@ -695,6 +849,96 @@ msgstr "" msgid "Users" msgstr "" +#: src/components/product/ProductPage.jsx:63 +#: src/components/product/ProductSelectionPage.jsx:73 +msgid "Choose a product" +msgstr "" + +#: src/components/product/ProductPage.jsx:122 +#, c-format +msgid "Register %s" +msgstr "" + +#: src/components/product/ProductPage.jsx:188 +#, c-format +msgid "Deregister %s" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:202 +#, c-format +msgid "Do you want to deregister %s?" +msgstr "" + +#: src/components/product/ProductPage.jsx:227 +msgid "Registered warning" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:232 +#, c-format +msgid "The product %s must be deregistered before selecting a new product." +msgstr "" + +#: src/components/product/ProductPage.jsx:263 +msgid "Change product" +msgstr "" + +#: src/components/product/ProductPage.jsx:305 +msgid "Register" +msgstr "" + +#: src/components/product/ProductPage.jsx:337 +msgid "Deregister product" +msgstr "" + +#: src/components/product/ProductPage.jsx:370 +msgid "Code:" +msgstr "" + +#: src/components/product/ProductPage.jsx:374 +msgid "Email:" +msgstr "" + +#. TRANSLATORS: section title. +#: src/components/product/ProductPage.jsx:390 +msgid "Registration" +msgstr "" + +#: src/components/product/ProductPage.jsx:399 +msgid "This product requires registration." +msgstr "" + +#: src/components/product/ProductPage.jsx:405 +msgid "This product does not require registration." +msgstr "" + +#: src/components/product/ProductRegistrationForm.jsx:63 +msgid "Registration code" +msgstr "" + +#: src/components/product/ProductRegistrationForm.jsx:66 +msgid "Email" +msgstr "" + +#: src/components/product/ProductSelectionPage.jsx:58 +msgid "Loading available products, please wait..." +msgstr "" + +#. TRANSLATORS: page header +#: src/components/product/ProductSelectionPage.jsx:64 +msgid "Product selection" +msgstr "" + +#. TRANSLATORS: button label +#: src/components/product/ProductSelectionPage.jsx:69 +msgid "Select" +msgstr "" + +#: src/components/product/ProductSelector.jsx:29 +msgid "No products available for selection" +msgstr "" + #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 msgid "Question" @@ -714,10 +958,6 @@ msgstr "" msgid "Encryption Password" msgstr "" -#: src/components/software/ChangeProductLink.jsx:36 -msgid "Change product" -msgstr "" - #. TRANSLATORS: pattern status, selected to install (by user) #: src/components/software/PatternItem.jsx:63 msgid "selected" @@ -735,46 +975,13 @@ msgstr "" #. TRANSLATORS: error summary, always plural, %d is replaced by number of errors (2 or more) #. if there is just a single error then the error is displayed directly instead of this summary -#: src/components/software/PatternSelector.jsx:206 +#: src/components/software/PatternSelector.jsx:207 #, c-format msgid "%d errors" msgstr "" -#: src/components/software/PatternSelector.jsx:210 -msgid "Software summary and filter options" -msgstr "" - -#. TRANSLATORS: search field placeholder text #: src/components/software/PatternSelector.jsx:215 -#: src/components/software/PatternSelector.jsx:216 -msgid "Search" -msgstr "" - -#: src/components/software/ProductSelectionPage.jsx:69 -msgid "Loading available products, please wait..." -msgstr "" - -#. TRANSLATORS: page header -#: src/components/software/ProductSelectionPage.jsx:94 -msgid "Product selection" -msgstr "" - -#. TRANSLATORS: button label -#: src/components/software/ProductSelectionPage.jsx:99 -msgid "Select" -msgstr "" - -#: src/components/software/ProductSelectionPage.jsx:104 -msgid "Choose a product" -msgstr "" - -#: src/components/software/SoftwarePage.jsx:81 -#: src/components/storage/DASDPage.jsx:187 -#: src/components/storage/ISCSIPage.jsx:39 -#: src/components/storage/ProposalPage.jsx:218 -#: src/components/storage/ZFCPPage.jsx:736 -#: src/components/users/UsersPage.jsx:30 -msgid "Back" +msgid "Software summary and filter options" msgstr "" #. TRANSLATORS: %s will be replaced by the estimated installation size, @@ -952,65 +1159,80 @@ msgstr "" msgid "iSCSI" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:135 +#: src/components/storage/ProposalSettingsSection.jsx:139 msgid "Select the device for installing the system." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:140 #: src/components/storage/ProposalSettingsSection.jsx:144 -#: src/components/storage/ProposalSettingsSection.jsx:240 +#: src/components/storage/ProposalSettingsSection.jsx:148 +#: src/components/storage/ProposalSettingsSection.jsx:244 msgid "Installation device" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:150 +#: src/components/storage/ProposalSettingsSection.jsx:154 msgid "No devices found" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:237 +#: src/components/storage/ProposalSettingsSection.jsx:241 msgid "Devices for creating the volume group" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:246 +#: src/components/storage/ProposalSettingsSection.jsx:250 msgid "Custom devices" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:310 +#: src/components/storage/ProposalSettingsSection.jsx:314 msgid "" "Configuration of the system volume group. All the file systems will be " "created in a logical volume of the system volume group." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:316 +#: src/components/storage/ProposalSettingsSection.jsx:320 msgid "Configure the LVM settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:321 -#: src/components/storage/ProposalSettingsSection.jsx:341 +#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:345 msgid "LVM settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:334 +#: src/components/storage/ProposalSettingsSection.jsx:338 msgid "Use logical volume management (LVM)" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:342 +#: src/components/storage/ProposalSettingsSection.jsx:346 msgid "System Volume Group" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:470 +#: src/components/storage/ProposalSettingsSection.jsx:474 msgid "Change encryption password" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:475 -#: src/components/storage/ProposalSettingsSection.jsx:496 +#: src/components/storage/ProposalSettingsSection.jsx:479 +#: src/components/storage/ProposalSettingsSection.jsx:500 msgid "Encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:489 +#: src/components/storage/ProposalSettingsSection.jsx:493 msgid "Use encryption" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:557 +#: src/components/storage/ProposalSettingsSection.jsx:578 +msgid "" +"Select how to make free space in the disks selected for allocating the " +"file systems." +msgstr "" + +#. TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" +#: src/components/storage/ProposalSettingsSection.jsx:584 +msgid "Find space" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:588 +msgid "Space Policy" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:662 msgid "Settings" msgstr "" @@ -1063,48 +1285,48 @@ msgid "partition" msgstr "" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:215 +#: src/components/storage/ProposalVolumes.jsx:216 msgid "encrypted" msgstr "" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:217 +#: src/components/storage/ProposalVolumes.jsx:218 msgid "with snapshots" msgstr "" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:219 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "transactional" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:228 +#: src/components/storage/ProposalVolumes.jsx:229 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:274 +#: src/components/storage/ProposalVolumes.jsx:275 msgid "Edit file system" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:306 +#: src/components/storage/ProposalVolumes.jsx:307 #: src/components/storage/VolumeForm.jsx:500 msgid "Mount point" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:307 +#: src/components/storage/ProposalVolumes.jsx:308 msgid "Details" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:309 #: src/components/storage/VolumeForm.jsx:517 msgid "Size" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:344 +#: src/components/storage/ProposalVolumes.jsx:345 msgid "Table with mount points" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:407 +#: src/components/storage/ProposalVolumes.jsx:408 msgid "File systems to create in your system" msgstr "" @@ -1313,64 +1535,64 @@ msgid "Storage zFCP" msgstr "" #. TRANSLATORS: multipath device type -#: src/components/storage/device-utils.jsx:98 +#: src/components/storage/device-utils.jsx:97 msgid "Multipath" msgstr "" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/device-utils.jsx:103 +#: src/components/storage/device-utils.jsx:102 #, c-format msgid "DASD %s" msgstr "" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/device-utils.jsx:108 +#: src/components/storage/device-utils.jsx:107 #, c-format msgid "Software %s" msgstr "" -#: src/components/storage/device-utils.jsx:113 +#: src/components/storage/device-utils.jsx:112 msgid "SD Card" msgstr "" #. TRANSLATORS: %s is replaced by the device transport name, e.g. USB, SATA, SCSI... -#: src/components/storage/device-utils.jsx:115 +#: src/components/storage/device-utils.jsx:114 #, c-format msgid "Transport %s" msgstr "" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:134 +#: src/components/storage/device-utils.jsx:133 #, c-format msgid "Members: %s" msgstr "" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:143 +#: src/components/storage/device-utils.jsx:142 #, c-format msgid "Devices: %s" msgstr "" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/device-utils.jsx:152 +#: src/components/storage/device-utils.jsx:151 #, c-format msgid "Wires: %s" msgstr "" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/device-utils.jsx:176 +#: src/components/storage/device-utils.jsx:175 #, c-format msgid "%s with %d partitions" msgstr "" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/device-utils.jsx:200 +#: src/components/storage/device-utils.jsx:199 msgid "No content found" msgstr "" -#: src/components/storage/device-utils.jsx:271 +#: src/components/storage/device-utils.jsx:270 msgid "Available devices" msgstr "" @@ -1546,6 +1768,68 @@ msgstr "" msgid "Targets" msgstr "" +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:59 +msgid "Delete current content" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:63 +msgid "Shrink existing partitions" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:67 +msgid "Use available space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:79 +msgid "All partitions will be removed and any data in the disks will be lost." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:82 +msgid "" +"The data is kept, but the current partitions will be resized as needed to " +"make enough space." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:85 +msgid "" +"The data is kept and existing partitions will not be modified. Only the " +"space that is not assigned to any partition will be used." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:112 +msgid "Select a mechanism to make space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:137 +#, c-format +msgid "deleting all content of the installation device" +msgid_plural "deleting all content of the %d selected disks" +msgstr[0] "" +msgstr[1] "" + +#: src/components/storage/space-policy-utils.jsx:147 +#, c-format +msgid "shrinking partitions of the installation device" +msgid_plural "shrinking partitions of the %d selected disks" +msgstr[0] "" +msgstr[1] "" + +#. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence +#. would read as "Find space without modifying any partition". +#: src/components/storage/space-policy-utils.jsx:155 +msgid "without modifying any partition" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:170 +#, c-format +msgid "This will only affect the installation device" +msgid_plural "This will affect the %d disks selected for installation" +msgstr[0] "" +msgstr[1] "" + #: src/components/storage/utils.js:44 msgid "KiB" msgstr "" diff --git a/web/po/es.po b/web/po/es.po index 63dcfe787e..d66fbecc57 100644 --- a/web/po/es.po +++ b/web/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-11-05 02:14+0000\n" +"POT-Creation-Date: 2023-12-03 02:17+0000\n" "PO-Revision-Date: 2023-10-27 13:15+0000\n" "Last-Translator: Victor hck \n" "Language-Team: Spanish \n" "Language-Team: French 1;\n" "X-Generator: Weblate 4.9.1\n" -#: src/App.jsx:112 +#: src/App.jsx:110 msgid "Diagnostic tools" msgstr "Outils de diagnostic" @@ -68,6 +68,7 @@ msgstr "Pour plus d'informations, veuillez consulter le dépôt du projet à %s. #: src/components/core/About.jsx:71 src/components/core/FileViewer.jsx:80 #: src/components/core/Sidebar.jsx:177 src/components/core/Terminal.jsx:48 #: src/components/network/WifiSelector.jsx:151 +#: src/components/product/ProductPage.jsx:239 msgid "Close" msgstr "Fermer" @@ -144,18 +145,23 @@ msgstr "" "Veuillez consulter les erreurs signalées et réessayer." #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:97 -#: src/components/storage/ProposalSettingsSection.jsx:166 -#: src/components/storage/ProposalSettingsSection.jsx:359 -#: src/components/storage/ProposalSettingsSection.jsx:504 +#: src/components/core/InstallButton.jsx:97 src/components/l10n/L10nPage.jsx:75 +#: src/components/l10n/L10nPage.jsx:191 src/components/l10n/L10nPage.jsx:304 +#: src/components/product/ProductPage.jsx:71 +#: src/components/product/ProductPage.jsx:140 +#: src/components/product/ProductPage.jsx:207 +#: src/components/storage/ProposalSettingsSection.jsx:170 +#: src/components/storage/ProposalSettingsSection.jsx:363 +#: src/components/storage/ProposalSettingsSection.jsx:508 +#: src/components/storage/ProposalSettingsSection.jsx:604 #: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:282 +#: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:513 msgid "Accept" msgstr "Accepter" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:145 +#: src/components/core/InstallButton.jsx:148 msgid "Install" msgstr "Installer" @@ -207,10 +213,40 @@ msgstr "Montrer les problèmes" msgid "There are new issues" msgstr "Il y a de nouveaux problèmes" -#: src/components/core/IssuesPage.jsx:115 +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:92 +#: src/components/overview/ProductSection.jsx:71 +#: src/components/product/ProductPage.jsx:434 +msgid "Product" +msgstr "" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section title +#: src/components/core/IssuesPage.jsx:100 +#: src/components/overview/StorageSection.jsx:208 +#: src/components/storage/ProposalPage.jsx:218 +msgid "Storage" +msgstr "Stockage" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:108 +#: src/components/overview/SoftwareSection.jsx:141 +#: src/components/software/SoftwarePage.jsx:81 +msgid "Software" +msgstr "Logiciel" + +#: src/components/core/IssuesPage.jsx:129 msgid "No issues found. Everything looks ok." msgstr "Aucun problème n'a été détecté. Tout semble correct." +#. TRANSLATORS: search field placeholder text +#: src/components/core/ListSearch.jsx:50 +#: src/components/software/PatternSelector.jsx:220 +#: src/components/software/PatternSelector.jsx:221 +msgid "Search" +msgstr "Rechercher" + #: src/components/core/LogsButton.jsx:98 msgid "Collecting logs..." msgstr "Collecte des journaux..." @@ -274,12 +310,11 @@ msgstr "Annuler" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 #: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:309 +#: src/components/storage/ProposalVolumes.jsx:310 msgid "Actions" msgstr "Actions" #: src/components/core/SectionSkeleton.jsx:29 -#: src/components/core/SectionSkeleton.jsx:34 msgid "Waiting" msgstr "En attente" @@ -335,22 +370,132 @@ msgstr[1] "erreurs %d trouvées" msgid "Basic popover" msgstr "Popover basique" -#: src/components/l10n/L10nPage.jsx:72 -#: src/components/l10n/LanguageSwitcher.jsx:50 -msgid "language" +#. TRANSLATORS: placeholder text for search input in the keyboard selector. +#: src/components/l10n/KeymapSelector.jsx:82 +msgid "Filter by description or keymap code" +msgstr "" + +#: src/components/l10n/KeymapSelector.jsx:89 +#, fuzzy +msgid "Available keymaps" +msgstr "Périphériques disponibles" + +#: src/components/l10n/L10nPage.jsx:66 src/components/l10n/L10nPage.jsx:140 +msgid "Select time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:67 +#, c-format +msgid "%s will use the selected time zone." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:128 +msgid "Time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:134 +msgid "Change time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:139 +#, fuzzy +msgid "Time zone not selected yet" +msgstr "non sélectionné" + +#: src/components/l10n/L10nPage.jsx:182 src/components/l10n/L10nPage.jsx:258 +#, fuzzy +msgid "Select language" msgstr "langue" -#. TRANSLATORS: page header +#: src/components/l10n/L10nPage.jsx:183 +#, fuzzy, c-format +msgid "%s will use the selected language." +msgstr "Le système utilisera %s comme langue par défaut." + +#: src/components/l10n/L10nPage.jsx:246 +#, fuzzy +msgid "Language" +msgstr "langue" + +#: src/components/l10n/L10nPage.jsx:252 +#, fuzzy +msgid "Change language" +msgstr "langue" + +#: src/components/l10n/L10nPage.jsx:257 +#, fuzzy +msgid "Language not selected yet" +msgstr "non sélectionné" + +#: src/components/l10n/L10nPage.jsx:295 src/components/l10n/L10nPage.jsx:369 +#, fuzzy +msgid "Select keyboard" +msgstr "sélectionné" + +#: src/components/l10n/L10nPage.jsx:296 +#, c-format +msgid "%s will use the selected keyboard." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:357 +msgid "Keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:363 +#, fuzzy +msgid "Change keyboard" +msgstr "Modifier le mot de passe de chiffrement" + +#: src/components/l10n/L10nPage.jsx:368 +#, fuzzy +msgid "Keyboard not selected yet" +msgstr "non sélectionné" + #. TRANSLATORS: page section -#: src/components/l10n/L10nPage.jsx:84 -#: src/components/overview/L10nSection.jsx:82 +#. TRANSLATORS: page title +#: src/components/l10n/L10nPage.jsx:385 +#: src/components/overview/L10nSection.jsx:52 msgid "Localization" msgstr "Localisation" +#: src/components/l10n/L10nPage.jsx:387 +#: src/components/product/ProductPage.jsx:434 +#: src/components/software/SoftwarePage.jsx:81 +#: src/components/storage/DASDPage.jsx:187 +#: src/components/storage/ISCSIPage.jsx:39 +#: src/components/storage/ProposalPage.jsx:218 +#: src/components/storage/ZFCPPage.jsx:736 +#: src/components/users/UsersPage.jsx:30 +msgid "Back" +msgstr "Retour" + #: src/components/l10n/LanguageSwitcher.jsx:46 msgid "Display Language" msgstr "Langue d'affichage" +#: src/components/l10n/LanguageSwitcher.jsx:50 +msgid "language" +msgstr "langue" + +#: src/components/l10n/LocaleSelector.jsx:82 +msgid "Filter by language, territory or locale code" +msgstr "" + +#: src/components/l10n/LocaleSelector.jsx:89 +#, fuzzy +msgid "Available locales" +msgstr "Périphériques disponibles" + +#. TRANSLATORS: placeholder text for search input in the timezone selector. +#: src/components/l10n/TimezoneSelector.jsx:102 +msgid "Filter by territory, time zone code or UTC offset" +msgstr "" + +#: src/components/l10n/TimezoneSelector.jsx:109 +#, fuzzy +msgid "Available time zones" +msgstr "Périphériques disponibles" + #: src/components/layout/Loading.jsx:30 msgid "Loading installation environment, please wait." msgstr "Chargement de l'environnement d'installation, veuillez patienter." @@ -411,7 +556,7 @@ msgid "IP addresses" msgstr "Adresses IP" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:233 +#: src/components/storage/ProposalVolumes.jsx:234 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:170 @@ -555,6 +700,8 @@ msgid "WPA & WPA2 Personal" msgstr "WPA & WPA2 Personal" #: src/components/network/WifiConnectionForm.jsx:91 +#: src/components/product/ProductPage.jsx:128 +#: src/components/product/ProductPage.jsx:194 #: src/components/storage/ZFCPDiskForm.jsx:112 #: src/components/storage/iscsi/DiscoverForm.jsx:108 #: src/components/storage/iscsi/LoginForm.jsx:72 @@ -627,9 +774,9 @@ msgstr "La connexion %s attend un changement d'état" msgid "Forget network" msgstr "Oublier le réseau" -#. TRANSLATORS: %s will be replaced by a language name and code, -#. example: "English (en_US.UTF-8)" -#: src/components/overview/L10nSection.jsx:70 +#. TRANSLATORS: %s will be replaced by a language name and territory, example: +#. "English (United States)". +#: src/components/overview/L10nSection.jsx:34 #, c-format msgid "The system will use %s as its default language." msgstr "Le système utilisera %s comme langue par défaut." @@ -647,46 +794,67 @@ msgid_plural "%d connections set:" msgstr[0] "Connexion %d établie:" msgstr[1] "Connexions %d établies:" +#. TRANSLATORS: page title +#: src/components/overview/Overview.jsx:47 +#, fuzzy +msgid "Installation Summary" +msgstr "Périphérique d'installation" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/overview/ProductSection.jsx:48 +#, c-format +msgid "%s (registered)" +msgstr "" + #: src/components/overview/SoftwareSection.jsx:37 msgid "Reading software repositories" msgstr "Lecture des dépôts de logiciels" #. TRANSLATORS: clickable link label -#: src/components/overview/SoftwareSection.jsx:135 +#: src/components/overview/SoftwareSection.jsx:131 msgid "Refresh the repositories" msgstr "Rafraîchir les dépôts" -#. TRANSLATORS: page section -#. TRANSLATORS: page title -#: src/components/overview/SoftwareSection.jsx:145 -#: src/components/software/SoftwarePage.jsx:81 -msgid "Software" -msgstr "Logiciel" - -#: src/components/overview/StorageSection.jsx:36 -#: src/components/storage/ProposalSettingsSection.jsx:122 +#: src/components/overview/StorageSection.jsx:42 +#: src/components/storage/ProposalSettingsSection.jsx:126 msgid "No device selected yet" msgstr "Aucun périphérique n'a encore été sélectionné" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:44 +#: src/components/overview/StorageSection.jsx:52 +#, fuzzy, c-format +msgid "Install using device %s shrinking existing partitions as needed" +msgstr "" +"Installer en utilisant le périphérique %s et en supprimant tout son contenu" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:56 +#, fuzzy, c-format +msgid "Install using device %s without modifying existing partitions" +msgstr "" +"Installer en utilisant le périphérique %s et en supprimant tout son contenu" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:60 #, c-format msgid "Install using device %s and deleting all its content" msgstr "" "Installer en utilisant le périphérique %s et en supprimant tout son contenu" -#: src/components/overview/StorageSection.jsx:57 +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:66 +#, fuzzy, c-format +msgid "Install using device %s" +msgstr "Périphérique d'installation" + +#: src/components/overview/StorageSection.jsx:83 msgid "Probing storage devices" msgstr "Sonder les périphériques de stockage" -#. TRANSLATORS: page title -#. TRANSLATORS: page section title -#: src/components/overview/StorageSection.jsx:182 -#: src/components/storage/ProposalPage.jsx:218 -msgid "Storage" -msgstr "Stockage" - #. TRANSLATORS: %s will be replaced by the user name #: src/components/overview/UsersSection.jsx:80 #, c-format @@ -722,6 +890,99 @@ msgstr "Authentification root pour l'utilisation d'une clé SSH publique" msgid "Users" msgstr "Utilisateurs" +#: src/components/product/ProductPage.jsx:63 +#: src/components/product/ProductSelectionPage.jsx:73 +msgid "Choose a product" +msgstr "Choisir un produit" + +#: src/components/product/ProductPage.jsx:122 +#, c-format +msgid "Register %s" +msgstr "" + +#: src/components/product/ProductPage.jsx:188 +#, c-format +msgid "Deregister %s" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:202 +#, c-format +msgid "Do you want to deregister %s?" +msgstr "" + +#: src/components/product/ProductPage.jsx:227 +msgid "Registered warning" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:232 +#, c-format +msgid "The product %s must be deregistered before selecting a new product." +msgstr "" + +#: src/components/product/ProductPage.jsx:263 +msgid "Change product" +msgstr "Changer de produit" + +#: src/components/product/ProductPage.jsx:305 +msgid "Register" +msgstr "" + +#: src/components/product/ProductPage.jsx:337 +#, fuzzy +msgid "Deregister product" +msgstr "Changer de produit" + +#: src/components/product/ProductPage.jsx:370 +msgid "Code:" +msgstr "" + +#: src/components/product/ProductPage.jsx:374 +msgid "Email:" +msgstr "" + +#. TRANSLATORS: section title. +#: src/components/product/ProductPage.jsx:390 +#, fuzzy +msgid "Registration" +msgstr "Question" + +#: src/components/product/ProductPage.jsx:399 +msgid "This product requires registration." +msgstr "" + +#: src/components/product/ProductPage.jsx:405 +msgid "This product does not require registration." +msgstr "" + +#: src/components/product/ProductRegistrationForm.jsx:63 +msgid "Registration code" +msgstr "" + +#: src/components/product/ProductRegistrationForm.jsx:66 +msgid "Email" +msgstr "" + +#: src/components/product/ProductSelectionPage.jsx:58 +msgid "Loading available products, please wait..." +msgstr "Chargement des produits disponibles, veuillez patienter..." + +#. TRANSLATORS: page header +#: src/components/product/ProductSelectionPage.jsx:64 +msgid "Product selection" +msgstr "Sélection des produits" + +#. TRANSLATORS: button label +#: src/components/product/ProductSelectionPage.jsx:69 +msgid "Select" +msgstr "Sélectionner" + +#: src/components/product/ProductSelector.jsx:29 +#, fuzzy +msgid "No products available for selection" +msgstr "Sélection des produits" + #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 msgid "Question" @@ -741,10 +1002,6 @@ msgstr "Appareil chiffré" msgid "Encryption Password" msgstr "Mot de passe de chiffrement" -#: src/components/software/ChangeProductLink.jsx:36 -msgid "Change product" -msgstr "Changer de produit" - #. TRANSLATORS: pattern status, selected to install (by user) #: src/components/software/PatternItem.jsx:63 msgid "selected" @@ -762,48 +1019,15 @@ msgstr "non sélectionné" #. TRANSLATORS: error summary, always plural, %d is replaced by number of errors (2 or more) #. if there is just a single error then the error is displayed directly instead of this summary -#: src/components/software/PatternSelector.jsx:206 +#: src/components/software/PatternSelector.jsx:207 #, c-format msgid "%d errors" msgstr "%d erreurs" -#: src/components/software/PatternSelector.jsx:210 +#: src/components/software/PatternSelector.jsx:215 msgid "Software summary and filter options" msgstr "Synthèse logiciel et options de filtrage" -#. TRANSLATORS: search field placeholder text -#: src/components/software/PatternSelector.jsx:215 -#: src/components/software/PatternSelector.jsx:216 -msgid "Search" -msgstr "Rechercher" - -#: src/components/software/ProductSelectionPage.jsx:69 -msgid "Loading available products, please wait..." -msgstr "Chargement des produits disponibles, veuillez patienter..." - -#. TRANSLATORS: page header -#: src/components/software/ProductSelectionPage.jsx:94 -msgid "Product selection" -msgstr "Sélection des produits" - -#. TRANSLATORS: button label -#: src/components/software/ProductSelectionPage.jsx:99 -msgid "Select" -msgstr "Sélectionner" - -#: src/components/software/ProductSelectionPage.jsx:104 -msgid "Choose a product" -msgstr "Choisir un produit" - -#: src/components/software/SoftwarePage.jsx:81 -#: src/components/storage/DASDPage.jsx:187 -#: src/components/storage/ISCSIPage.jsx:39 -#: src/components/storage/ProposalPage.jsx:218 -#: src/components/storage/ZFCPPage.jsx:736 -#: src/components/users/UsersPage.jsx:30 -msgid "Back" -msgstr "Retour" - #. TRANSLATORS: %s will be replaced by the estimated installation size, #. example: "728.8 MiB" #: src/components/software/UsedSize.jsx:32 @@ -983,29 +1207,29 @@ msgstr "Se connecter aux cibles iSCSI" msgid "iSCSI" msgstr "iSCSI" -#: src/components/storage/ProposalSettingsSection.jsx:135 +#: src/components/storage/ProposalSettingsSection.jsx:139 msgid "Select the device for installing the system." msgstr "Sélectionner le périphérique pour l'installation du système." -#: src/components/storage/ProposalSettingsSection.jsx:140 #: src/components/storage/ProposalSettingsSection.jsx:144 -#: src/components/storage/ProposalSettingsSection.jsx:240 +#: src/components/storage/ProposalSettingsSection.jsx:148 +#: src/components/storage/ProposalSettingsSection.jsx:244 msgid "Installation device" msgstr "Périphérique d'installation" -#: src/components/storage/ProposalSettingsSection.jsx:150 +#: src/components/storage/ProposalSettingsSection.jsx:154 msgid "No devices found" msgstr "Aucun périphérique n'a été trouvé" -#: src/components/storage/ProposalSettingsSection.jsx:237 +#: src/components/storage/ProposalSettingsSection.jsx:241 msgid "Devices for creating the volume group" msgstr "Périphériques pour la création du groupe de volumes" -#: src/components/storage/ProposalSettingsSection.jsx:246 +#: src/components/storage/ProposalSettingsSection.jsx:250 msgid "Custom devices" msgstr "Périphériques personnalisés" -#: src/components/storage/ProposalSettingsSection.jsx:310 +#: src/components/storage/ProposalSettingsSection.jsx:314 msgid "" "Configuration of the system volume group. All the file systems will be " "created in a logical volume of the system volume group." @@ -1013,37 +1237,52 @@ msgstr "" "Configuration du groupe de volumes système. Tous les systèmes de fichiers " "seront créés dans un volume logique du groupe de volume système." -#: src/components/storage/ProposalSettingsSection.jsx:316 +#: src/components/storage/ProposalSettingsSection.jsx:320 msgid "Configure the LVM settings" msgstr "Configurer les paramètres LVM" -#: src/components/storage/ProposalSettingsSection.jsx:321 -#: src/components/storage/ProposalSettingsSection.jsx:341 +#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:345 msgid "LVM settings" msgstr "Paramètres LVM" -#: src/components/storage/ProposalSettingsSection.jsx:334 +#: src/components/storage/ProposalSettingsSection.jsx:338 msgid "Use logical volume management (LVM)" msgstr "Utiliser la gestion des volumes logiques (LVM)" -#: src/components/storage/ProposalSettingsSection.jsx:342 +#: src/components/storage/ProposalSettingsSection.jsx:346 msgid "System Volume Group" msgstr "Groupe de volume système" -#: src/components/storage/ProposalSettingsSection.jsx:470 +#: src/components/storage/ProposalSettingsSection.jsx:474 msgid "Change encryption password" msgstr "Modifier le mot de passe de chiffrement" -#: src/components/storage/ProposalSettingsSection.jsx:475 -#: src/components/storage/ProposalSettingsSection.jsx:496 +#: src/components/storage/ProposalSettingsSection.jsx:479 +#: src/components/storage/ProposalSettingsSection.jsx:500 msgid "Encryption settings" msgstr "Paramètres de chiffrement" -#: src/components/storage/ProposalSettingsSection.jsx:489 +#: src/components/storage/ProposalSettingsSection.jsx:493 msgid "Use encryption" msgstr "Utiliser le chiffrement" -#: src/components/storage/ProposalSettingsSection.jsx:557 +#: src/components/storage/ProposalSettingsSection.jsx:578 +msgid "" +"Select how to make free space in the disks selected for allocating the " +"file systems." +msgstr "" + +#. TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" +#: src/components/storage/ProposalSettingsSection.jsx:584 +msgid "Find space" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:588 +msgid "Space Policy" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:662 msgid "Settings" msgstr "Paramètres" @@ -1096,48 +1335,48 @@ msgid "partition" msgstr "partition" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:215 +#: src/components/storage/ProposalVolumes.jsx:216 msgid "encrypted" msgstr "chiffré" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:217 +#: src/components/storage/ProposalVolumes.jsx:218 msgid "with snapshots" msgstr "avec des clichés" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:219 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "transactional" msgstr "transactionnel" -#: src/components/storage/ProposalVolumes.jsx:228 +#: src/components/storage/ProposalVolumes.jsx:229 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "Supprimer" -#: src/components/storage/ProposalVolumes.jsx:274 +#: src/components/storage/ProposalVolumes.jsx:275 msgid "Edit file system" msgstr "Modifier le système de fichiers" -#: src/components/storage/ProposalVolumes.jsx:306 +#: src/components/storage/ProposalVolumes.jsx:307 #: src/components/storage/VolumeForm.jsx:500 msgid "Mount point" msgstr "Point de montage" -#: src/components/storage/ProposalVolumes.jsx:307 +#: src/components/storage/ProposalVolumes.jsx:308 msgid "Details" msgstr "Détails" -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:309 #: src/components/storage/VolumeForm.jsx:517 msgid "Size" msgstr "Taille" -#: src/components/storage/ProposalVolumes.jsx:344 +#: src/components/storage/ProposalVolumes.jsx:345 msgid "Table with mount points" msgstr "Table avec points de montage" -#: src/components/storage/ProposalVolumes.jsx:407 +#: src/components/storage/ProposalVolumes.jsx:408 msgid "File systems to create in your system" msgstr "Systèmes de fichiers à créer dans votre système" @@ -1354,64 +1593,64 @@ msgid "Storage zFCP" msgstr "Stockage zFCP" #. TRANSLATORS: multipath device type -#: src/components/storage/device-utils.jsx:98 +#: src/components/storage/device-utils.jsx:97 msgid "Multipath" msgstr "Chemins multiples" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/device-utils.jsx:103 +#: src/components/storage/device-utils.jsx:102 #, c-format msgid "DASD %s" msgstr "DASD %s" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/device-utils.jsx:108 +#: src/components/storage/device-utils.jsx:107 #, c-format msgid "Software %s" msgstr "Logiciel %s" -#: src/components/storage/device-utils.jsx:113 +#: src/components/storage/device-utils.jsx:112 msgid "SD Card" msgstr "Carte SD" #. TRANSLATORS: %s is replaced by the device transport name, e.g. USB, SATA, SCSI... -#: src/components/storage/device-utils.jsx:115 +#: src/components/storage/device-utils.jsx:114 #, c-format msgid "Transport %s" msgstr "Transport %s" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:134 +#: src/components/storage/device-utils.jsx:133 #, c-format msgid "Members: %s" msgstr "Membres : %s" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:143 +#: src/components/storage/device-utils.jsx:142 #, c-format msgid "Devices: %s" msgstr "Périphériques : %s" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/device-utils.jsx:152 +#: src/components/storage/device-utils.jsx:151 #, c-format msgid "Wires: %s" msgstr "Chemins: %s" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/device-utils.jsx:176 +#: src/components/storage/device-utils.jsx:175 #, c-format msgid "%s with %d partitions" msgstr "%s avec %d partitions" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/device-utils.jsx:200 +#: src/components/storage/device-utils.jsx:199 msgid "No content found" msgstr "Aucun contenu n'a été trouvé" -#: src/components/storage/device-utils.jsx:271 +#: src/components/storage/device-utils.jsx:270 msgid "Available devices" msgstr "Périphériques disponibles" @@ -1589,6 +1828,70 @@ msgstr "Découvrir" msgid "Targets" msgstr "Cibles" +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:59 +msgid "Delete current content" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:63 +msgid "Shrink existing partitions" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:67 +#, fuzzy +msgid "Use available space" +msgstr "Périphériques disponibles" + +#: src/components/storage/space-policy-utils.jsx:79 +msgid "All partitions will be removed and any data in the disks will be lost." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:82 +msgid "" +"The data is kept, but the current partitions will be resized as needed to " +"make enough space." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:85 +msgid "" +"The data is kept and existing partitions will not be modified. Only the " +"space that is not assigned to any partition will be used." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:112 +msgid "Select a mechanism to make space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:137 +#, c-format +msgid "deleting all content of the installation device" +msgid_plural "deleting all content of the %d selected disks" +msgstr[0] "" +msgstr[1] "" + +#: src/components/storage/space-policy-utils.jsx:147 +#, c-format +msgid "shrinking partitions of the installation device" +msgid_plural "shrinking partitions of the %d selected disks" +msgstr[0] "" +msgstr[1] "" + +#. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence +#. would read as "Find space without modifying any partition". +#: src/components/storage/space-policy-utils.jsx:155 +#, fuzzy +msgid "without modifying any partition" +msgstr "%s avec %d partitions" + +#: src/components/storage/space-policy-utils.jsx:170 +#, c-format +msgid "This will only affect the installation device" +msgid_plural "This will affect the %d disks selected for installation" +msgstr[0] "" +msgstr[1] "" + #: src/components/storage/utils.js:44 msgid "KiB" msgstr "KiB" diff --git a/web/po/ja.po b/web/po/ja.po index 26db6fb39b..2c34aeb395 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-11-05 02:14+0000\n" -"PO-Revision-Date: 2023-11-01 05:15+0000\n" +"POT-Creation-Date: 2023-12-03 02:17+0000\n" +"PO-Revision-Date: 2023-11-29 01:01+0000\n" "Last-Translator: Yasuhiko Kamata \n" "Language-Team: Japanese \n" @@ -19,7 +19,7 @@ msgstr "" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Weblate 4.9.1\n" -#: src/App.jsx:112 +#: src/App.jsx:110 msgid "Diagnostic tools" msgstr "診断ツール" @@ -66,6 +66,7 @@ msgstr "詳しくは %s にあるプロジェクトのリポジトリをご覧 #: src/components/core/About.jsx:71 src/components/core/FileViewer.jsx:80 #: src/components/core/Sidebar.jsx:177 src/components/core/Terminal.jsx:48 #: src/components/network/WifiSelector.jsx:151 +#: src/components/product/ProductPage.jsx:239 msgid "Close" msgstr "閉じる" @@ -143,18 +144,23 @@ msgstr "" "をご確認のうえ、やり直してください。" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:97 -#: src/components/storage/ProposalSettingsSection.jsx:166 -#: src/components/storage/ProposalSettingsSection.jsx:359 -#: src/components/storage/ProposalSettingsSection.jsx:504 +#: src/components/core/InstallButton.jsx:97 src/components/l10n/L10nPage.jsx:75 +#: src/components/l10n/L10nPage.jsx:191 src/components/l10n/L10nPage.jsx:304 +#: src/components/product/ProductPage.jsx:71 +#: src/components/product/ProductPage.jsx:140 +#: src/components/product/ProductPage.jsx:207 +#: src/components/storage/ProposalSettingsSection.jsx:170 +#: src/components/storage/ProposalSettingsSection.jsx:363 +#: src/components/storage/ProposalSettingsSection.jsx:508 +#: src/components/storage/ProposalSettingsSection.jsx:604 #: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:282 +#: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:513 msgid "Accept" msgstr "受け入れる" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:145 +#: src/components/core/InstallButton.jsx:148 msgid "Install" msgstr "インストール" @@ -204,11 +210,41 @@ msgstr "問題点の表示" msgid "There are new issues" msgstr "新しい問題点があります" -#: src/components/core/IssuesPage.jsx:115 +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:92 +#: src/components/overview/ProductSection.jsx:71 +#: src/components/product/ProductPage.jsx:434 +msgid "Product" +msgstr "製品" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section title +#: src/components/core/IssuesPage.jsx:100 +#: src/components/overview/StorageSection.jsx:208 +#: src/components/storage/ProposalPage.jsx:218 +msgid "Storage" +msgstr "ストレージ" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:108 +#: src/components/overview/SoftwareSection.jsx:141 +#: src/components/software/SoftwarePage.jsx:81 +msgid "Software" +msgstr "ソフトウエア" + +#: src/components/core/IssuesPage.jsx:129 msgid "No issues found. Everything looks ok." msgstr "" "問題点は見つかりませんでした。インストールに支障はないものと思われます。" +#. TRANSLATORS: search field placeholder text +#: src/components/core/ListSearch.jsx:50 +#: src/components/software/PatternSelector.jsx:220 +#: src/components/software/PatternSelector.jsx:221 +msgid "Search" +msgstr "検索" + #: src/components/core/LogsButton.jsx:98 msgid "Collecting logs..." msgstr "ログを収集しています..." @@ -270,12 +306,11 @@ msgstr "キャンセル" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 #: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:309 +#: src/components/storage/ProposalVolumes.jsx:310 msgid "Actions" msgstr "処理" #: src/components/core/SectionSkeleton.jsx:29 -#: src/components/core/SectionSkeleton.jsx:34 msgid "Waiting" msgstr "お待ちください" @@ -330,22 +365,132 @@ msgstr[0] "%d 個のエラーが見つかりました" msgid "Basic popover" msgstr "基本ポップオーバー" -#: src/components/l10n/L10nPage.jsx:72 -#: src/components/l10n/LanguageSwitcher.jsx:50 -msgid "language" +#. TRANSLATORS: placeholder text for search input in the keyboard selector. +#: src/components/l10n/KeymapSelector.jsx:82 +msgid "Filter by description or keymap code" +msgstr "" + +#: src/components/l10n/KeymapSelector.jsx:89 +#, fuzzy +msgid "Available keymaps" +msgstr "利用可能なデバイス" + +#: src/components/l10n/L10nPage.jsx:66 src/components/l10n/L10nPage.jsx:140 +msgid "Select time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:67 +#, c-format +msgid "%s will use the selected time zone." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:128 +msgid "Time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:134 +msgid "Change time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:139 +#, fuzzy +msgid "Time zone not selected yet" +msgstr "未選択" + +#: src/components/l10n/L10nPage.jsx:182 src/components/l10n/L10nPage.jsx:258 +#, fuzzy +msgid "Select language" msgstr "言語" -#. TRANSLATORS: page header +#: src/components/l10n/L10nPage.jsx:183 +#, fuzzy, c-format +msgid "%s will use the selected language." +msgstr "システムは %s を既定の言語として使用します。" + +#: src/components/l10n/L10nPage.jsx:246 +#, fuzzy +msgid "Language" +msgstr "言語" + +#: src/components/l10n/L10nPage.jsx:252 +#, fuzzy +msgid "Change language" +msgstr "言語" + +#: src/components/l10n/L10nPage.jsx:257 +#, fuzzy +msgid "Language not selected yet" +msgstr "未選択" + +#: src/components/l10n/L10nPage.jsx:295 src/components/l10n/L10nPage.jsx:369 +#, fuzzy +msgid "Select keyboard" +msgstr "選択済み" + +#: src/components/l10n/L10nPage.jsx:296 +#, c-format +msgid "%s will use the selected keyboard." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:357 +msgid "Keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:363 +#, fuzzy +msgid "Change keyboard" +msgstr "暗号化パスワードを変更する" + +#: src/components/l10n/L10nPage.jsx:368 +#, fuzzy +msgid "Keyboard not selected yet" +msgstr "未選択" + #. TRANSLATORS: page section -#: src/components/l10n/L10nPage.jsx:84 -#: src/components/overview/L10nSection.jsx:82 +#. TRANSLATORS: page title +#: src/components/l10n/L10nPage.jsx:385 +#: src/components/overview/L10nSection.jsx:52 msgid "Localization" msgstr "ローカライゼーション" +#: src/components/l10n/L10nPage.jsx:387 +#: src/components/product/ProductPage.jsx:434 +#: src/components/software/SoftwarePage.jsx:81 +#: src/components/storage/DASDPage.jsx:187 +#: src/components/storage/ISCSIPage.jsx:39 +#: src/components/storage/ProposalPage.jsx:218 +#: src/components/storage/ZFCPPage.jsx:736 +#: src/components/users/UsersPage.jsx:30 +msgid "Back" +msgstr "戻る" + #: src/components/l10n/LanguageSwitcher.jsx:46 msgid "Display Language" msgstr "表示言語" +#: src/components/l10n/LanguageSwitcher.jsx:50 +msgid "language" +msgstr "言語" + +#: src/components/l10n/LocaleSelector.jsx:82 +msgid "Filter by language, territory or locale code" +msgstr "" + +#: src/components/l10n/LocaleSelector.jsx:89 +#, fuzzy +msgid "Available locales" +msgstr "利用可能なデバイス" + +#. TRANSLATORS: placeholder text for search input in the timezone selector. +#: src/components/l10n/TimezoneSelector.jsx:102 +msgid "Filter by territory, time zone code or UTC offset" +msgstr "" + +#: src/components/l10n/TimezoneSelector.jsx:109 +#, fuzzy +msgid "Available time zones" +msgstr "利用可能なデバイス" + #: src/components/layout/Loading.jsx:30 msgid "Loading installation environment, please wait." msgstr "インストール環境を読み込んでいます。しばらくお待ちください。" @@ -406,7 +551,7 @@ msgid "IP addresses" msgstr "IP アドレス" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:233 +#: src/components/storage/ProposalVolumes.jsx:234 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:170 @@ -551,6 +696,8 @@ msgid "WPA & WPA2 Personal" msgstr "WPA および WPA2 Personal" #: src/components/network/WifiConnectionForm.jsx:91 +#: src/components/product/ProductPage.jsx:128 +#: src/components/product/ProductPage.jsx:194 #: src/components/storage/ZFCPDiskForm.jsx:112 #: src/components/storage/iscsi/DiscoverForm.jsx:108 #: src/components/storage/iscsi/LoginForm.jsx:72 @@ -623,9 +770,9 @@ msgstr "接続 %s は状態の変化を待っています" msgid "Forget network" msgstr "ネットワークの削除" -#. TRANSLATORS: %s will be replaced by a language name and code, -#. example: "English (en_US.UTF-8)" -#: src/components/overview/L10nSection.jsx:70 +#. TRANSLATORS: %s will be replaced by a language name and territory, example: +#. "English (United States)". +#: src/components/overview/L10nSection.jsx:34 #, c-format msgid "The system will use %s as its default language." msgstr "システムは %s を既定の言語として使用します。" @@ -642,45 +789,63 @@ msgid "%d connection set:" msgid_plural "%d connections set:" msgstr[0] "%d 個の接続が設定されています:" +#. TRANSLATORS: page title +#: src/components/overview/Overview.jsx:47 +msgid "Installation Summary" +msgstr "インストールの概要" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/overview/ProductSection.jsx:48 +#, c-format +msgid "%s (registered)" +msgstr "%s (登録済み)" + #: src/components/overview/SoftwareSection.jsx:37 msgid "Reading software repositories" msgstr "ソフトウエアリポジトリを読み込んでいます" #. TRANSLATORS: clickable link label -#: src/components/overview/SoftwareSection.jsx:135 +#: src/components/overview/SoftwareSection.jsx:131 msgid "Refresh the repositories" msgstr "リポジトリの更新" -#. TRANSLATORS: page section -#. TRANSLATORS: page title -#: src/components/overview/SoftwareSection.jsx:145 -#: src/components/software/SoftwarePage.jsx:81 -msgid "Software" -msgstr "ソフトウエア" - -#: src/components/overview/StorageSection.jsx:36 -#: src/components/storage/ProposalSettingsSection.jsx:122 +#: src/components/overview/StorageSection.jsx:42 +#: src/components/storage/ProposalSettingsSection.jsx:126 msgid "No device selected yet" msgstr "まだ何もデバイスを選択していません" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:44 +#: src/components/overview/StorageSection.jsx:52 +#, fuzzy, c-format +msgid "Install using device %s shrinking existing partitions as needed" +msgstr "デバイス %s の内容を全て削除してインストールします" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:56 +#, fuzzy, c-format +msgid "Install using device %s without modifying existing partitions" +msgstr "デバイス %s の内容を全て削除してインストールします" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:60 #, c-format msgid "Install using device %s and deleting all its content" msgstr "デバイス %s の内容を全て削除してインストールします" -#: src/components/overview/StorageSection.jsx:57 +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:66 +#, fuzzy, c-format +msgid "Install using device %s" +msgstr "インストール先のデバイス" + +#: src/components/overview/StorageSection.jsx:83 msgid "Probing storage devices" msgstr "ストレージデバイスを検出しています" -#. TRANSLATORS: page title -#. TRANSLATORS: page section title -#: src/components/overview/StorageSection.jsx:182 -#: src/components/storage/ProposalPage.jsx:218 -msgid "Storage" -msgstr "ストレージ" - #. TRANSLATORS: %s will be replaced by the user name #: src/components/overview/UsersSection.jsx:80 #, c-format @@ -714,6 +879,96 @@ msgstr "公開 SSH 鍵による root 認証を設定済み" msgid "Users" msgstr "ユーザ" +#: src/components/product/ProductPage.jsx:63 +#: src/components/product/ProductSelectionPage.jsx:73 +msgid "Choose a product" +msgstr "製品を選択してください" + +#: src/components/product/ProductPage.jsx:122 +#, c-format +msgid "Register %s" +msgstr "%s の登録" + +#: src/components/product/ProductPage.jsx:188 +#, c-format +msgid "Deregister %s" +msgstr "%s の登録解除" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:202 +#, c-format +msgid "Do you want to deregister %s?" +msgstr "%s を登録解除してよろしいですか?" + +#: src/components/product/ProductPage.jsx:227 +msgid "Registered warning" +msgstr "登録済みによる警告" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:232 +#, c-format +msgid "The product %s must be deregistered before selecting a new product." +msgstr "新しい製品を選択する前に、製品 %s の登録を解除しなければなりません。" + +#: src/components/product/ProductPage.jsx:263 +msgid "Change product" +msgstr "製品の変更" + +#: src/components/product/ProductPage.jsx:305 +msgid "Register" +msgstr "登録" + +#: src/components/product/ProductPage.jsx:337 +msgid "Deregister product" +msgstr "製品の登録解除" + +#: src/components/product/ProductPage.jsx:370 +msgid "Code:" +msgstr "コード:" + +#: src/components/product/ProductPage.jsx:374 +msgid "Email:" +msgstr "電子メール:" + +#. TRANSLATORS: section title. +#: src/components/product/ProductPage.jsx:390 +msgid "Registration" +msgstr "登録" + +#: src/components/product/ProductPage.jsx:399 +msgid "This product requires registration." +msgstr "この製品には登録が必要です。" + +#: src/components/product/ProductPage.jsx:405 +msgid "This product does not require registration." +msgstr "この製品には登録は不要です。" + +#: src/components/product/ProductRegistrationForm.jsx:63 +msgid "Registration code" +msgstr "登録コード" + +#: src/components/product/ProductRegistrationForm.jsx:66 +msgid "Email" +msgstr "電子メール" + +#: src/components/product/ProductSelectionPage.jsx:58 +msgid "Loading available products, please wait..." +msgstr "利用可能な製品を読み込んでいます。しばらくお待ちください..." + +#. TRANSLATORS: page header +#: src/components/product/ProductSelectionPage.jsx:64 +msgid "Product selection" +msgstr "製品の選択" + +#. TRANSLATORS: button label +#: src/components/product/ProductSelectionPage.jsx:69 +msgid "Select" +msgstr "選択" + +#: src/components/product/ProductSelector.jsx:29 +msgid "No products available for selection" +msgstr "選択できる製品がありません" + #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 msgid "Question" @@ -733,10 +988,6 @@ msgstr "暗号化されたデバイス" msgid "Encryption Password" msgstr "暗号化パスワード" -#: src/components/software/ChangeProductLink.jsx:36 -msgid "Change product" -msgstr "製品の変更" - #. TRANSLATORS: pattern status, selected to install (by user) #: src/components/software/PatternItem.jsx:63 msgid "selected" @@ -754,48 +1005,15 @@ msgstr "未選択" #. TRANSLATORS: error summary, always plural, %d is replaced by number of errors (2 or more) #. if there is just a single error then the error is displayed directly instead of this summary -#: src/components/software/PatternSelector.jsx:206 +#: src/components/software/PatternSelector.jsx:207 #, c-format msgid "%d errors" msgstr "%d 個のエラー" -#: src/components/software/PatternSelector.jsx:210 +#: src/components/software/PatternSelector.jsx:215 msgid "Software summary and filter options" msgstr "ソフトウエアの概要とフィルタのオプション" -#. TRANSLATORS: search field placeholder text -#: src/components/software/PatternSelector.jsx:215 -#: src/components/software/PatternSelector.jsx:216 -msgid "Search" -msgstr "検索" - -#: src/components/software/ProductSelectionPage.jsx:69 -msgid "Loading available products, please wait..." -msgstr "利用可能な製品を読み込んでいます。しばらくお待ちください..." - -#. TRANSLATORS: page header -#: src/components/software/ProductSelectionPage.jsx:94 -msgid "Product selection" -msgstr "製品の選択" - -#. TRANSLATORS: button label -#: src/components/software/ProductSelectionPage.jsx:99 -msgid "Select" -msgstr "選択" - -#: src/components/software/ProductSelectionPage.jsx:104 -msgid "Choose a product" -msgstr "製品を選択してください" - -#: src/components/software/SoftwarePage.jsx:81 -#: src/components/storage/DASDPage.jsx:187 -#: src/components/storage/ISCSIPage.jsx:39 -#: src/components/storage/ProposalPage.jsx:218 -#: src/components/storage/ZFCPPage.jsx:736 -#: src/components/users/UsersPage.jsx:30 -msgid "Back" -msgstr "戻る" - #. TRANSLATORS: %s will be replaced by the estimated installation size, #. example: "728.8 MiB" #: src/components/software/UsedSize.jsx:32 @@ -971,29 +1189,29 @@ msgstr "iSCSI ターゲットへの接続" msgid "iSCSI" msgstr "iSCSI" -#: src/components/storage/ProposalSettingsSection.jsx:135 +#: src/components/storage/ProposalSettingsSection.jsx:139 msgid "Select the device for installing the system." msgstr "システムのインストール先デバイスを選択してください。" -#: src/components/storage/ProposalSettingsSection.jsx:140 #: src/components/storage/ProposalSettingsSection.jsx:144 -#: src/components/storage/ProposalSettingsSection.jsx:240 +#: src/components/storage/ProposalSettingsSection.jsx:148 +#: src/components/storage/ProposalSettingsSection.jsx:244 msgid "Installation device" msgstr "インストール先のデバイス" -#: src/components/storage/ProposalSettingsSection.jsx:150 +#: src/components/storage/ProposalSettingsSection.jsx:154 msgid "No devices found" msgstr "デバイスが見つかりませんでした" -#: src/components/storage/ProposalSettingsSection.jsx:237 +#: src/components/storage/ProposalSettingsSection.jsx:241 msgid "Devices for creating the volume group" msgstr "ボリュームグループを作成するデバイス" -#: src/components/storage/ProposalSettingsSection.jsx:246 +#: src/components/storage/ProposalSettingsSection.jsx:250 msgid "Custom devices" msgstr "独自のデバイス" -#: src/components/storage/ProposalSettingsSection.jsx:310 +#: src/components/storage/ProposalSettingsSection.jsx:314 msgid "" "Configuration of the system volume group. All the file systems will be " "created in a logical volume of the system volume group." @@ -1001,37 +1219,52 @@ msgstr "" "システムのボリュームグループ向けの設定です。全てのファイルシステムは、システ" "ムのボリュームグループにある論理ボリューム内に作成されます。" -#: src/components/storage/ProposalSettingsSection.jsx:316 +#: src/components/storage/ProposalSettingsSection.jsx:320 msgid "Configure the LVM settings" msgstr "LVM 設定" -#: src/components/storage/ProposalSettingsSection.jsx:321 -#: src/components/storage/ProposalSettingsSection.jsx:341 +#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:345 msgid "LVM settings" msgstr "LVM 設定" -#: src/components/storage/ProposalSettingsSection.jsx:334 +#: src/components/storage/ProposalSettingsSection.jsx:338 msgid "Use logical volume management (LVM)" msgstr "論理ボリュームマネージャ (LVM) を使用する" -#: src/components/storage/ProposalSettingsSection.jsx:342 +#: src/components/storage/ProposalSettingsSection.jsx:346 msgid "System Volume Group" msgstr "システムのボリュームグループ" -#: src/components/storage/ProposalSettingsSection.jsx:470 +#: src/components/storage/ProposalSettingsSection.jsx:474 msgid "Change encryption password" msgstr "暗号化パスワードを変更する" -#: src/components/storage/ProposalSettingsSection.jsx:475 -#: src/components/storage/ProposalSettingsSection.jsx:496 +#: src/components/storage/ProposalSettingsSection.jsx:479 +#: src/components/storage/ProposalSettingsSection.jsx:500 msgid "Encryption settings" msgstr "暗号化の設定" -#: src/components/storage/ProposalSettingsSection.jsx:489 +#: src/components/storage/ProposalSettingsSection.jsx:493 msgid "Use encryption" msgstr "暗号化を使用する" -#: src/components/storage/ProposalSettingsSection.jsx:557 +#: src/components/storage/ProposalSettingsSection.jsx:578 +msgid "" +"Select how to make free space in the disks selected for allocating the " +"file systems." +msgstr "" + +#. TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" +#: src/components/storage/ProposalSettingsSection.jsx:584 +msgid "Find space" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:588 +msgid "Space Policy" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:662 msgid "Settings" msgstr "設定" @@ -1084,48 +1317,48 @@ msgid "partition" msgstr "パーティション" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:215 +#: src/components/storage/ProposalVolumes.jsx:216 msgid "encrypted" msgstr "暗号化" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:217 +#: src/components/storage/ProposalVolumes.jsx:218 msgid "with snapshots" msgstr "スナップショット有り" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:219 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "transactional" msgstr "トランザクション型" -#: src/components/storage/ProposalVolumes.jsx:228 +#: src/components/storage/ProposalVolumes.jsx:229 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "削除" -#: src/components/storage/ProposalVolumes.jsx:274 +#: src/components/storage/ProposalVolumes.jsx:275 msgid "Edit file system" msgstr "ファイルシステムの編集" -#: src/components/storage/ProposalVolumes.jsx:306 +#: src/components/storage/ProposalVolumes.jsx:307 #: src/components/storage/VolumeForm.jsx:500 msgid "Mount point" msgstr "マウントポイント" -#: src/components/storage/ProposalVolumes.jsx:307 +#: src/components/storage/ProposalVolumes.jsx:308 msgid "Details" msgstr "詳細" -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:309 #: src/components/storage/VolumeForm.jsx:517 msgid "Size" msgstr "サイズ" -#: src/components/storage/ProposalVolumes.jsx:344 +#: src/components/storage/ProposalVolumes.jsx:345 msgid "Table with mount points" msgstr "マウントポイントの一覧" -#: src/components/storage/ProposalVolumes.jsx:407 +#: src/components/storage/ProposalVolumes.jsx:408 msgid "File systems to create in your system" msgstr "お使いのシステム内で作成するファイルシステム" @@ -1341,64 +1574,64 @@ msgid "Storage zFCP" msgstr "ストレージ zFCP" #. TRANSLATORS: multipath device type -#: src/components/storage/device-utils.jsx:98 +#: src/components/storage/device-utils.jsx:97 msgid "Multipath" msgstr "マルチパス" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/device-utils.jsx:103 +#: src/components/storage/device-utils.jsx:102 #, c-format msgid "DASD %s" msgstr "DASD %s" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/device-utils.jsx:108 +#: src/components/storage/device-utils.jsx:107 #, c-format msgid "Software %s" msgstr "ソフトウエア %s" -#: src/components/storage/device-utils.jsx:113 +#: src/components/storage/device-utils.jsx:112 msgid "SD Card" msgstr "SD カード" #. TRANSLATORS: %s is replaced by the device transport name, e.g. USB, SATA, SCSI... -#: src/components/storage/device-utils.jsx:115 +#: src/components/storage/device-utils.jsx:114 #, c-format msgid "Transport %s" msgstr "トランスポート %s" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:134 +#: src/components/storage/device-utils.jsx:133 #, c-format msgid "Members: %s" msgstr "メンバー: %s" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:143 +#: src/components/storage/device-utils.jsx:142 #, c-format msgid "Devices: %s" msgstr "デバイス: %s" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/device-utils.jsx:152 +#: src/components/storage/device-utils.jsx:151 #, c-format msgid "Wires: %s" msgstr "接続: %s" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/device-utils.jsx:176 +#: src/components/storage/device-utils.jsx:175 #, c-format msgid "%s with %d partitions" msgstr "%s (%d 個のパーティション)" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/device-utils.jsx:200 +#: src/components/storage/device-utils.jsx:199 msgid "No content found" msgstr "内容が見つかりませんでした" -#: src/components/storage/device-utils.jsx:271 +#: src/components/storage/device-utils.jsx:270 msgid "Available devices" msgstr "利用可能なデバイス" @@ -1575,6 +1808,67 @@ msgstr "検索" msgid "Targets" msgstr "ターゲット" +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:59 +msgid "Delete current content" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:63 +msgid "Shrink existing partitions" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:67 +#, fuzzy +msgid "Use available space" +msgstr "利用可能なデバイス" + +#: src/components/storage/space-policy-utils.jsx:79 +msgid "All partitions will be removed and any data in the disks will be lost." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:82 +msgid "" +"The data is kept, but the current partitions will be resized as needed to " +"make enough space." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:85 +msgid "" +"The data is kept and existing partitions will not be modified. Only the " +"space that is not assigned to any partition will be used." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:112 +msgid "Select a mechanism to make space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:137 +#, c-format +msgid "deleting all content of the installation device" +msgid_plural "deleting all content of the %d selected disks" +msgstr[0] "" + +#: src/components/storage/space-policy-utils.jsx:147 +#, c-format +msgid "shrinking partitions of the installation device" +msgid_plural "shrinking partitions of the %d selected disks" +msgstr[0] "" + +#. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence +#. would read as "Find space without modifying any partition". +#: src/components/storage/space-policy-utils.jsx:155 +#, fuzzy +msgid "without modifying any partition" +msgstr "%s (%d 個のパーティション)" + +#: src/components/storage/space-policy-utils.jsx:170 +#, c-format +msgid "This will only affect the installation device" +msgid_plural "This will affect the %d disks selected for installation" +msgstr[0] "" + #: src/components/storage/utils.js:44 msgid "KiB" msgstr "KiB" diff --git a/web/po/mk.po b/web/po/mk.po index 80184f36df..71e8565b1f 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-11-05 02:14+0000\n" +"POT-Creation-Date: 2023-12-03 02:17+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) 1;\n" "X-Generator: Weblate 4.9.1\n" -#: src/App.jsx:112 +#: src/App.jsx:110 msgid "Diagnostic tools" msgstr "Ferramentas de diagnóstico" @@ -63,6 +63,7 @@ msgstr "" #: src/components/core/About.jsx:71 src/components/core/FileViewer.jsx:80 #: src/components/core/Sidebar.jsx:177 src/components/core/Terminal.jsx:48 #: src/components/network/WifiSelector.jsx:151 +#: src/components/product/ProductPage.jsx:239 msgid "Close" msgstr "" @@ -131,18 +132,23 @@ msgid "" msgstr "" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:97 -#: src/components/storage/ProposalSettingsSection.jsx:166 -#: src/components/storage/ProposalSettingsSection.jsx:359 -#: src/components/storage/ProposalSettingsSection.jsx:504 +#: src/components/core/InstallButton.jsx:97 src/components/l10n/L10nPage.jsx:75 +#: src/components/l10n/L10nPage.jsx:191 src/components/l10n/L10nPage.jsx:304 +#: src/components/product/ProductPage.jsx:71 +#: src/components/product/ProductPage.jsx:140 +#: src/components/product/ProductPage.jsx:207 +#: src/components/storage/ProposalSettingsSection.jsx:170 +#: src/components/storage/ProposalSettingsSection.jsx:363 +#: src/components/storage/ProposalSettingsSection.jsx:508 +#: src/components/storage/ProposalSettingsSection.jsx:604 #: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:282 +#: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:513 msgid "Accept" msgstr "" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:145 +#: src/components/core/InstallButton.jsx:148 msgid "Install" msgstr "" @@ -192,10 +198,40 @@ msgstr "" msgid "There are new issues" msgstr "" -#: src/components/core/IssuesPage.jsx:115 +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:92 +#: src/components/overview/ProductSection.jsx:71 +#: src/components/product/ProductPage.jsx:434 +msgid "Product" +msgstr "" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section title +#: src/components/core/IssuesPage.jsx:100 +#: src/components/overview/StorageSection.jsx:208 +#: src/components/storage/ProposalPage.jsx:218 +msgid "Storage" +msgstr "" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:108 +#: src/components/overview/SoftwareSection.jsx:141 +#: src/components/software/SoftwarePage.jsx:81 +msgid "Software" +msgstr "" + +#: src/components/core/IssuesPage.jsx:129 msgid "No issues found. Everything looks ok." msgstr "" +#. TRANSLATORS: search field placeholder text +#: src/components/core/ListSearch.jsx:50 +#: src/components/software/PatternSelector.jsx:220 +#: src/components/software/PatternSelector.jsx:221 +msgid "Search" +msgstr "" + #: src/components/core/LogsButton.jsx:98 msgid "Collecting logs..." msgstr "" @@ -255,12 +291,11 @@ msgstr "" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 #: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:309 +#: src/components/storage/ProposalVolumes.jsx:310 msgid "Actions" msgstr "" #: src/components/core/SectionSkeleton.jsx:29 -#: src/components/core/SectionSkeleton.jsx:34 msgid "Waiting" msgstr "" @@ -316,22 +351,121 @@ msgstr[1] "" msgid "Basic popover" msgstr "" -#: src/components/l10n/L10nPage.jsx:72 -#: src/components/l10n/LanguageSwitcher.jsx:50 -msgid "language" +#. TRANSLATORS: placeholder text for search input in the keyboard selector. +#: src/components/l10n/KeymapSelector.jsx:82 +msgid "Filter by description or keymap code" +msgstr "" + +#: src/components/l10n/KeymapSelector.jsx:89 +msgid "Available keymaps" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:66 src/components/l10n/L10nPage.jsx:140 +msgid "Select time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:67 +#, c-format +msgid "%s will use the selected time zone." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:128 +msgid "Time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:134 +msgid "Change time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:139 +msgid "Time zone not selected yet" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:182 src/components/l10n/L10nPage.jsx:258 +msgid "Select language" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:183 +#, c-format +msgid "%s will use the selected language." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:246 +msgid "Language" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:252 +msgid "Change language" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:257 +msgid "Language not selected yet" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:295 src/components/l10n/L10nPage.jsx:369 +msgid "Select keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:296 +#, c-format +msgid "%s will use the selected keyboard." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:357 +msgid "Keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:363 +msgid "Change keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:368 +msgid "Keyboard not selected yet" msgstr "" -#. TRANSLATORS: page header #. TRANSLATORS: page section -#: src/components/l10n/L10nPage.jsx:84 -#: src/components/overview/L10nSection.jsx:82 +#. TRANSLATORS: page title +#: src/components/l10n/L10nPage.jsx:385 +#: src/components/overview/L10nSection.jsx:52 msgid "Localization" msgstr "" +#: src/components/l10n/L10nPage.jsx:387 +#: src/components/product/ProductPage.jsx:434 +#: src/components/software/SoftwarePage.jsx:81 +#: src/components/storage/DASDPage.jsx:187 +#: src/components/storage/ISCSIPage.jsx:39 +#: src/components/storage/ProposalPage.jsx:218 +#: src/components/storage/ZFCPPage.jsx:736 +#: src/components/users/UsersPage.jsx:30 +msgid "Back" +msgstr "" + #: src/components/l10n/LanguageSwitcher.jsx:46 msgid "Display Language" msgstr "" +#: src/components/l10n/LanguageSwitcher.jsx:50 +msgid "language" +msgstr "" + +#: src/components/l10n/LocaleSelector.jsx:82 +msgid "Filter by language, territory or locale code" +msgstr "" + +#: src/components/l10n/LocaleSelector.jsx:89 +msgid "Available locales" +msgstr "" + +#. TRANSLATORS: placeholder text for search input in the timezone selector. +#: src/components/l10n/TimezoneSelector.jsx:102 +msgid "Filter by territory, time zone code or UTC offset" +msgstr "" + +#: src/components/l10n/TimezoneSelector.jsx:109 +msgid "Available time zones" +msgstr "" + #: src/components/layout/Loading.jsx:30 msgid "Loading installation environment, please wait." msgstr "" @@ -392,7 +526,7 @@ msgid "IP addresses" msgstr "" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:233 +#: src/components/storage/ProposalVolumes.jsx:234 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:170 @@ -533,6 +667,8 @@ msgid "WPA & WPA2 Personal" msgstr "" #: src/components/network/WifiConnectionForm.jsx:91 +#: src/components/product/ProductPage.jsx:128 +#: src/components/product/ProductPage.jsx:194 #: src/components/storage/ZFCPDiskForm.jsx:112 #: src/components/storage/iscsi/DiscoverForm.jsx:108 #: src/components/storage/iscsi/LoginForm.jsx:72 @@ -605,9 +741,9 @@ msgstr "" msgid "Forget network" msgstr "" -#. TRANSLATORS: %s will be replaced by a language name and code, -#. example: "English (en_US.UTF-8)" -#: src/components/overview/L10nSection.jsx:70 +#. TRANSLATORS: %s will be replaced by a language name and territory, example: +#. "English (United States)". +#: src/components/overview/L10nSection.jsx:34 #, c-format msgid "The system will use %s as its default language." msgstr "" @@ -625,43 +761,61 @@ msgid_plural "%d connections set:" msgstr[0] "" msgstr[1] "" +#. TRANSLATORS: page title +#: src/components/overview/Overview.jsx:47 +msgid "Installation Summary" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/overview/ProductSection.jsx:48 +#, c-format +msgid "%s (registered)" +msgstr "" + #: src/components/overview/SoftwareSection.jsx:37 msgid "Reading software repositories" msgstr "" #. TRANSLATORS: clickable link label -#: src/components/overview/SoftwareSection.jsx:135 +#: src/components/overview/SoftwareSection.jsx:131 msgid "Refresh the repositories" msgstr "" -#. TRANSLATORS: page section -#. TRANSLATORS: page title -#: src/components/overview/SoftwareSection.jsx:145 -#: src/components/software/SoftwarePage.jsx:81 -msgid "Software" +#: src/components/overview/StorageSection.jsx:42 +#: src/components/storage/ProposalSettingsSection.jsx:126 +msgid "No device selected yet" msgstr "" -#: src/components/overview/StorageSection.jsx:36 -#: src/components/storage/ProposalSettingsSection.jsx:122 -msgid "No device selected yet" +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:52 +#, c-format +msgid "Install using device %s shrinking existing partitions as needed" msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:44 +#: src/components/overview/StorageSection.jsx:56 +#, c-format +msgid "Install using device %s without modifying existing partitions" +msgstr "" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:60 #, c-format msgid "Install using device %s and deleting all its content" msgstr "" -#: src/components/overview/StorageSection.jsx:57 -msgid "Probing storage devices" +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:66 +#, c-format +msgid "Install using device %s" msgstr "" -#. TRANSLATORS: page title -#. TRANSLATORS: page section title -#: src/components/overview/StorageSection.jsx:182 -#: src/components/storage/ProposalPage.jsx:218 -msgid "Storage" +#: src/components/overview/StorageSection.jsx:83 +msgid "Probing storage devices" msgstr "" #. TRANSLATORS: %s will be replaced by the user name @@ -697,6 +851,96 @@ msgstr "" msgid "Users" msgstr "" +#: src/components/product/ProductPage.jsx:63 +#: src/components/product/ProductSelectionPage.jsx:73 +msgid "Choose a product" +msgstr "" + +#: src/components/product/ProductPage.jsx:122 +#, c-format +msgid "Register %s" +msgstr "" + +#: src/components/product/ProductPage.jsx:188 +#, c-format +msgid "Deregister %s" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:202 +#, c-format +msgid "Do you want to deregister %s?" +msgstr "" + +#: src/components/product/ProductPage.jsx:227 +msgid "Registered warning" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:232 +#, c-format +msgid "The product %s must be deregistered before selecting a new product." +msgstr "" + +#: src/components/product/ProductPage.jsx:263 +msgid "Change product" +msgstr "" + +#: src/components/product/ProductPage.jsx:305 +msgid "Register" +msgstr "" + +#: src/components/product/ProductPage.jsx:337 +msgid "Deregister product" +msgstr "" + +#: src/components/product/ProductPage.jsx:370 +msgid "Code:" +msgstr "" + +#: src/components/product/ProductPage.jsx:374 +msgid "Email:" +msgstr "" + +#. TRANSLATORS: section title. +#: src/components/product/ProductPage.jsx:390 +msgid "Registration" +msgstr "" + +#: src/components/product/ProductPage.jsx:399 +msgid "This product requires registration." +msgstr "" + +#: src/components/product/ProductPage.jsx:405 +msgid "This product does not require registration." +msgstr "" + +#: src/components/product/ProductRegistrationForm.jsx:63 +msgid "Registration code" +msgstr "" + +#: src/components/product/ProductRegistrationForm.jsx:66 +msgid "Email" +msgstr "" + +#: src/components/product/ProductSelectionPage.jsx:58 +msgid "Loading available products, please wait..." +msgstr "" + +#. TRANSLATORS: page header +#: src/components/product/ProductSelectionPage.jsx:64 +msgid "Product selection" +msgstr "" + +#. TRANSLATORS: button label +#: src/components/product/ProductSelectionPage.jsx:69 +msgid "Select" +msgstr "" + +#: src/components/product/ProductSelector.jsx:29 +msgid "No products available for selection" +msgstr "" + #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 msgid "Question" @@ -716,10 +960,6 @@ msgstr "" msgid "Encryption Password" msgstr "" -#: src/components/software/ChangeProductLink.jsx:36 -msgid "Change product" -msgstr "" - #. TRANSLATORS: pattern status, selected to install (by user) #: src/components/software/PatternItem.jsx:63 msgid "selected" @@ -737,46 +977,13 @@ msgstr "" #. TRANSLATORS: error summary, always plural, %d is replaced by number of errors (2 or more) #. if there is just a single error then the error is displayed directly instead of this summary -#: src/components/software/PatternSelector.jsx:206 +#: src/components/software/PatternSelector.jsx:207 #, c-format msgid "%d errors" msgstr "" -#: src/components/software/PatternSelector.jsx:210 -msgid "Software summary and filter options" -msgstr "" - -#. TRANSLATORS: search field placeholder text #: src/components/software/PatternSelector.jsx:215 -#: src/components/software/PatternSelector.jsx:216 -msgid "Search" -msgstr "" - -#: src/components/software/ProductSelectionPage.jsx:69 -msgid "Loading available products, please wait..." -msgstr "" - -#. TRANSLATORS: page header -#: src/components/software/ProductSelectionPage.jsx:94 -msgid "Product selection" -msgstr "" - -#. TRANSLATORS: button label -#: src/components/software/ProductSelectionPage.jsx:99 -msgid "Select" -msgstr "" - -#: src/components/software/ProductSelectionPage.jsx:104 -msgid "Choose a product" -msgstr "" - -#: src/components/software/SoftwarePage.jsx:81 -#: src/components/storage/DASDPage.jsx:187 -#: src/components/storage/ISCSIPage.jsx:39 -#: src/components/storage/ProposalPage.jsx:218 -#: src/components/storage/ZFCPPage.jsx:736 -#: src/components/users/UsersPage.jsx:30 -msgid "Back" +msgid "Software summary and filter options" msgstr "" #. TRANSLATORS: %s will be replaced by the estimated installation size, @@ -954,65 +1161,80 @@ msgstr "" msgid "iSCSI" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:135 +#: src/components/storage/ProposalSettingsSection.jsx:139 msgid "Select the device for installing the system." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:140 #: src/components/storage/ProposalSettingsSection.jsx:144 -#: src/components/storage/ProposalSettingsSection.jsx:240 +#: src/components/storage/ProposalSettingsSection.jsx:148 +#: src/components/storage/ProposalSettingsSection.jsx:244 msgid "Installation device" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:150 +#: src/components/storage/ProposalSettingsSection.jsx:154 msgid "No devices found" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:237 +#: src/components/storage/ProposalSettingsSection.jsx:241 msgid "Devices for creating the volume group" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:246 +#: src/components/storage/ProposalSettingsSection.jsx:250 msgid "Custom devices" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:310 +#: src/components/storage/ProposalSettingsSection.jsx:314 msgid "" "Configuration of the system volume group. All the file systems will be " "created in a logical volume of the system volume group." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:316 +#: src/components/storage/ProposalSettingsSection.jsx:320 msgid "Configure the LVM settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:321 -#: src/components/storage/ProposalSettingsSection.jsx:341 +#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:345 msgid "LVM settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:334 +#: src/components/storage/ProposalSettingsSection.jsx:338 msgid "Use logical volume management (LVM)" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:342 +#: src/components/storage/ProposalSettingsSection.jsx:346 msgid "System Volume Group" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:470 +#: src/components/storage/ProposalSettingsSection.jsx:474 msgid "Change encryption password" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:475 -#: src/components/storage/ProposalSettingsSection.jsx:496 +#: src/components/storage/ProposalSettingsSection.jsx:479 +#: src/components/storage/ProposalSettingsSection.jsx:500 msgid "Encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:489 +#: src/components/storage/ProposalSettingsSection.jsx:493 msgid "Use encryption" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:557 +#: src/components/storage/ProposalSettingsSection.jsx:578 +msgid "" +"Select how to make free space in the disks selected for allocating the " +"file systems." +msgstr "" + +#. TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" +#: src/components/storage/ProposalSettingsSection.jsx:584 +msgid "Find space" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:588 +msgid "Space Policy" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:662 msgid "Settings" msgstr "" @@ -1065,48 +1287,48 @@ msgid "partition" msgstr "" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:215 +#: src/components/storage/ProposalVolumes.jsx:216 msgid "encrypted" msgstr "" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:217 +#: src/components/storage/ProposalVolumes.jsx:218 msgid "with snapshots" msgstr "" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:219 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "transactional" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:228 +#: src/components/storage/ProposalVolumes.jsx:229 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:274 +#: src/components/storage/ProposalVolumes.jsx:275 msgid "Edit file system" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:306 +#: src/components/storage/ProposalVolumes.jsx:307 #: src/components/storage/VolumeForm.jsx:500 msgid "Mount point" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:307 +#: src/components/storage/ProposalVolumes.jsx:308 msgid "Details" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:309 #: src/components/storage/VolumeForm.jsx:517 msgid "Size" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:344 +#: src/components/storage/ProposalVolumes.jsx:345 msgid "Table with mount points" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:407 +#: src/components/storage/ProposalVolumes.jsx:408 msgid "File systems to create in your system" msgstr "" @@ -1315,64 +1537,64 @@ msgid "Storage zFCP" msgstr "" #. TRANSLATORS: multipath device type -#: src/components/storage/device-utils.jsx:98 +#: src/components/storage/device-utils.jsx:97 msgid "Multipath" msgstr "" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/device-utils.jsx:103 +#: src/components/storage/device-utils.jsx:102 #, c-format msgid "DASD %s" msgstr "" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/device-utils.jsx:108 +#: src/components/storage/device-utils.jsx:107 #, c-format msgid "Software %s" msgstr "" -#: src/components/storage/device-utils.jsx:113 +#: src/components/storage/device-utils.jsx:112 msgid "SD Card" msgstr "" #. TRANSLATORS: %s is replaced by the device transport name, e.g. USB, SATA, SCSI... -#: src/components/storage/device-utils.jsx:115 +#: src/components/storage/device-utils.jsx:114 #, c-format msgid "Transport %s" msgstr "" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:134 +#: src/components/storage/device-utils.jsx:133 #, c-format msgid "Members: %s" msgstr "" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:143 +#: src/components/storage/device-utils.jsx:142 #, c-format msgid "Devices: %s" msgstr "" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/device-utils.jsx:152 +#: src/components/storage/device-utils.jsx:151 #, c-format msgid "Wires: %s" msgstr "" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/device-utils.jsx:176 +#: src/components/storage/device-utils.jsx:175 #, c-format msgid "%s with %d partitions" msgstr "" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/device-utils.jsx:200 +#: src/components/storage/device-utils.jsx:199 msgid "No content found" msgstr "" -#: src/components/storage/device-utils.jsx:271 +#: src/components/storage/device-utils.jsx:270 msgid "Available devices" msgstr "" @@ -1548,6 +1770,68 @@ msgstr "" msgid "Targets" msgstr "" +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:59 +msgid "Delete current content" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:63 +msgid "Shrink existing partitions" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:67 +msgid "Use available space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:79 +msgid "All partitions will be removed and any data in the disks will be lost." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:82 +msgid "" +"The data is kept, but the current partitions will be resized as needed to " +"make enough space." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:85 +msgid "" +"The data is kept and existing partitions will not be modified. Only the " +"space that is not assigned to any partition will be used." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:112 +msgid "Select a mechanism to make space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:137 +#, c-format +msgid "deleting all content of the installation device" +msgid_plural "deleting all content of the %d selected disks" +msgstr[0] "" +msgstr[1] "" + +#: src/components/storage/space-policy-utils.jsx:147 +#, c-format +msgid "shrinking partitions of the installation device" +msgid_plural "shrinking partitions of the %d selected disks" +msgstr[0] "" +msgstr[1] "" + +#. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence +#. would read as "Find space without modifying any partition". +#: src/components/storage/space-policy-utils.jsx:155 +msgid "without modifying any partition" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:170 +#, c-format +msgid "This will only affect the installation device" +msgid_plural "This will affect the %d disks selected for installation" +msgstr[0] "" +msgstr[1] "" + #: src/components/storage/utils.js:44 msgid "KiB" msgstr "" diff --git a/web/po/ru.po b/web/po/ru.po index 2aa9dd8489..179651a49a 100644 --- a/web/po/ru.po +++ b/web/po/ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-11-05 02:14+0000\n" +"POT-Creation-Date: 2023-12-03 02:17+0000\n" "PO-Revision-Date: 2023-09-11 13:15+0000\n" "Last-Translator: Alex Minton \n" "Language-Team: Russian =2 && n" -"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "X-Generator: Weblate 4.9.1\n" -#: src/App.jsx:112 +#: src/App.jsx:110 msgid "Diagnostic tools" msgstr "Утилиты диагностики" @@ -33,7 +33,6 @@ msgstr "" #. TRANSLATORS: error message #: src/DevServerWrapper.jsx:88 #, fuzzy -#| msgid "Cannot connect to D-Bus" msgid "Cannot connect to the Cockpit server" msgstr "Не удалось подключиться к службе D-Bus" @@ -72,6 +71,7 @@ msgstr "" #: src/components/core/About.jsx:71 src/components/core/FileViewer.jsx:80 #: src/components/core/Sidebar.jsx:177 src/components/core/Terminal.jsx:48 #: src/components/network/WifiSelector.jsx:151 +#: src/components/product/ProductPage.jsx:239 msgid "Close" msgstr "Закрыть" @@ -144,18 +144,23 @@ msgid "" msgstr "" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:97 -#: src/components/storage/ProposalSettingsSection.jsx:166 -#: src/components/storage/ProposalSettingsSection.jsx:359 -#: src/components/storage/ProposalSettingsSection.jsx:504 +#: src/components/core/InstallButton.jsx:97 src/components/l10n/L10nPage.jsx:75 +#: src/components/l10n/L10nPage.jsx:191 src/components/l10n/L10nPage.jsx:304 +#: src/components/product/ProductPage.jsx:71 +#: src/components/product/ProductPage.jsx:140 +#: src/components/product/ProductPage.jsx:207 +#: src/components/storage/ProposalSettingsSection.jsx:170 +#: src/components/storage/ProposalSettingsSection.jsx:363 +#: src/components/storage/ProposalSettingsSection.jsx:508 +#: src/components/storage/ProposalSettingsSection.jsx:604 #: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:282 +#: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:513 msgid "Accept" msgstr "Подтвердить" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:145 +#: src/components/core/InstallButton.jsx:148 msgid "Install" msgstr "Установить" @@ -207,10 +212,40 @@ msgstr "Показать ошибки" msgid "There are new issues" msgstr "Есть новые ошибки" -#: src/components/core/IssuesPage.jsx:115 +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:92 +#: src/components/overview/ProductSection.jsx:71 +#: src/components/product/ProductPage.jsx:434 +msgid "Product" +msgstr "" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section title +#: src/components/core/IssuesPage.jsx:100 +#: src/components/overview/StorageSection.jsx:208 +#: src/components/storage/ProposalPage.jsx:218 +msgid "Storage" +msgstr "" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:108 +#: src/components/overview/SoftwareSection.jsx:141 +#: src/components/software/SoftwarePage.jsx:81 +msgid "Software" +msgstr "Программы" + +#: src/components/core/IssuesPage.jsx:129 msgid "No issues found. Everything looks ok." msgstr "Проблем не обнаружено. Все выглядит нормально." +#. TRANSLATORS: search field placeholder text +#: src/components/core/ListSearch.jsx:50 +#: src/components/software/PatternSelector.jsx:220 +#: src/components/software/PatternSelector.jsx:221 +msgid "Search" +msgstr "" + #: src/components/core/LogsButton.jsx:98 msgid "Collecting logs..." msgstr "Сбор журналов..." @@ -273,12 +308,11 @@ msgstr "Отмена" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 #: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:309 +#: src/components/storage/ProposalVolumes.jsx:310 msgid "Actions" msgstr "Действия" #: src/components/core/SectionSkeleton.jsx:29 -#: src/components/core/SectionSkeleton.jsx:34 msgid "Waiting" msgstr "Ожидание" @@ -335,24 +369,128 @@ msgstr[2] "Найдено %d ошибок" msgid "Basic popover" msgstr "Базовое всплывающее окно" -#: src/components/l10n/L10nPage.jsx:72 -#: src/components/l10n/LanguageSwitcher.jsx:50 -msgid "language" +#. TRANSLATORS: placeholder text for search input in the keyboard selector. +#: src/components/l10n/KeymapSelector.jsx:82 +msgid "Filter by description or keymap code" +msgstr "" + +#: src/components/l10n/KeymapSelector.jsx:89 +msgid "Available keymaps" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:66 src/components/l10n/L10nPage.jsx:140 +msgid "Select time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:67 +#, c-format +msgid "%s will use the selected time zone." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:128 +msgid "Time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:134 +msgid "Change time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:139 +#, fuzzy +msgid "Time zone not selected yet" +msgstr "Устройство ещё не выбрано" + +#: src/components/l10n/L10nPage.jsx:182 src/components/l10n/L10nPage.jsx:258 +#, fuzzy +msgid "Select language" msgstr "Язык" -#. TRANSLATORS: page header +#: src/components/l10n/L10nPage.jsx:183 +#, fuzzy, c-format +msgid "%s will use the selected language." +msgstr "Система будет использовать %s в качестве языка по умолчанию." + +#: src/components/l10n/L10nPage.jsx:246 +#, fuzzy +msgid "Language" +msgstr "Язык" + +#: src/components/l10n/L10nPage.jsx:252 +#, fuzzy +msgid "Change language" +msgstr "Язык" + +#: src/components/l10n/L10nPage.jsx:257 +msgid "Language not selected yet" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:295 src/components/l10n/L10nPage.jsx:369 +#, fuzzy +msgid "Select keyboard" +msgstr "Отключено" + +#: src/components/l10n/L10nPage.jsx:296 +#, c-format +msgid "%s will use the selected keyboard." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:357 +msgid "Keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:363 +msgid "Change keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:368 +#, fuzzy +msgid "Keyboard not selected yet" +msgstr "Устройство ещё не выбрано" + #. TRANSLATORS: page section -#: src/components/l10n/L10nPage.jsx:84 -#: src/components/overview/L10nSection.jsx:82 +#. TRANSLATORS: page title +#: src/components/l10n/L10nPage.jsx:385 +#: src/components/overview/L10nSection.jsx:52 msgid "Localization" msgstr "Локализация" +#: src/components/l10n/L10nPage.jsx:387 +#: src/components/product/ProductPage.jsx:434 +#: src/components/software/SoftwarePage.jsx:81 +#: src/components/storage/DASDPage.jsx:187 +#: src/components/storage/ISCSIPage.jsx:39 +#: src/components/storage/ProposalPage.jsx:218 +#: src/components/storage/ZFCPPage.jsx:736 +#: src/components/users/UsersPage.jsx:30 +msgid "Back" +msgstr "" + #: src/components/l10n/LanguageSwitcher.jsx:46 #, fuzzy -#| msgid "language" msgid "Display Language" msgstr "Язык" +#: src/components/l10n/LanguageSwitcher.jsx:50 +msgid "language" +msgstr "Язык" + +#: src/components/l10n/LocaleSelector.jsx:82 +msgid "Filter by language, territory or locale code" +msgstr "" + +#: src/components/l10n/LocaleSelector.jsx:89 +msgid "Available locales" +msgstr "" + +#. TRANSLATORS: placeholder text for search input in the timezone selector. +#: src/components/l10n/TimezoneSelector.jsx:102 +msgid "Filter by territory, time zone code or UTC offset" +msgstr "" + +#: src/components/l10n/TimezoneSelector.jsx:109 +msgid "Available time zones" +msgstr "" + #: src/components/layout/Loading.jsx:30 msgid "Loading installation environment, please wait." msgstr "Загрузка установочной среды, пожалуйста, подождите." @@ -413,7 +551,7 @@ msgid "IP addresses" msgstr "IP-адреса" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:233 +#: src/components/storage/ProposalVolumes.jsx:234 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:170 @@ -556,6 +694,8 @@ msgid "WPA & WPA2 Personal" msgstr "WPA и WPA2 Personal" #: src/components/network/WifiConnectionForm.jsx:91 +#: src/components/product/ProductPage.jsx:128 +#: src/components/product/ProductPage.jsx:194 #: src/components/storage/ZFCPDiskForm.jsx:112 #: src/components/storage/iscsi/DiscoverForm.jsx:108 #: src/components/storage/iscsi/LoginForm.jsx:72 @@ -628,9 +768,9 @@ msgstr "Соединение %s ожидает изменения состоян msgid "Forget network" msgstr "Забыть сеть" -#. TRANSLATORS: %s will be replaced by a language name and code, -#. example: "English (en_US.UTF-8)" -#: src/components/overview/L10nSection.jsx:70 +#. TRANSLATORS: %s will be replaced by a language name and territory, example: +#. "English (United States)". +#: src/components/overview/L10nSection.jsx:34 #, c-format msgid "The system will use %s as its default language." msgstr "Система будет использовать %s в качестве языка по умолчанию." @@ -649,43 +789,62 @@ msgstr[0] "Активно %d соединение:" msgstr[1] "Активно %d соединения:" msgstr[2] "Активно %d соединений:" +#. TRANSLATORS: page title +#: src/components/overview/Overview.jsx:47 +#, fuzzy +msgid "Installation Summary" +msgstr "Установка завершена" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/overview/ProductSection.jsx:48 +#, c-format +msgid "%s (registered)" +msgstr "" + #: src/components/overview/SoftwareSection.jsx:37 msgid "Reading software repositories" msgstr "Чтение репозиториев" #. TRANSLATORS: clickable link label -#: src/components/overview/SoftwareSection.jsx:135 +#: src/components/overview/SoftwareSection.jsx:131 msgid "Refresh the repositories" msgstr "Обновить репозитории" -#. TRANSLATORS: page section -#. TRANSLATORS: page title -#: src/components/overview/SoftwareSection.jsx:145 -#: src/components/software/SoftwarePage.jsx:81 -msgid "Software" -msgstr "Программы" - -#: src/components/overview/StorageSection.jsx:36 -#: src/components/storage/ProposalSettingsSection.jsx:122 +#: src/components/overview/StorageSection.jsx:42 +#: src/components/storage/ProposalSettingsSection.jsx:126 msgid "No device selected yet" msgstr "Устройство ещё не выбрано" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:44 +#: src/components/overview/StorageSection.jsx:52 #, c-format -msgid "Install using device %s and deleting all its content" +msgid "Install using device %s shrinking existing partitions as needed" msgstr "" -#: src/components/overview/StorageSection.jsx:57 -msgid "Probing storage devices" +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:56 +#, c-format +msgid "Install using device %s without modifying existing partitions" msgstr "" -#. TRANSLATORS: page title -#. TRANSLATORS: page section title -#: src/components/overview/StorageSection.jsx:182 -#: src/components/storage/ProposalPage.jsx:218 -msgid "Storage" +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:60 +#, c-format +msgid "Install using device %s and deleting all its content" +msgstr "" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:66 +#, fuzzy, c-format +msgid "Install using device %s" +msgstr "Установка займёт %s" + +#: src/components/overview/StorageSection.jsx:83 +msgid "Probing storage devices" msgstr "" #. TRANSLATORS: %s will be replaced by the user name @@ -721,6 +880,96 @@ msgstr "" msgid "Users" msgstr "" +#: src/components/product/ProductPage.jsx:63 +#: src/components/product/ProductSelectionPage.jsx:73 +msgid "Choose a product" +msgstr "" + +#: src/components/product/ProductPage.jsx:122 +#, c-format +msgid "Register %s" +msgstr "" + +#: src/components/product/ProductPage.jsx:188 +#, c-format +msgid "Deregister %s" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:202 +#, c-format +msgid "Do you want to deregister %s?" +msgstr "" + +#: src/components/product/ProductPage.jsx:227 +msgid "Registered warning" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:232 +#, c-format +msgid "The product %s must be deregistered before selecting a new product." +msgstr "" + +#: src/components/product/ProductPage.jsx:263 +msgid "Change product" +msgstr "" + +#: src/components/product/ProductPage.jsx:305 +msgid "Register" +msgstr "" + +#: src/components/product/ProductPage.jsx:337 +msgid "Deregister product" +msgstr "" + +#: src/components/product/ProductPage.jsx:370 +msgid "Code:" +msgstr "" + +#: src/components/product/ProductPage.jsx:374 +msgid "Email:" +msgstr "" + +#. TRANSLATORS: section title. +#: src/components/product/ProductPage.jsx:390 +msgid "Registration" +msgstr "" + +#: src/components/product/ProductPage.jsx:399 +msgid "This product requires registration." +msgstr "" + +#: src/components/product/ProductPage.jsx:405 +msgid "This product does not require registration." +msgstr "" + +#: src/components/product/ProductRegistrationForm.jsx:63 +msgid "Registration code" +msgstr "" + +#: src/components/product/ProductRegistrationForm.jsx:66 +msgid "Email" +msgstr "" + +#: src/components/product/ProductSelectionPage.jsx:58 +msgid "Loading available products, please wait..." +msgstr "" + +#. TRANSLATORS: page header +#: src/components/product/ProductSelectionPage.jsx:64 +msgid "Product selection" +msgstr "" + +#. TRANSLATORS: button label +#: src/components/product/ProductSelectionPage.jsx:69 +msgid "Select" +msgstr "" + +#: src/components/product/ProductSelector.jsx:29 +msgid "No products available for selection" +msgstr "" + #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 msgid "Question" @@ -740,14 +989,9 @@ msgstr "" msgid "Encryption Password" msgstr "" -#: src/components/software/ChangeProductLink.jsx:36 -msgid "Change product" -msgstr "" - #. TRANSLATORS: pattern status, selected to install (by user) #: src/components/software/PatternItem.jsx:63 #, fuzzy -#| msgid "Disconnected" msgid "selected" msgstr "Отключено" @@ -763,48 +1007,13 @@ msgstr "" #. TRANSLATORS: error summary, always plural, %d is replaced by number of errors (2 or more) #. if there is just a single error then the error is displayed directly instead of this summary -#: src/components/software/PatternSelector.jsx:206 +#: src/components/software/PatternSelector.jsx:207 #, fuzzy, c-format -#| msgid "%d error found" -#| msgid_plural "%d errors found" 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:215 -#: src/components/software/PatternSelector.jsx:216 -msgid "Search" -msgstr "" - -#: src/components/software/ProductSelectionPage.jsx:69 -msgid "Loading available products, please wait..." -msgstr "" - -#. TRANSLATORS: page header -#: src/components/software/ProductSelectionPage.jsx:94 -msgid "Product selection" -msgstr "" - -#. TRANSLATORS: button label -#: src/components/software/ProductSelectionPage.jsx:99 -msgid "Select" -msgstr "" - -#: src/components/software/ProductSelectionPage.jsx:104 -msgid "Choose a product" -msgstr "" - -#: src/components/software/SoftwarePage.jsx:81 -#: src/components/storage/DASDPage.jsx:187 -#: src/components/storage/ISCSIPage.jsx:39 -#: src/components/storage/ProposalPage.jsx:218 -#: src/components/storage/ZFCPPage.jsx:736 -#: src/components/users/UsersPage.jsx:30 -msgid "Back" +msgid "Software summary and filter options" msgstr "" #. TRANSLATORS: %s will be replaced by the estimated installation size, @@ -984,65 +1193,80 @@ msgstr "" msgid "iSCSI" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:135 +#: src/components/storage/ProposalSettingsSection.jsx:139 msgid "Select the device for installing the system." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:140 #: src/components/storage/ProposalSettingsSection.jsx:144 -#: src/components/storage/ProposalSettingsSection.jsx:240 +#: src/components/storage/ProposalSettingsSection.jsx:148 +#: src/components/storage/ProposalSettingsSection.jsx:244 msgid "Installation device" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:150 +#: src/components/storage/ProposalSettingsSection.jsx:154 msgid "No devices found" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:237 +#: src/components/storage/ProposalSettingsSection.jsx:241 msgid "Devices for creating the volume group" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:246 +#: src/components/storage/ProposalSettingsSection.jsx:250 msgid "Custom devices" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:310 +#: src/components/storage/ProposalSettingsSection.jsx:314 msgid "" "Configuration of the system volume group. All the file systems will be " "created in a logical volume of the system volume group." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:316 +#: src/components/storage/ProposalSettingsSection.jsx:320 msgid "Configure the LVM settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:321 -#: src/components/storage/ProposalSettingsSection.jsx:341 +#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:345 msgid "LVM settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:334 +#: src/components/storage/ProposalSettingsSection.jsx:338 msgid "Use logical volume management (LVM)" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:342 +#: src/components/storage/ProposalSettingsSection.jsx:346 msgid "System Volume Group" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:470 +#: src/components/storage/ProposalSettingsSection.jsx:474 msgid "Change encryption password" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:475 -#: src/components/storage/ProposalSettingsSection.jsx:496 +#: src/components/storage/ProposalSettingsSection.jsx:479 +#: src/components/storage/ProposalSettingsSection.jsx:500 msgid "Encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:489 +#: src/components/storage/ProposalSettingsSection.jsx:493 msgid "Use encryption" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:557 +#: src/components/storage/ProposalSettingsSection.jsx:578 +msgid "" +"Select how to make free space in the disks selected for allocating the " +"file systems." +msgstr "" + +#. TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" +#: src/components/storage/ProposalSettingsSection.jsx:584 +msgid "Find space" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:588 +msgid "Space Policy" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:662 msgid "Settings" msgstr "" @@ -1095,48 +1319,48 @@ msgid "partition" msgstr "" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:215 +#: src/components/storage/ProposalVolumes.jsx:216 msgid "encrypted" msgstr "" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:217 +#: src/components/storage/ProposalVolumes.jsx:218 msgid "with snapshots" msgstr "" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:219 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "transactional" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:228 +#: src/components/storage/ProposalVolumes.jsx:229 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:274 +#: src/components/storage/ProposalVolumes.jsx:275 msgid "Edit file system" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:306 +#: src/components/storage/ProposalVolumes.jsx:307 #: src/components/storage/VolumeForm.jsx:500 msgid "Mount point" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:307 +#: src/components/storage/ProposalVolumes.jsx:308 msgid "Details" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:309 #: src/components/storage/VolumeForm.jsx:517 msgid "Size" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:344 +#: src/components/storage/ProposalVolumes.jsx:345 msgid "Table with mount points" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:407 +#: src/components/storage/ProposalVolumes.jsx:408 msgid "File systems to create in your system" msgstr "" @@ -1345,64 +1569,64 @@ msgid "Storage zFCP" msgstr "" #. TRANSLATORS: multipath device type -#: src/components/storage/device-utils.jsx:98 +#: src/components/storage/device-utils.jsx:97 msgid "Multipath" msgstr "" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/device-utils.jsx:103 +#: src/components/storage/device-utils.jsx:102 #, c-format msgid "DASD %s" msgstr "" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/device-utils.jsx:108 +#: src/components/storage/device-utils.jsx:107 #, c-format msgid "Software %s" msgstr "" -#: src/components/storage/device-utils.jsx:113 +#: src/components/storage/device-utils.jsx:112 msgid "SD Card" msgstr "" #. TRANSLATORS: %s is replaced by the device transport name, e.g. USB, SATA, SCSI... -#: src/components/storage/device-utils.jsx:115 +#: src/components/storage/device-utils.jsx:114 #, c-format msgid "Transport %s" msgstr "" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:134 +#: src/components/storage/device-utils.jsx:133 #, c-format msgid "Members: %s" msgstr "" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:143 +#: src/components/storage/device-utils.jsx:142 #, c-format msgid "Devices: %s" msgstr "" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/device-utils.jsx:152 +#: src/components/storage/device-utils.jsx:151 #, c-format msgid "Wires: %s" msgstr "" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/device-utils.jsx:176 +#: src/components/storage/device-utils.jsx:175 #, c-format msgid "%s with %d partitions" msgstr "" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/device-utils.jsx:200 +#: src/components/storage/device-utils.jsx:199 msgid "No content found" msgstr "" -#: src/components/storage/device-utils.jsx:271 +#: src/components/storage/device-utils.jsx:270 msgid "Available devices" msgstr "" @@ -1578,6 +1802,71 @@ msgstr "" msgid "Targets" msgstr "" +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:59 +msgid "Delete current content" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:63 +msgid "Shrink existing partitions" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:67 +msgid "Use available space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:79 +msgid "All partitions will be removed and any data in the disks will be lost." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:82 +msgid "" +"The data is kept, but the current partitions will be resized as needed to " +"make enough space." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:85 +msgid "" +"The data is kept and existing partitions will not be modified. Only the " +"space that is not assigned to any partition will be used." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:112 +msgid "Select a mechanism to make space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:137 +#, c-format +msgid "deleting all content of the installation device" +msgid_plural "deleting all content of the %d selected disks" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: src/components/storage/space-policy-utils.jsx:147 +#, c-format +msgid "shrinking partitions of the installation device" +msgid_plural "shrinking partitions of the %d selected disks" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence +#. would read as "Find space without modifying any partition". +#: src/components/storage/space-policy-utils.jsx:155 +msgid "without modifying any partition" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:170 +#, c-format +msgid "This will only affect the installation device" +msgid_plural "This will affect the %d disks selected for installation" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + #: src/components/storage/utils.js:44 msgid "KiB" msgstr "" diff --git a/web/po/sv.po b/web/po/sv.po index db664c6e06..5307ea2b9a 100644 --- a/web/po/sv.po +++ b/web/po/sv.po @@ -5,10 +5,10 @@ # msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-11-06 12:00+0000\n" -"PO-Revision-Date: 2023-11-06 12:01+0000\n" +"POT-Creation-Date: 2023-12-03 02:17+0000\n" +"PO-Revision-Date: 2023-11-23 09:02+0100\n" "Last-Translator: Luna Jernberg \n" "Language-Team: Swedish \n" @@ -17,9 +17,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.9.1\n" +"X-Generator: Poedit 3.4.1\n" -#: src/App.jsx:112 +#: src/App.jsx:110 msgid "Diagnostic tools" msgstr "Diagnostik verktyg" @@ -67,6 +67,7 @@ msgstr "För mer information, var vänlig och besök projektets arkiv på %s." #: src/components/core/About.jsx:71 src/components/core/FileViewer.jsx:80 #: src/components/core/Sidebar.jsx:177 src/components/core/Terminal.jsx:48 #: src/components/network/WifiSelector.jsx:151 +#: src/components/product/ProductPage.jsx:239 msgid "Close" msgstr "Stäng" @@ -142,18 +143,23 @@ msgstr "" "de rapporterade felen och försök igen." #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:97 -#: src/components/storage/ProposalSettingsSection.jsx:166 -#: src/components/storage/ProposalSettingsSection.jsx:359 -#: src/components/storage/ProposalSettingsSection.jsx:504 +#: src/components/core/InstallButton.jsx:97 src/components/l10n/L10nPage.jsx:75 +#: src/components/l10n/L10nPage.jsx:191 src/components/l10n/L10nPage.jsx:304 +#: src/components/product/ProductPage.jsx:71 +#: src/components/product/ProductPage.jsx:140 +#: src/components/product/ProductPage.jsx:207 +#: src/components/storage/ProposalSettingsSection.jsx:170 +#: src/components/storage/ProposalSettingsSection.jsx:363 +#: src/components/storage/ProposalSettingsSection.jsx:508 +#: src/components/storage/ProposalSettingsSection.jsx:604 #: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:282 +#: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:513 msgid "Accept" msgstr "Acceptera" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:145 +#: src/components/core/InstallButton.jsx:148 msgid "Install" msgstr "Installera" @@ -205,10 +211,40 @@ msgstr "Visa problem" msgid "There are new issues" msgstr "Det finns nya problem" -#: src/components/core/IssuesPage.jsx:115 +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:92 +#: src/components/overview/ProductSection.jsx:71 +#: src/components/product/ProductPage.jsx:434 +msgid "Product" +msgstr "Produkt" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section title +#: src/components/core/IssuesPage.jsx:100 +#: src/components/overview/StorageSection.jsx:208 +#: src/components/storage/ProposalPage.jsx:218 +msgid "Storage" +msgstr "Lagring" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:108 +#: src/components/overview/SoftwareSection.jsx:141 +#: src/components/software/SoftwarePage.jsx:81 +msgid "Software" +msgstr "Programvara" + +#: src/components/core/IssuesPage.jsx:129 msgid "No issues found. Everything looks ok." msgstr "Inga problem hittades. Allt ser ok ut." +#. TRANSLATORS: search field placeholder text +#: src/components/core/ListSearch.jsx:50 +#: src/components/software/PatternSelector.jsx:220 +#: src/components/software/PatternSelector.jsx:221 +msgid "Search" +msgstr "Sök" + #: src/components/core/LogsButton.jsx:98 msgid "Collecting logs..." msgstr "Samlar loggar..." @@ -270,12 +306,11 @@ msgstr "Avbryt" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 #: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:309 +#: src/components/storage/ProposalVolumes.jsx:310 msgid "Actions" msgstr "Åtgärder" #: src/components/core/SectionSkeleton.jsx:29 -#: src/components/core/SectionSkeleton.jsx:34 msgid "Waiting" msgstr "Väntande" @@ -331,23 +366,132 @@ msgstr[1] "%d fel upptäcktes" msgid "Basic popover" msgstr "Enkel popover" -#: src/components/l10n/L10nPage.jsx:72 -#: src/components/l10n/LanguageSwitcher.jsx:50 -msgid "language" +#. TRANSLATORS: placeholder text for search input in the keyboard selector. +#: src/components/l10n/KeymapSelector.jsx:82 +msgid "Filter by description or keymap code" +msgstr "" + +#: src/components/l10n/KeymapSelector.jsx:89 +#, fuzzy +msgid "Available keymaps" +msgstr "Tillgängliga enheter" + +#: src/components/l10n/L10nPage.jsx:66 src/components/l10n/L10nPage.jsx:140 +msgid "Select time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:67 +#, c-format +msgid "%s will use the selected time zone." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:128 +msgid "Time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:134 +msgid "Change time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:139 +#, fuzzy +msgid "Time zone not selected yet" +msgstr "inte vald" + +#: src/components/l10n/L10nPage.jsx:182 src/components/l10n/L10nPage.jsx:258 +#, fuzzy +msgid "Select language" msgstr "språk" -#. TRANSLATORS: page header +#: src/components/l10n/L10nPage.jsx:183 +#, fuzzy, c-format +msgid "%s will use the selected language." +msgstr "Systemet kommer att använda %s som dess standardspråk." + +#: src/components/l10n/L10nPage.jsx:246 +#, fuzzy +msgid "Language" +msgstr "språk" + +#: src/components/l10n/L10nPage.jsx:252 +#, fuzzy +msgid "Change language" +msgstr "språk" + +#: src/components/l10n/L10nPage.jsx:257 +#, fuzzy +msgid "Language not selected yet" +msgstr "inte vald" + +#: src/components/l10n/L10nPage.jsx:295 src/components/l10n/L10nPage.jsx:369 +#, fuzzy +msgid "Select keyboard" +msgstr "vald" + +#: src/components/l10n/L10nPage.jsx:296 +#, c-format +msgid "%s will use the selected keyboard." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:357 +msgid "Keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:363 +#, fuzzy +msgid "Change keyboard" +msgstr "Ändra krypteringslösenord" + +#: src/components/l10n/L10nPage.jsx:368 +#, fuzzy +msgid "Keyboard not selected yet" +msgstr "inte vald" + #. TRANSLATORS: page section -#: src/components/l10n/L10nPage.jsx:84 -#: src/components/overview/L10nSection.jsx:82 +#. TRANSLATORS: page title +#: src/components/l10n/L10nPage.jsx:385 +#: src/components/overview/L10nSection.jsx:52 msgid "Localization" msgstr "Lokalisering" +#: src/components/l10n/L10nPage.jsx:387 +#: src/components/product/ProductPage.jsx:434 +#: src/components/software/SoftwarePage.jsx:81 +#: src/components/storage/DASDPage.jsx:187 +#: src/components/storage/ISCSIPage.jsx:39 +#: src/components/storage/ProposalPage.jsx:218 +#: src/components/storage/ZFCPPage.jsx:736 +#: src/components/users/UsersPage.jsx:30 +msgid "Back" +msgstr "Bakåt" + #: src/components/l10n/LanguageSwitcher.jsx:46 -#| msgid "language" msgid "Display Language" msgstr "Visningsspråk" +#: src/components/l10n/LanguageSwitcher.jsx:50 +msgid "language" +msgstr "språk" + +#: src/components/l10n/LocaleSelector.jsx:82 +msgid "Filter by language, territory or locale code" +msgstr "" + +#: src/components/l10n/LocaleSelector.jsx:89 +#, fuzzy +msgid "Available locales" +msgstr "Tillgängliga enheter" + +#. TRANSLATORS: placeholder text for search input in the timezone selector. +#: src/components/l10n/TimezoneSelector.jsx:102 +msgid "Filter by territory, time zone code or UTC offset" +msgstr "" + +#: src/components/l10n/TimezoneSelector.jsx:109 +#, fuzzy +msgid "Available time zones" +msgstr "Tillgängliga enheter" + #: src/components/layout/Loading.jsx:30 msgid "Loading installation environment, please wait." msgstr "Laddar installationsmiljö, vänligen vänta." @@ -408,7 +552,7 @@ msgid "IP addresses" msgstr "IP adresser" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:233 +#: src/components/storage/ProposalVolumes.jsx:234 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:170 @@ -552,6 +696,8 @@ msgid "WPA & WPA2 Personal" msgstr "WPA & WPA2 Personal" #: src/components/network/WifiConnectionForm.jsx:91 +#: src/components/product/ProductPage.jsx:128 +#: src/components/product/ProductPage.jsx:194 #: src/components/storage/ZFCPDiskForm.jsx:112 #: src/components/storage/iscsi/DiscoverForm.jsx:108 #: src/components/storage/iscsi/LoginForm.jsx:72 @@ -624,9 +770,9 @@ msgstr "%s anslutningen väntar på en tillståndsändring" msgid "Forget network" msgstr "Glöm nätverk" -#. TRANSLATORS: %s will be replaced by a language name and code, -#. example: "English (en_US.UTF-8)" -#: src/components/overview/L10nSection.jsx:70 +#. TRANSLATORS: %s will be replaced by a language name and territory, example: +#. "English (United States)". +#: src/components/overview/L10nSection.jsx:34 #, c-format msgid "The system will use %s as its default language." msgstr "Systemet kommer att använda %s som dess standardspråk." @@ -644,45 +790,63 @@ msgid_plural "%d connections set:" msgstr[0] "%d anslutning inställd:" msgstr[1] "%d anslutningar inställda:" +#. TRANSLATORS: page title +#: src/components/overview/Overview.jsx:47 +msgid "Installation Summary" +msgstr "Installationssammanfattning" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/overview/ProductSection.jsx:48 +#, c-format +msgid "%s (registered)" +msgstr "%s (registrerad)" + #: src/components/overview/SoftwareSection.jsx:37 msgid "Reading software repositories" msgstr "Läser programvaruförråd" #. TRANSLATORS: clickable link label -#: src/components/overview/SoftwareSection.jsx:135 +#: src/components/overview/SoftwareSection.jsx:131 msgid "Refresh the repositories" msgstr "Uppdatera förråden" -#. TRANSLATORS: page section -#. TRANSLATORS: page title -#: src/components/overview/SoftwareSection.jsx:145 -#: src/components/software/SoftwarePage.jsx:81 -msgid "Software" -msgstr "Programvara" - -#: src/components/overview/StorageSection.jsx:36 -#: src/components/storage/ProposalSettingsSection.jsx:122 +#: src/components/overview/StorageSection.jsx:42 +#: src/components/storage/ProposalSettingsSection.jsx:126 msgid "No device selected yet" msgstr "Ingen enhet vald ännu" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:44 +#: src/components/overview/StorageSection.jsx:52 +#, fuzzy, c-format +msgid "Install using device %s shrinking existing partitions as needed" +msgstr "Installerar på enhet %s och raderar allt innehåll" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:56 +#, fuzzy, c-format +msgid "Install using device %s without modifying existing partitions" +msgstr "Installerar på enhet %s och raderar allt innehåll" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:60 #, c-format msgid "Install using device %s and deleting all its content" msgstr "Installerar på enhet %s och raderar allt innehåll" -#: src/components/overview/StorageSection.jsx:57 +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:66 +#, fuzzy, c-format +msgid "Install using device %s" +msgstr "Installationsenhet" + +#: src/components/overview/StorageSection.jsx:83 msgid "Probing storage devices" msgstr "Undersöker lagringsenheter" -#. TRANSLATORS: page title -#. TRANSLATORS: page section title -#: src/components/overview/StorageSection.jsx:182 -#: src/components/storage/ProposalPage.jsx:218 -msgid "Storage" -msgstr "Lagring" - #. TRANSLATORS: %s will be replaced by the user name #: src/components/overview/UsersSection.jsx:80 #, c-format @@ -718,6 +882,96 @@ msgstr "Rootautentiseringsuppsättning för användning av offentlig SSH nyckel" msgid "Users" msgstr "Användare" +#: src/components/product/ProductPage.jsx:63 +#: src/components/product/ProductSelectionPage.jsx:73 +msgid "Choose a product" +msgstr "Välj en produkt" + +#: src/components/product/ProductPage.jsx:122 +#, c-format +msgid "Register %s" +msgstr "Registrera %s" + +#: src/components/product/ProductPage.jsx:188 +#, c-format +msgid "Deregister %s" +msgstr "Avregistrera %s" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:202 +#, c-format +msgid "Do you want to deregister %s?" +msgstr "Vill du avregistrera %s?" + +#: src/components/product/ProductPage.jsx:227 +msgid "Registered warning" +msgstr "Registrerad varning" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:232 +#, c-format +msgid "The product %s must be deregistered before selecting a new product." +msgstr "Produkten %s måste avregistreras innan du väljer en ny produkt." + +#: src/components/product/ProductPage.jsx:263 +msgid "Change product" +msgstr "Ändra produkt" + +#: src/components/product/ProductPage.jsx:305 +msgid "Register" +msgstr "Registrera" + +#: src/components/product/ProductPage.jsx:337 +msgid "Deregister product" +msgstr "Avregistrera produkt" + +#: src/components/product/ProductPage.jsx:370 +msgid "Code:" +msgstr "Kod:" + +#: src/components/product/ProductPage.jsx:374 +msgid "Email:" +msgstr "E-post:" + +#. TRANSLATORS: section title. +#: src/components/product/ProductPage.jsx:390 +msgid "Registration" +msgstr "Registrering" + +#: src/components/product/ProductPage.jsx:399 +msgid "This product requires registration." +msgstr "Denna produkt kräver registrering." + +#: src/components/product/ProductPage.jsx:405 +msgid "This product does not require registration." +msgstr "Denna produkt kräver ingen registrering." + +#: src/components/product/ProductRegistrationForm.jsx:63 +msgid "Registration code" +msgstr "Registreringskod" + +#: src/components/product/ProductRegistrationForm.jsx:66 +msgid "Email" +msgstr "E-post" + +#: src/components/product/ProductSelectionPage.jsx:58 +msgid "Loading available products, please wait..." +msgstr "Laddar tillgängliga produkter, vänligen vänta..." + +#. TRANSLATORS: page header +#: src/components/product/ProductSelectionPage.jsx:64 +msgid "Product selection" +msgstr "Produktval" + +#. TRANSLATORS: button label +#: src/components/product/ProductSelectionPage.jsx:69 +msgid "Select" +msgstr "Välj" + +#: src/components/product/ProductSelector.jsx:29 +msgid "No products available for selection" +msgstr "Inga produkter tillgängliga för val" + #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 msgid "Question" @@ -737,10 +991,6 @@ msgstr "Krypterad enhet" msgid "Encryption Password" msgstr "Krypteringslösenord" -#: src/components/software/ChangeProductLink.jsx:36 -msgid "Change product" -msgstr "Ändra produkt" - #. TRANSLATORS: pattern status, selected to install (by user) #: src/components/software/PatternItem.jsx:63 msgid "selected" @@ -758,47 +1008,14 @@ msgstr "inte vald" #. TRANSLATORS: error summary, always plural, %d is replaced by number of errors (2 or more) #. if there is just a single error then the error is displayed directly instead of this summary -#: src/components/software/PatternSelector.jsx:206 +#: src/components/software/PatternSelector.jsx:207 #, c-format 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:215 -#: src/components/software/PatternSelector.jsx:216 -msgid "Search" -msgstr "Sök" - -#: src/components/software/ProductSelectionPage.jsx:69 -msgid "Loading available products, please wait..." -msgstr "Laddar tillgängliga produkter, vänligen vänta..." - -#. TRANSLATORS: page header -#: src/components/software/ProductSelectionPage.jsx:94 -msgid "Product selection" -msgstr "Produktval" - -#. TRANSLATORS: button label -#: src/components/software/ProductSelectionPage.jsx:99 -msgid "Select" -msgstr "Välj" - -#: src/components/software/ProductSelectionPage.jsx:104 -msgid "Choose a product" -msgstr "Välj en produkt" - -#: src/components/software/SoftwarePage.jsx:81 -#: src/components/storage/DASDPage.jsx:187 -#: src/components/storage/ISCSIPage.jsx:39 -#: src/components/storage/ProposalPage.jsx:218 -#: src/components/storage/ZFCPPage.jsx:736 -#: src/components/users/UsersPage.jsx:30 -msgid "Back" -msgstr "Bakåt" +msgid "Software summary and filter options" +msgstr "Programsammanfattning och filteralternativ" #. TRANSLATORS: %s will be replaced by the estimated installation size, #. example: "728.8 MiB" @@ -977,29 +1194,29 @@ msgstr "Anslut till iSCSI mål" msgid "iSCSI" msgstr "iSCSI" -#: src/components/storage/ProposalSettingsSection.jsx:135 +#: src/components/storage/ProposalSettingsSection.jsx:139 msgid "Select the device for installing the system." msgstr "Välj enhet för installation av systemet." -#: src/components/storage/ProposalSettingsSection.jsx:140 #: src/components/storage/ProposalSettingsSection.jsx:144 -#: src/components/storage/ProposalSettingsSection.jsx:240 +#: src/components/storage/ProposalSettingsSection.jsx:148 +#: src/components/storage/ProposalSettingsSection.jsx:244 msgid "Installation device" msgstr "Installationsenhet" -#: src/components/storage/ProposalSettingsSection.jsx:150 +#: src/components/storage/ProposalSettingsSection.jsx:154 msgid "No devices found" msgstr "Inga enheter hittades" -#: src/components/storage/ProposalSettingsSection.jsx:237 +#: src/components/storage/ProposalSettingsSection.jsx:241 msgid "Devices for creating the volume group" msgstr "Enheter för att skapa volymgrupp" -#: src/components/storage/ProposalSettingsSection.jsx:246 +#: src/components/storage/ProposalSettingsSection.jsx:250 msgid "Custom devices" msgstr "Anpassade enheter" -#: src/components/storage/ProposalSettingsSection.jsx:310 +#: src/components/storage/ProposalSettingsSection.jsx:314 msgid "" "Configuration of the system volume group. All the file systems will be " "created in a logical volume of the system volume group." @@ -1007,37 +1224,52 @@ msgstr "" "Konfiguration av systemvolymgruppen. Alla filsystem kommer att skapas i en " "logisk volym av systemvolymgruppen." -#: src/components/storage/ProposalSettingsSection.jsx:316 +#: src/components/storage/ProposalSettingsSection.jsx:320 msgid "Configure the LVM settings" msgstr "Konfigurera LVM-inställningar" -#: src/components/storage/ProposalSettingsSection.jsx:321 -#: src/components/storage/ProposalSettingsSection.jsx:341 +#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:345 msgid "LVM settings" msgstr "LVM-inställningar" -#: src/components/storage/ProposalSettingsSection.jsx:334 +#: src/components/storage/ProposalSettingsSection.jsx:338 msgid "Use logical volume management (LVM)" msgstr "Använd logisk volymhantering (LVM)" -#: src/components/storage/ProposalSettingsSection.jsx:342 +#: src/components/storage/ProposalSettingsSection.jsx:346 msgid "System Volume Group" msgstr "System volymgrupp" -#: src/components/storage/ProposalSettingsSection.jsx:470 +#: src/components/storage/ProposalSettingsSection.jsx:474 msgid "Change encryption password" msgstr "Ändra krypteringslösenord" -#: src/components/storage/ProposalSettingsSection.jsx:475 -#: src/components/storage/ProposalSettingsSection.jsx:496 +#: src/components/storage/ProposalSettingsSection.jsx:479 +#: src/components/storage/ProposalSettingsSection.jsx:500 msgid "Encryption settings" msgstr "Krypteringsinställningar" -#: src/components/storage/ProposalSettingsSection.jsx:489 +#: src/components/storage/ProposalSettingsSection.jsx:493 msgid "Use encryption" msgstr "Använd kryptering" -#: src/components/storage/ProposalSettingsSection.jsx:557 +#: src/components/storage/ProposalSettingsSection.jsx:578 +msgid "" +"Select how to make free space in the disks selected for allocating the " +"file systems." +msgstr "" + +#. TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" +#: src/components/storage/ProposalSettingsSection.jsx:584 +msgid "Find space" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:588 +msgid "Space Policy" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:662 msgid "Settings" msgstr "Inställningar" @@ -1090,48 +1322,48 @@ msgid "partition" msgstr "partition" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:215 +#: src/components/storage/ProposalVolumes.jsx:216 msgid "encrypted" msgstr "krypterad" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:217 +#: src/components/storage/ProposalVolumes.jsx:218 msgid "with snapshots" msgstr "med ögonblicksavbilder" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:219 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "transactional" msgstr "transaktionell" -#: src/components/storage/ProposalVolumes.jsx:228 +#: src/components/storage/ProposalVolumes.jsx:229 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "Ta bort" -#: src/components/storage/ProposalVolumes.jsx:274 +#: src/components/storage/ProposalVolumes.jsx:275 msgid "Edit file system" msgstr "Redigera filsystem" -#: src/components/storage/ProposalVolumes.jsx:306 +#: src/components/storage/ProposalVolumes.jsx:307 #: src/components/storage/VolumeForm.jsx:500 msgid "Mount point" msgstr "Monteringspunkt" -#: src/components/storage/ProposalVolumes.jsx:307 +#: src/components/storage/ProposalVolumes.jsx:308 msgid "Details" msgstr "Detaljer" -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:309 #: src/components/storage/VolumeForm.jsx:517 msgid "Size" msgstr "Storlek" -#: src/components/storage/ProposalVolumes.jsx:344 +#: src/components/storage/ProposalVolumes.jsx:345 msgid "Table with mount points" msgstr "Tabell med monteringspunkter" -#: src/components/storage/ProposalVolumes.jsx:407 +#: src/components/storage/ProposalVolumes.jsx:408 msgid "File systems to create in your system" msgstr "Filsystem att skapa i ditt system" @@ -1347,64 +1579,64 @@ msgid "Storage zFCP" msgstr "Lagring zFCP" #. TRANSLATORS: multipath device type -#: src/components/storage/device-utils.jsx:98 +#: src/components/storage/device-utils.jsx:97 msgid "Multipath" msgstr "Flervägs" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/device-utils.jsx:103 +#: src/components/storage/device-utils.jsx:102 #, c-format msgid "DASD %s" msgstr "DASD %s" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/device-utils.jsx:108 +#: src/components/storage/device-utils.jsx:107 #, c-format msgid "Software %s" msgstr "Programvara %s" -#: src/components/storage/device-utils.jsx:113 +#: src/components/storage/device-utils.jsx:112 msgid "SD Card" msgstr "SD-kort" #. TRANSLATORS: %s is replaced by the device transport name, e.g. USB, SATA, SCSI... -#: src/components/storage/device-utils.jsx:115 +#: src/components/storage/device-utils.jsx:114 #, c-format msgid "Transport %s" msgstr "Transport %s" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:134 +#: src/components/storage/device-utils.jsx:133 #, c-format msgid "Members: %s" msgstr "Medlemmar: %s" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:143 +#: src/components/storage/device-utils.jsx:142 #, c-format msgid "Devices: %s" msgstr "Enheter: %s" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/device-utils.jsx:152 +#: src/components/storage/device-utils.jsx:151 #, c-format msgid "Wires: %s" msgstr "Kablar: %s" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/device-utils.jsx:176 +#: src/components/storage/device-utils.jsx:175 #, c-format msgid "%s with %d partitions" msgstr "%s med %d partitioner" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/device-utils.jsx:200 +#: src/components/storage/device-utils.jsx:199 msgid "No content found" msgstr "Inget innehåll hittades" -#: src/components/storage/device-utils.jsx:271 +#: src/components/storage/device-utils.jsx:270 msgid "Available devices" msgstr "Tillgängliga enheter" @@ -1491,7 +1723,7 @@ msgstr "Redigera iSCSI initiativtagare" #. TRANSLATORS: iSCSI initiator name #: src/components/storage/iscsi/InitiatorForm.jsx:49 msgid "Initiator name" -msgstr "initiativtagarens namn" +msgstr "Initiativtagarens namn" #. TRANSLATORS: usually just keep the original text #. iBFT = iSCSI Boot Firmware Table, HW support for booting from iSCSI @@ -1580,6 +1812,70 @@ msgstr "Upptäck" msgid "Targets" msgstr "Mål" +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:59 +msgid "Delete current content" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:63 +msgid "Shrink existing partitions" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:67 +#, fuzzy +msgid "Use available space" +msgstr "Tillgängliga enheter" + +#: src/components/storage/space-policy-utils.jsx:79 +msgid "All partitions will be removed and any data in the disks will be lost." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:82 +msgid "" +"The data is kept, but the current partitions will be resized as needed to " +"make enough space." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:85 +msgid "" +"The data is kept and existing partitions will not be modified. Only the " +"space that is not assigned to any partition will be used." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:112 +msgid "Select a mechanism to make space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:137 +#, c-format +msgid "deleting all content of the installation device" +msgid_plural "deleting all content of the %d selected disks" +msgstr[0] "" +msgstr[1] "" + +#: src/components/storage/space-policy-utils.jsx:147 +#, c-format +msgid "shrinking partitions of the installation device" +msgid_plural "shrinking partitions of the %d selected disks" +msgstr[0] "" +msgstr[1] "" + +#. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence +#. would read as "Find space without modifying any partition". +#: src/components/storage/space-policy-utils.jsx:155 +#, fuzzy +msgid "without modifying any partition" +msgstr "%s med %d partitioner" + +#: src/components/storage/space-policy-utils.jsx:170 +#, c-format +msgid "This will only affect the installation device" +msgid_plural "This will affect the %d disks selected for installation" +msgstr[0] "" +msgstr[1] "" + #: src/components/storage/utils.js:44 msgid "KiB" msgstr "KiB" diff --git a/web/po/uk.po b/web/po/uk.po index 6c0254a32e..cfe42f68fa 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-11-05 02:14+0000\n" +"POT-Creation-Date: 2023-12-03 02:17+0000\n" "PO-Revision-Date: 2023-08-06 21:15+0000\n" "Last-Translator: Milachew \n" "Language-Team: Ukrainian =2 && n" -"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "X-Generator: Weblate 4.9.1\n" -#: src/App.jsx:112 +#: src/App.jsx:110 msgid "Diagnostic tools" msgstr "" @@ -64,6 +64,7 @@ msgstr "" #: src/components/core/About.jsx:71 src/components/core/FileViewer.jsx:80 #: src/components/core/Sidebar.jsx:177 src/components/core/Terminal.jsx:48 #: src/components/network/WifiSelector.jsx:151 +#: src/components/product/ProductPage.jsx:239 msgid "Close" msgstr "Закрити" @@ -132,18 +133,23 @@ msgid "" msgstr "" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:97 -#: src/components/storage/ProposalSettingsSection.jsx:166 -#: src/components/storage/ProposalSettingsSection.jsx:359 -#: src/components/storage/ProposalSettingsSection.jsx:504 +#: src/components/core/InstallButton.jsx:97 src/components/l10n/L10nPage.jsx:75 +#: src/components/l10n/L10nPage.jsx:191 src/components/l10n/L10nPage.jsx:304 +#: src/components/product/ProductPage.jsx:71 +#: src/components/product/ProductPage.jsx:140 +#: src/components/product/ProductPage.jsx:207 +#: src/components/storage/ProposalSettingsSection.jsx:170 +#: src/components/storage/ProposalSettingsSection.jsx:363 +#: src/components/storage/ProposalSettingsSection.jsx:508 +#: src/components/storage/ProposalSettingsSection.jsx:604 #: src/components/storage/ProposalVolumes.jsx:148 -#: src/components/storage/ProposalVolumes.jsx:282 +#: src/components/storage/ProposalVolumes.jsx:283 #: src/components/storage/ZFCPPage.jsx:513 msgid "Accept" msgstr "" #. TRANSLATORS: button label -#: src/components/core/InstallButton.jsx:145 +#: src/components/core/InstallButton.jsx:148 msgid "Install" msgstr "" @@ -193,10 +199,40 @@ msgstr "" msgid "There are new issues" msgstr "" -#: src/components/core/IssuesPage.jsx:115 +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:92 +#: src/components/overview/ProductSection.jsx:71 +#: src/components/product/ProductPage.jsx:434 +msgid "Product" +msgstr "" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section title +#: src/components/core/IssuesPage.jsx:100 +#: src/components/overview/StorageSection.jsx:208 +#: src/components/storage/ProposalPage.jsx:218 +msgid "Storage" +msgstr "" + +#. TRANSLATORS: page title +#. TRANSLATORS: page section +#: src/components/core/IssuesPage.jsx:108 +#: src/components/overview/SoftwareSection.jsx:141 +#: src/components/software/SoftwarePage.jsx:81 +msgid "Software" +msgstr "" + +#: src/components/core/IssuesPage.jsx:129 msgid "No issues found. Everything looks ok." msgstr "" +#. TRANSLATORS: search field placeholder text +#: src/components/core/ListSearch.jsx:50 +#: src/components/software/PatternSelector.jsx:220 +#: src/components/software/PatternSelector.jsx:221 +msgid "Search" +msgstr "" + #: src/components/core/LogsButton.jsx:98 msgid "Collecting logs..." msgstr "" @@ -256,12 +292,11 @@ msgstr "" #. TRANSLATORS: dropdown label #: src/components/core/RowActions.jsx:66 #: src/components/storage/ProposalVolumes.jsx:119 -#: src/components/storage/ProposalVolumes.jsx:309 +#: src/components/storage/ProposalVolumes.jsx:310 msgid "Actions" msgstr "" #: src/components/core/SectionSkeleton.jsx:29 -#: src/components/core/SectionSkeleton.jsx:34 msgid "Waiting" msgstr "" @@ -318,22 +353,121 @@ msgstr[2] "" msgid "Basic popover" msgstr "" -#: src/components/l10n/L10nPage.jsx:72 -#: src/components/l10n/LanguageSwitcher.jsx:50 -msgid "language" +#. TRANSLATORS: placeholder text for search input in the keyboard selector. +#: src/components/l10n/KeymapSelector.jsx:82 +msgid "Filter by description or keymap code" +msgstr "" + +#: src/components/l10n/KeymapSelector.jsx:89 +msgid "Available keymaps" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:66 src/components/l10n/L10nPage.jsx:140 +msgid "Select time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:67 +#, c-format +msgid "%s will use the selected time zone." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:128 +msgid "Time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:134 +msgid "Change time zone" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:139 +msgid "Time zone not selected yet" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:182 src/components/l10n/L10nPage.jsx:258 +msgid "Select language" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:183 +#, c-format +msgid "%s will use the selected language." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:246 +msgid "Language" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:252 +msgid "Change language" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:257 +msgid "Language not selected yet" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:295 src/components/l10n/L10nPage.jsx:369 +msgid "Select keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:296 +#, c-format +msgid "%s will use the selected keyboard." +msgstr "" + +#: src/components/l10n/L10nPage.jsx:357 +msgid "Keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:363 +msgid "Change keyboard" +msgstr "" + +#: src/components/l10n/L10nPage.jsx:368 +msgid "Keyboard not selected yet" msgstr "" -#. TRANSLATORS: page header #. TRANSLATORS: page section -#: src/components/l10n/L10nPage.jsx:84 -#: src/components/overview/L10nSection.jsx:82 +#. TRANSLATORS: page title +#: src/components/l10n/L10nPage.jsx:385 +#: src/components/overview/L10nSection.jsx:52 msgid "Localization" msgstr "" +#: src/components/l10n/L10nPage.jsx:387 +#: src/components/product/ProductPage.jsx:434 +#: src/components/software/SoftwarePage.jsx:81 +#: src/components/storage/DASDPage.jsx:187 +#: src/components/storage/ISCSIPage.jsx:39 +#: src/components/storage/ProposalPage.jsx:218 +#: src/components/storage/ZFCPPage.jsx:736 +#: src/components/users/UsersPage.jsx:30 +msgid "Back" +msgstr "" + #: src/components/l10n/LanguageSwitcher.jsx:46 msgid "Display Language" msgstr "" +#: src/components/l10n/LanguageSwitcher.jsx:50 +msgid "language" +msgstr "" + +#: src/components/l10n/LocaleSelector.jsx:82 +msgid "Filter by language, territory or locale code" +msgstr "" + +#: src/components/l10n/LocaleSelector.jsx:89 +msgid "Available locales" +msgstr "" + +#. TRANSLATORS: placeholder text for search input in the timezone selector. +#: src/components/l10n/TimezoneSelector.jsx:102 +msgid "Filter by territory, time zone code or UTC offset" +msgstr "" + +#: src/components/l10n/TimezoneSelector.jsx:109 +msgid "Available time zones" +msgstr "" + #: src/components/layout/Loading.jsx:30 msgid "Loading installation environment, please wait." msgstr "" @@ -394,7 +528,7 @@ msgid "IP addresses" msgstr "" #: src/components/network/ConnectionsTable.jsx:67 -#: src/components/storage/ProposalVolumes.jsx:233 +#: src/components/storage/ProposalVolumes.jsx:234 #: src/components/storage/iscsi/InitiatorPresenter.jsx:49 #: src/components/storage/iscsi/NodesPresenter.jsx:73 #: src/components/users/FirstUser.jsx:170 @@ -535,6 +669,8 @@ msgid "WPA & WPA2 Personal" msgstr "" #: src/components/network/WifiConnectionForm.jsx:91 +#: src/components/product/ProductPage.jsx:128 +#: src/components/product/ProductPage.jsx:194 #: src/components/storage/ZFCPDiskForm.jsx:112 #: src/components/storage/iscsi/DiscoverForm.jsx:108 #: src/components/storage/iscsi/LoginForm.jsx:72 @@ -607,9 +743,9 @@ msgstr "" msgid "Forget network" msgstr "" -#. TRANSLATORS: %s will be replaced by a language name and code, -#. example: "English (en_US.UTF-8)" -#: src/components/overview/L10nSection.jsx:70 +#. TRANSLATORS: %s will be replaced by a language name and territory, example: +#. "English (United States)". +#: src/components/overview/L10nSection.jsx:34 #, c-format msgid "The system will use %s as its default language." msgstr "" @@ -628,43 +764,61 @@ msgstr[0] "" msgstr[1] "" msgstr[2] "" +#. TRANSLATORS: page title +#: src/components/overview/Overview.jsx:47 +msgid "Installation Summary" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/overview/ProductSection.jsx:48 +#, c-format +msgid "%s (registered)" +msgstr "" + #: src/components/overview/SoftwareSection.jsx:37 msgid "Reading software repositories" msgstr "" #. TRANSLATORS: clickable link label -#: src/components/overview/SoftwareSection.jsx:135 +#: src/components/overview/SoftwareSection.jsx:131 msgid "Refresh the repositories" msgstr "" -#. TRANSLATORS: page section -#. TRANSLATORS: page title -#: src/components/overview/SoftwareSection.jsx:145 -#: src/components/software/SoftwarePage.jsx:81 -msgid "Software" +#: src/components/overview/StorageSection.jsx:42 +#: src/components/storage/ProposalSettingsSection.jsx:126 +msgid "No device selected yet" msgstr "" -#: src/components/overview/StorageSection.jsx:36 -#: src/components/storage/ProposalSettingsSection.jsx:122 -msgid "No device selected yet" +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:52 +#, c-format +msgid "Install using device %s shrinking existing partitions as needed" msgstr "" #. TRANSLATORS: %s will be replaced by the device name and its size, #. example: "/dev/sda, 20 GiB" -#: src/components/overview/StorageSection.jsx:44 +#: src/components/overview/StorageSection.jsx:56 +#, c-format +msgid "Install using device %s without modifying existing partitions" +msgstr "" + +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:60 #, c-format msgid "Install using device %s and deleting all its content" msgstr "" -#: src/components/overview/StorageSection.jsx:57 -msgid "Probing storage devices" +#. TRANSLATORS: %s will be replaced by the device name and its size, +#. example: "/dev/sda, 20 GiB" +#: src/components/overview/StorageSection.jsx:66 +#, c-format +msgid "Install using device %s" msgstr "" -#. TRANSLATORS: page title -#. TRANSLATORS: page section title -#: src/components/overview/StorageSection.jsx:182 -#: src/components/storage/ProposalPage.jsx:218 -msgid "Storage" +#: src/components/overview/StorageSection.jsx:83 +msgid "Probing storage devices" msgstr "" #. TRANSLATORS: %s will be replaced by the user name @@ -700,6 +854,96 @@ msgstr "" msgid "Users" msgstr "" +#: src/components/product/ProductPage.jsx:63 +#: src/components/product/ProductSelectionPage.jsx:73 +msgid "Choose a product" +msgstr "" + +#: src/components/product/ProductPage.jsx:122 +#, c-format +msgid "Register %s" +msgstr "" + +#: src/components/product/ProductPage.jsx:188 +#, c-format +msgid "Deregister %s" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:202 +#, c-format +msgid "Do you want to deregister %s?" +msgstr "" + +#: src/components/product/ProductPage.jsx:227 +msgid "Registered warning" +msgstr "" + +#. TRANSLATORS: %s is replaced by a product name (e.g., SUSE ALP-Dolomite) +#: src/components/product/ProductPage.jsx:232 +#, c-format +msgid "The product %s must be deregistered before selecting a new product." +msgstr "" + +#: src/components/product/ProductPage.jsx:263 +msgid "Change product" +msgstr "" + +#: src/components/product/ProductPage.jsx:305 +msgid "Register" +msgstr "" + +#: src/components/product/ProductPage.jsx:337 +msgid "Deregister product" +msgstr "" + +#: src/components/product/ProductPage.jsx:370 +msgid "Code:" +msgstr "" + +#: src/components/product/ProductPage.jsx:374 +msgid "Email:" +msgstr "" + +#. TRANSLATORS: section title. +#: src/components/product/ProductPage.jsx:390 +msgid "Registration" +msgstr "" + +#: src/components/product/ProductPage.jsx:399 +msgid "This product requires registration." +msgstr "" + +#: src/components/product/ProductPage.jsx:405 +msgid "This product does not require registration." +msgstr "" + +#: src/components/product/ProductRegistrationForm.jsx:63 +msgid "Registration code" +msgstr "" + +#: src/components/product/ProductRegistrationForm.jsx:66 +msgid "Email" +msgstr "" + +#: src/components/product/ProductSelectionPage.jsx:58 +msgid "Loading available products, please wait..." +msgstr "" + +#. TRANSLATORS: page header +#: src/components/product/ProductSelectionPage.jsx:64 +msgid "Product selection" +msgstr "" + +#. TRANSLATORS: button label +#: src/components/product/ProductSelectionPage.jsx:69 +msgid "Select" +msgstr "" + +#: src/components/product/ProductSelector.jsx:29 +msgid "No products available for selection" +msgstr "" + #: src/components/questions/GenericQuestion.jsx:35 #: src/components/questions/LuksActivationQuestion.jsx:60 msgid "Question" @@ -719,10 +963,6 @@ msgstr "" msgid "Encryption Password" msgstr "" -#: src/components/software/ChangeProductLink.jsx:36 -msgid "Change product" -msgstr "" - #. TRANSLATORS: pattern status, selected to install (by user) #: src/components/software/PatternItem.jsx:63 msgid "selected" @@ -740,46 +980,13 @@ msgstr "" #. TRANSLATORS: error summary, always plural, %d is replaced by number of errors (2 or more) #. if there is just a single error then the error is displayed directly instead of this summary -#: src/components/software/PatternSelector.jsx:206 +#: src/components/software/PatternSelector.jsx:207 #, c-format msgid "%d errors" msgstr "" -#: src/components/software/PatternSelector.jsx:210 -msgid "Software summary and filter options" -msgstr "" - -#. TRANSLATORS: search field placeholder text #: src/components/software/PatternSelector.jsx:215 -#: src/components/software/PatternSelector.jsx:216 -msgid "Search" -msgstr "" - -#: src/components/software/ProductSelectionPage.jsx:69 -msgid "Loading available products, please wait..." -msgstr "" - -#. TRANSLATORS: page header -#: src/components/software/ProductSelectionPage.jsx:94 -msgid "Product selection" -msgstr "" - -#. TRANSLATORS: button label -#: src/components/software/ProductSelectionPage.jsx:99 -msgid "Select" -msgstr "" - -#: src/components/software/ProductSelectionPage.jsx:104 -msgid "Choose a product" -msgstr "" - -#: src/components/software/SoftwarePage.jsx:81 -#: src/components/storage/DASDPage.jsx:187 -#: src/components/storage/ISCSIPage.jsx:39 -#: src/components/storage/ProposalPage.jsx:218 -#: src/components/storage/ZFCPPage.jsx:736 -#: src/components/users/UsersPage.jsx:30 -msgid "Back" +msgid "Software summary and filter options" msgstr "" #. TRANSLATORS: %s will be replaced by the estimated installation size, @@ -959,65 +1166,80 @@ msgstr "" msgid "iSCSI" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:135 +#: src/components/storage/ProposalSettingsSection.jsx:139 msgid "Select the device for installing the system." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:140 #: src/components/storage/ProposalSettingsSection.jsx:144 -#: src/components/storage/ProposalSettingsSection.jsx:240 +#: src/components/storage/ProposalSettingsSection.jsx:148 +#: src/components/storage/ProposalSettingsSection.jsx:244 msgid "Installation device" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:150 +#: src/components/storage/ProposalSettingsSection.jsx:154 msgid "No devices found" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:237 +#: src/components/storage/ProposalSettingsSection.jsx:241 msgid "Devices for creating the volume group" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:246 +#: src/components/storage/ProposalSettingsSection.jsx:250 msgid "Custom devices" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:310 +#: src/components/storage/ProposalSettingsSection.jsx:314 msgid "" "Configuration of the system volume group. All the file systems will be " "created in a logical volume of the system volume group." msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:316 +#: src/components/storage/ProposalSettingsSection.jsx:320 msgid "Configure the LVM settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:321 -#: src/components/storage/ProposalSettingsSection.jsx:341 +#: src/components/storage/ProposalSettingsSection.jsx:325 +#: src/components/storage/ProposalSettingsSection.jsx:345 msgid "LVM settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:334 +#: src/components/storage/ProposalSettingsSection.jsx:338 msgid "Use logical volume management (LVM)" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:342 +#: src/components/storage/ProposalSettingsSection.jsx:346 msgid "System Volume Group" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:470 +#: src/components/storage/ProposalSettingsSection.jsx:474 msgid "Change encryption password" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:475 -#: src/components/storage/ProposalSettingsSection.jsx:496 +#: src/components/storage/ProposalSettingsSection.jsx:479 +#: src/components/storage/ProposalSettingsSection.jsx:500 msgid "Encryption settings" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:489 +#: src/components/storage/ProposalSettingsSection.jsx:493 msgid "Use encryption" msgstr "" -#: src/components/storage/ProposalSettingsSection.jsx:557 +#: src/components/storage/ProposalSettingsSection.jsx:578 +msgid "" +"Select how to make free space in the disks selected for allocating the " +"file systems." +msgstr "" + +#. TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" +#: src/components/storage/ProposalSettingsSection.jsx:584 +msgid "Find space" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:588 +msgid "Space Policy" +msgstr "" + +#: src/components/storage/ProposalSettingsSection.jsx:662 msgid "Settings" msgstr "" @@ -1070,48 +1292,48 @@ msgid "partition" msgstr "" #. TRANSLATORS: filesystem flag, it uses an encryption -#: src/components/storage/ProposalVolumes.jsx:215 +#: src/components/storage/ProposalVolumes.jsx:216 msgid "encrypted" msgstr "" #. TRANSLATORS: filesystem flag, it allows creating snapshots -#: src/components/storage/ProposalVolumes.jsx:217 +#: src/components/storage/ProposalVolumes.jsx:218 msgid "with snapshots" msgstr "" #. TRANSLATORS: flag for transactional file system -#: src/components/storage/ProposalVolumes.jsx:219 +#: src/components/storage/ProposalVolumes.jsx:220 msgid "transactional" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:228 +#: src/components/storage/ProposalVolumes.jsx:229 #: src/components/storage/iscsi/NodesPresenter.jsx:77 msgid "Delete" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:274 +#: src/components/storage/ProposalVolumes.jsx:275 msgid "Edit file system" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:306 +#: src/components/storage/ProposalVolumes.jsx:307 #: src/components/storage/VolumeForm.jsx:500 msgid "Mount point" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:307 +#: src/components/storage/ProposalVolumes.jsx:308 msgid "Details" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:308 +#: src/components/storage/ProposalVolumes.jsx:309 #: src/components/storage/VolumeForm.jsx:517 msgid "Size" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:344 +#: src/components/storage/ProposalVolumes.jsx:345 msgid "Table with mount points" msgstr "" -#: src/components/storage/ProposalVolumes.jsx:407 +#: src/components/storage/ProposalVolumes.jsx:408 msgid "File systems to create in your system" msgstr "" @@ -1320,64 +1542,64 @@ msgid "Storage zFCP" msgstr "" #. TRANSLATORS: multipath device type -#: src/components/storage/device-utils.jsx:98 +#: src/components/storage/device-utils.jsx:97 msgid "Multipath" msgstr "" #. TRANSLATORS: %s is replaced by the device bus ID -#: src/components/storage/device-utils.jsx:103 +#: src/components/storage/device-utils.jsx:102 #, c-format msgid "DASD %s" msgstr "" #. TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1 -#: src/components/storage/device-utils.jsx:108 +#: src/components/storage/device-utils.jsx:107 #, c-format msgid "Software %s" msgstr "" -#: src/components/storage/device-utils.jsx:113 +#: src/components/storage/device-utils.jsx:112 msgid "SD Card" msgstr "" #. TRANSLATORS: %s is replaced by the device transport name, e.g. USB, SATA, SCSI... -#: src/components/storage/device-utils.jsx:115 +#: src/components/storage/device-utils.jsx:114 #, c-format msgid "Transport %s" msgstr "" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:134 +#: src/components/storage/device-utils.jsx:133 #, c-format msgid "Members: %s" msgstr "" #. TRANSLATORS: RAID details, %s is replaced by list of devices used by the array -#: src/components/storage/device-utils.jsx:143 +#: src/components/storage/device-utils.jsx:142 #, c-format msgid "Devices: %s" msgstr "" #. TRANSLATORS: multipath details, %s is replaced by list of connections used by the device -#: src/components/storage/device-utils.jsx:152 +#: src/components/storage/device-utils.jsx:151 #, c-format msgid "Wires: %s" msgstr "" #. TRANSLATORS: disk partition info, %s is replaced by partition table #. type (MS-DOS or GPT), %d is the number of the partitions -#: src/components/storage/device-utils.jsx:176 +#: src/components/storage/device-utils.jsx:175 #, c-format msgid "%s with %d partitions" msgstr "" #. TRANSLATORS: status message, no existing content was found on the disk, #. i.e. the disk is completely empty -#: src/components/storage/device-utils.jsx:200 +#: src/components/storage/device-utils.jsx:199 msgid "No content found" msgstr "" -#: src/components/storage/device-utils.jsx:271 +#: src/components/storage/device-utils.jsx:270 msgid "Available devices" msgstr "" @@ -1553,6 +1775,71 @@ msgstr "" msgid "Targets" msgstr "" +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:59 +msgid "Delete current content" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:63 +msgid "Shrink existing partitions" +msgstr "" + +#. TRANSLATORS: automatic actions to find space for installation in the target disk(s) +#: src/components/storage/space-policy-utils.jsx:67 +msgid "Use available space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:79 +msgid "All partitions will be removed and any data in the disks will be lost." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:82 +msgid "" +"The data is kept, but the current partitions will be resized as needed to " +"make enough space." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:85 +msgid "" +"The data is kept and existing partitions will not be modified. Only the " +"space that is not assigned to any partition will be used." +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:112 +msgid "Select a mechanism to make space" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:137 +#, c-format +msgid "deleting all content of the installation device" +msgid_plural "deleting all content of the %d selected disks" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: src/components/storage/space-policy-utils.jsx:147 +#, c-format +msgid "shrinking partitions of the installation device" +msgid_plural "shrinking partitions of the %d selected disks" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#. TRANSLATORS: This is presented next to the label "Find space", so the whole sentence +#. would read as "Find space without modifying any partition". +#: src/components/storage/space-policy-utils.jsx:155 +msgid "without modifying any partition" +msgstr "" + +#: src/components/storage/space-policy-utils.jsx:170 +#, c-format +msgid "This will only affect the installation device" +msgid_plural "This will affect the %d disks selected for installation" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + #: src/components/storage/utils.js:44 msgid "KiB" msgstr "" diff --git a/web/src/App.jsx b/web/src/App.jsx index 4099afcd10..c374c3c87b 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -41,7 +41,7 @@ import { } from "~/components/core"; import { LanguageSwitcher } from "./components/l10n"; import { Layout, Loading, Title } from "./components/layout"; -import { useL10n } from "./context/l10n"; +import { useInstallerL10n } from "./context/installerL10n"; // D-Bus connection attempts before displaying an error. const ATTEMPTS = 3; @@ -57,7 +57,7 @@ function App() { const client = useInstallerClient(); const { attempt } = useInstallerClientStatus(); const { products } = useProduct(); - const { language } = useL10n(); + const { language } = useInstallerL10n(); const [status, setStatus] = useState(undefined); const [phase, setPhase] = useState(undefined); diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx index 59b0753521..0e6bab251d 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.jsx @@ -70,9 +70,14 @@ describe("App", () => { onPhaseChange: onPhaseChangeFn, onStatusChange: onStatusChangeFn, }, - language: { - getUILanguage: jest.fn().mockResolvedValue("en-us"), - setUILanguage: jest.fn().mockResolvedValue("en-us"), + l10n: { + locales: jest.fn().mockResolvedValue([["en_us", "English", "United States"]]), + getLocales: jest.fn().mockResolvedValue(["en_us"]), + getUILocale: jest.fn().mockResolvedValue("en_us"), + setUILocale: jest.fn().mockResolvedValue("en_us"), + onTimezoneChange: jest.fn(), + onLocalesChange: jest.fn(), + onKeymapChange: jest.fn() } }; }); diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss index 0498607f60..22113f29e1 100644 --- a/web/src/assets/styles/blocks.scss +++ b/web/src/assets/styles/blocks.scss @@ -3,11 +3,15 @@ // section layouts. section:not([class^="pf-c"]) { display: grid; - grid-template-columns: min-content 1fr; + grid-template-columns: var(--section-icon-size) 1fr; grid-template-areas: "icon title" ".... content"; gap: var(--spacer-small); + padding-inline-start: calc( + var(--header-icon-size) - var(--section-icon-size) + ); + padding-inline-end: var(--section-icon-size); } section:not(:last-child, [class^="pf-c"]) { @@ -16,6 +20,8 @@ section:not(:last-child, [class^="pf-c"]) { section > svg { grid-area: icon; + block-size: var(--section-icon-size); + inline-size: var(--section-icon-size); } section > h2 { @@ -84,6 +90,7 @@ section > .content { right: 0; z-index: 1000; inline-size: 70%; + min-inline-size: min-content; box-shadow: -10px 10px 20px 0 var(--color-primary); } @@ -140,6 +147,7 @@ section > .content { .sidebar[data-state="hidden"] { transition: all 0.04s ease-in-out; inline-size: 0; + min-inline-size: 0; box-shadow: none; } @@ -264,31 +272,32 @@ span.notification-mark[data-variant="sidebar"] { } } -.device-list { - [role="option"] { - padding: var(--spacer-normal); +ul[data-type="agama/list"] { + li { border: 2px solid var(--color-gray-dark); + padding: var(--spacer-normal); + text-align: start; background: var(--color-gray-light); - border-radius: 5px; - transition: all 0.2s ease-in-out; - display: grid; + &:nth-child(n+2) { + border-top: 0; + } - gap: var(--spacer-small); - grid-template-columns: 1fr 2fr 2fr; - grid-template-areas: - "type-and-size drive-info drive-content" - ; + > div { + margin-block-end: var(--spacer-smaller); + } - > :first-child { - align-self: center; - text-align: center; - justify-self: start; + // Done in two rules instead of div:not(:last-child) to avoid specificity + // problems later; see the storage-devices selector + > div:last-child { + margin-block-end: 0; } } - [aria-selected] { - border: 2px solid var(--color-primary); + // FIXME: see if it's semantically correct to mark an li as aria-selected when + // not belongs to a listbox or grid list ul. + li[aria-selected] { + border-color: var(--color-primary); box-shadow: 0 2px 5px 0 var(--color-gray-dark); background: var(--color-primary); color: white; @@ -300,6 +309,113 @@ span.notification-mark[data-variant="sidebar"] { } } +// These attributes together means that UI is rendering a selector +ul[data-type="agama/list"][role="listbox"] { + li[role="option"] { + cursor: pointer; + + &:first-child { + border-radius: 5px 5px 0 0; + } + + &:last-child { + border-radius: 0 0 5px 5px; + } + + &:only-child { + border-radius: 5px; + } + + &:hover { + &:not([aria-selected]) { + background: var(--color-gray-dark); + } + } + } +} + +// Each kind of list/selector has its way of laying out their items +ul[data-of="agama/storage-devices"] { + li { + display: grid; + gap: var(--spacer-smaller); + grid-template-columns: 1fr 2fr 2fr; + grid-template-areas: "type-and-size drive-info drive-content"; + + svg { + vertical-align: inherit; + } + + > div { + margin-block-end: 0; + } + + > :first-child { + align-self: center; + text-align: center; + justify-self: start; + } + } +} + +ul[data-of="agama/space-policies"] { + // It works with the default styling +} + +ul[data-of="agama/locales"] { + li { + display: grid; + grid-template-columns: 1fr 2fr; + + > :last-child { + grid-column: 1 / -1; + font-size: var(--fs-small); + } + } +} + +ul[data-of="agama/keymaps"] { + li { + > :last-child { + font-size: var(--fs-small); + } + } +} + +ul[data-of="agama/timezones"] { + li { + display: grid; + grid-template-columns: 1fr 2fr 1fr; + + > :last-child { + grid-column: 1 / -1; + font-size: 80%; + } + + > :nth-child(3) { + color: var(--color-gray-dimmed); + text-align: end; + } + } +} + +[role="dialog"] { + .sticky-top-0 { + position: sticky; + top: calc(-1 * var(--pf-v5-c-modal-box__body--PaddingTop)); + margin-block-start: calc(-1 * var(--pf-v5-c-modal-box__body--PaddingTop)); + padding-block-start: var(--pf-v5-c-modal-box__body--PaddingTop); + background-color: var(--pf-v5-c-modal-box--BackgroundColor); + + [role="search"] { + width: 100%; + padding: var(--spacer-small); + border: 1px solid var(--color-primary); + border-radius: 5px; + } + } +} + // compact lists in popover .pf-v5-c-popover li + li { margin: 0; diff --git a/web/src/assets/styles/global.scss b/web/src/assets/styles/global.scss index 9e2723e621..c9d0625683 100644 --- a/web/src/assets/styles/global.scss +++ b/web/src/assets/styles/global.scss @@ -56,6 +56,16 @@ th { text-align: start; } +li { + svg { + vertical-align: middle; + } + + span { + margin-inline-start: var(--spacer-small); + } +} + // Style focus making use of :focus-visible *:focus { outline: none; diff --git a/web/src/assets/styles/layout.scss b/web/src/assets/styles/layout.scss index eca0fc4683..a0243fa6dc 100644 --- a/web/src/assets/styles/layout.scss +++ b/web/src/assets/styles/layout.scss @@ -33,6 +33,8 @@ svg { color: white; + block-size: var(--header-icon-size); + inline-size: var(--header-icon-size); } } @@ -40,7 +42,7 @@ grid-area: content; overflow-y: auto; // Sadly, only Firefox supports overflow-block at this moment (Jan 2023) overflow-block: auto; - padding: var(--spacer-medium); + padding: var(--spacer-medium) var(--spacer-small); } .wrapper > footer { diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss index fa620490e7..26f7377dce 100644 --- a/web/src/assets/styles/patternfly-overrides.scss +++ b/web/src/assets/styles/patternfly-overrides.scss @@ -190,3 +190,38 @@ table td > .pf-v5-c-empty-state { --pf-v5-c-toggle-group__button--m-selected--BackgroundColor: var(--color-primary); --pf-v5-c-toggle-group__button--Color: var(--color-gray-light); } + +// Reduce padding of PF/Hint because it looks like an option of current Agama +// select +.pf-v5-c-hint { + --pf-v5-c-hint--PaddingTop: var(--spacer-small); + --pf-v5-c-hint--PaddingRight: var(--spacer-small); + --pf-v5-c-hint--PaddingBottom: var(--spacer-small); + --pf-v5-c-hint--PaddingLeft: var(--spacer-small); +} + +// Do not reserve space for PF/Hint actions when there are none +.pf-v5-c-hint__actions:empty { + display: none; +} + +// Make PF/ExpandableSection looks a bit different when wrapped in a PF/Hint +.pf-v5-c-hint { + .pf-v5-c-expandable-section { + --pf-v5-c-expandable-section__toggle--Color: var(--color-primary); + } + + .pf-v5-c-expandable-section__toggle, + .pf-v5-c-expandable-section__toggle:hover { + // NOTE. would be nice to being able to use darker variant of primary color + // when hovering the link, but we aren't ready yet. We should switch to hsla + // colors or so. + --pf-v5-c-expandable-section__toggle--Color: var(--color-primary); + text-decoration: underline; + } + + .pf-v5-c-expandable-section__content { + --pf-v5-c-expandable-section__content--PaddingRight: var(--spacer-normal); + --pf-v5-c-expandable-section__content--PaddingLeft: var(--spacer-normal); + } +} diff --git a/web/src/assets/styles/utilities.scss b/web/src/assets/styles/utilities.scss index 1f84016877..39f71a946f 100644 --- a/web/src/assets/styles/utilities.scss +++ b/web/src/assets/styles/utilities.scss @@ -25,6 +25,11 @@ text-align: center; } +.title { + font-size: var(--fs-large); + font-weight: var(--fw-bold); +} + .bold { font-weight: bold; } @@ -59,6 +64,11 @@ height: 24px; } +.icon-size-28 { + width: 28px; + height: 28px; +} + .icon-size-32 { width: 32px; height: 32px; @@ -161,6 +171,6 @@ max-inline-size: calc(var(--ui-max-inline-size) + var(--spacer-large)) } -.cursor-pointer { - cursor: pointer; +.height-75 { + height: 75dvh; } diff --git a/web/src/assets/styles/variables.scss b/web/src/assets/styles/variables.scss index 670e87510c..0b01334550 100644 --- a/web/src/assets/styles/variables.scss +++ b/web/src/assets/styles/variables.scss @@ -8,7 +8,9 @@ --fw-medium: 500; --fw-bold: 700; + --fs-small: 0.7rem; --fs-base: 14px; + --fs-large: 1rem; --fs-h1: 1.5rem; --fs-h2: 1.2rem; @@ -18,6 +20,8 @@ --ui-max-inline-size: 1024px; + // FIXME: this should be changed to --spacer-xs, --spacer-s, and so + --spacer-smaller: 0.3rem; --spacer-small: 0.5rem; --spacer-normal: 1rem; --spacer-medium: 1.5rem; @@ -26,7 +30,7 @@ --stack-gutter: var(--spacer-normal); --split-gutter: var(--spacer-small); - --wrapper-padding: var(--spacer-normal); + --wrapper-padding: var(--spacer-small); --wrapper-background: white; --color-primary: #0c322c; @@ -35,6 +39,7 @@ --color-gray: #f2f2f2; --color-gray-dark: #efefef; // Fog --color-gray-darker: #999; + --color-gray-dimmed: #888; --color-link: #0c322c; --color-link-hover: #30ba78; @@ -60,7 +65,8 @@ --gradient-border-start-color: var(--color-gray); --gradient-border-end-color: transparent; - --icon-size-m: 32px; + --header-icon-size: 32px; + --section-icon-size: 28px; --header-block-size: auto; --footer-block-size: auto; diff --git a/web/src/client/index.js b/web/src/client/index.js index 34a656972e..34942691c8 100644 --- a/web/src/client/index.js +++ b/web/src/client/index.js @@ -21,7 +21,7 @@ // @ts-check -import { LanguageClient } from "./language"; +import { L10nClient } from "./l10n"; import { ManagerClient } from "./manager"; import { Monitor } from "./monitor"; import { SoftwareClient } from "./software"; @@ -37,7 +37,7 @@ const MANAGER_SERVICE = "org.opensuse.Agama.Manager1"; /** * @typedef {object} InstallerClient - * @property {LanguageClient} language - language client. + * @property {L10nClient} l10n - localization client. * @property {ManagerClient} manager - manager client. * @property {Monitor} monitor - service monitor. * @property {NetworkClient} network - network client. @@ -71,7 +71,7 @@ const MANAGER_SERVICE = "org.opensuse.Agama.Manager1"; * @return {InstallerClient} */ const createClient = (address = "unix:path=/run/agama/bus") => { - const language = new LanguageClient(address); + const l10n = new L10nClient(address); const manager = new ManagerClient(address); const monitor = new Monitor(address, MANAGER_SERVICE); const network = new NetworkClient(); @@ -122,7 +122,7 @@ const createClient = (address = "unix:path=/run/agama/bus") => { }; return { - language, + l10n, manager, monitor, network, diff --git a/web/src/client/l10n.js b/web/src/client/l10n.js new file mode 100644 index 0000000000..cf9154a6dd --- /dev/null +++ b/web/src/client/l10n.js @@ -0,0 +1,263 @@ +/* + * Copyright (c) [2022-2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +// @ts-check +import DBusClient from "./dbus"; +import { timezoneUTCOffset } from "~/utils"; + +const LOCALE_SERVICE = "org.opensuse.Agama1"; +const LOCALE_IFACE = "org.opensuse.Agama1.Locale"; +const LOCALE_PATH = "/org/opensuse/Agama1/Locale"; + +/** + * @typedef {object} Timezone + * @property {string} id - Timezone id (e.g., "Atlantic/Canary"). + * @property {Array} parts - Name of the timezone parts (e.g., ["Atlantic", "Canary"]). + * @property {number} utcOffset - UTC offset. + */ + +/** + * @typedef {object} Locale + * @property {string} id - Language id (e.g., "en_US"). + * @property {string} name - Language name (e.g., "English"). + * @property {string} territory - Territory name (e.g., "United States"). + */ + +/** + * @typedef {object} Keymap + * @property {string} id - Keyboard id (e.g., "us"). + * @property {string} name - Keyboard name (e.g., "English (US)"). + */ + +/** + * Manages localization. + */ +class L10nClient { + /** + * @param {string|undefined} address - D-Bus address; if it is undefined, it uses the system bus. + */ + constructor(address = undefined) { + this.client = new DBusClient(LOCALE_SERVICE, address); + } + + /** + * Selected locale to translate the installer UI. + * + * @return {Promise} Locale id. + */ + async getUILocale() { + const proxy = await this.client.proxy(LOCALE_IFACE); + return proxy.UILocale; + } + + /** + * Sets the locale to translate the installer UI. + * + * @param {String} id - Locale id. + * @return {Promise} + */ + async setUILocale(id) { + const proxy = await this.client.proxy(LOCALE_IFACE); + proxy.UILocale = id; + } + + /** + * All possible timezones for the target system. + * + * @return {Promise>} + */ + async timezones() { + const proxy = await this.client.proxy(LOCALE_IFACE); + const timezones = await proxy.ListTimezones(); + + return timezones.map(this.buildTimezone); + } + + /** + * Timezone selected for the target system. + * + * @return {Promise} Id of the timezone. + */ + async getTimezone() { + const proxy = await this.client.proxy(LOCALE_IFACE); + return proxy.Timezone; + } + + /** + * Sets the timezone for the target system. + * + * @param {string} id - Id of the timezone. + * @return {Promise} + */ + async setTimezone(id) { + const proxy = await this.client.proxy(LOCALE_IFACE); + proxy.Timezone = id; + } + + /** + * Available locales to install in the target system. + * + * @return {Promise>} + */ + async locales() { + const proxy = await this.client.proxy(LOCALE_IFACE); + const locales = await proxy.ListLocales(); + + return locales.map(this.buildLocale); + } + + /** + * Locales selected to install in the target system. + * + * @return {Promise>} Ids of the locales. + */ + async getLocales() { + const proxy = await this.client.proxy(LOCALE_IFACE); + return proxy.Locales; + } + + /** + * Sets the locales to install in the target system. + * + * @param {Array} ids - Ids of the locales. + * @return {Promise} + */ + async setLocales(ids) { + const proxy = await this.client.proxy(LOCALE_IFACE); + proxy.Locales = ids; + } + + /** + * Available keymaps to install in the target system. + * + * Note that name is localized to the current selected UI language: + * { id: "es", name: "Spanish (ES)" } + * + * @return {Promise>} + */ + async keymaps() { + const proxy = await this.client.proxy(LOCALE_IFACE); + const keymaps = await proxy.ListKeymaps(); + + return keymaps.map(this.buildKeymap); + } + + /** + * Keymap selected to install in the target system. + * + * @return {Promise} Id of the keymap. + */ + async getKeymap() { + const proxy = await this.client.proxy(LOCALE_IFACE); + return proxy.Keymap; + } + + /** + * Sets the keymap to install in the target system. + * + * @param {string} id - Id of the keymap. + * @return {Promise} + */ + async setKeymap(id) { + const proxy = await this.client.proxy(LOCALE_IFACE); + + proxy.Keymap = id; + } + + /** + * Register a callback to run when Timezone D-Bus property changes. + * + * @param {(timezone: string) => void} handler - Function to call when Timezone changes. + * @return {import ("./dbus").RemoveFn} Function to disable the callback. + */ + onTimezoneChange(handler) { + return this.client.onObjectChanged(LOCALE_PATH, LOCALE_IFACE, changes => { + if ("Timezone" in changes) { + const id = changes.Timezone.v; + handler(id); + } + }); + } + + /** + * Register a callback to run when Locales D-Bus property changes. + * + * @param {(language: string) => void} handler - Function to call when Locales changes. + * @return {import ("./dbus").RemoveFn} Function to disable the callback. + */ + onLocalesChange(handler) { + return this.client.onObjectChanged(LOCALE_PATH, LOCALE_IFACE, changes => { + if ("Locales" in changes) { + const selectedIds = changes.Locales.v; + handler(selectedIds); + } + }); + } + + /** + * Register a callback to run when Keymap D-Bus property changes. + * + * @param {(language: string) => void} handler - Function to call when Keymap changes. + * @return {import ("./dbus").RemoveFn} Function to disable the callback. + */ + onKeymapChange(handler) { + return this.client.onObjectChanged(LOCALE_PATH, LOCALE_IFACE, changes => { + if ("Keymap" in changes) { + const id = changes.Keymap.v; + handler(id); + } + }); + } + + /** + * @private + * + * @param {[string, Array]} dbusTimezone + * @returns {Timezone} + */ + buildTimezone([id, parts]) { + const utcOffset = timezoneUTCOffset(id); + + return ({ id, parts, utcOffset }); + } + + /** + * @private + * + * @param {[string, string, string]} dbusLocale + * @returns {Locale} + */ + buildLocale([id, name, territory]) { + return ({ id, name, territory }); + } + + /** + * @private + * + * @param {[string, string]} dbusKeymap + * @returns {Keymap} + */ + buildKeymap([id, name]) { + return ({ id, name }); + } +} + +export { L10nClient }; diff --git a/web/src/client/language.test.js b/web/src/client/l10n.test.js similarity index 56% rename from web/src/client/language.test.js rename to web/src/client/l10n.test.js index ebe05fb712..590d177143 100644 --- a/web/src/client/language.test.js +++ b/web/src/client/l10n.test.js @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2023] SUSE LLC * * All Rights Reserved. * @@ -23,34 +23,40 @@ // cspell:ignore Cestina import DBusClient from "./dbus"; -import { LanguageClient } from "./language"; +import { L10nClient } from "./l10n"; jest.mock("./dbus"); -const langProxy = { - wait: jest.fn(), - SupportedLocales: ["es_ES.UTF-8", "en_US.UTF-8"], - LabelsForLocales: jest.fn().mockResolvedValue( - [[["Spanish", "Spain"], ["Español", "España"]], [['English', 'United States'], ['English', 'United States']]] +const L10N_IFACE = "org.opensuse.Agama1.Locale"; + +const l10nProxy = { + ListLocales: jest.fn().mockResolvedValue( + [ + ["es_ES.UTF-8", "Spanish", "Spain"], + ["en_US.UTF-8", "English", "United States"] + ] ), }; -jest.mock("./dbus"); - beforeEach(() => { // @ts-ignore DBusClient.mockImplementation(() => { - return { proxy: () => langProxy }; + return { + proxy: (iface) => { + if (iface === L10N_IFACE) return l10nProxy; + } + }; }); }); -describe("#getLanguages", () => { - it("returns the list of available languages", async () => { - const client = new LanguageClient(); - const availableLanguages = await client.getLanguages(); - expect(availableLanguages).toEqual([ - { id: "es_ES.UTF-8", name: "Spanish" }, - { id: "en_US.UTF-8", name: "English" } +describe("#locales", () => { + it("returns the list of available locales", async () => { + const client = new L10nClient(); + const locales = await client.locales(); + + expect(locales).toEqual([ + { id: "es_ES.UTF-8", name: "Spanish", territory: "Spain" }, + { id: "en_US.UTF-8", name: "English", territory: "United States" } ]); }); }); diff --git a/web/src/client/language.js b/web/src/client/language.js deleted file mode 100644 index e40ab1164c..0000000000 --- a/web/src/client/language.js +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (c) [2022] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check -import DBusClient from "./dbus"; - -const LANGUAGE_SERVICE = "org.opensuse.Agama1"; -const LANGUAGE_IFACE = "org.opensuse.Agama1.Locale"; -const LANGUAGE_PATH = "/org/opensuse/Agama1/Locale"; - -/** - * @typedef {object} Language - * @property {string} id - Language ID (e.g., "en_US") - * @property {string} name - Language name (e.g., "English (US)") - */ - -/** - * Allows getting the list of available languages and selecting one for installation. - */ -class LanguageClient { - /** - * @param {string|undefined} address - D-Bus address; if it is undefined, it uses the system bus. - */ - constructor(address = undefined) { - this.client = new DBusClient(LANGUAGE_SERVICE, address); - } - - /** - * Returns the list of available languages - * - * @return {Promise>} - */ - async getLanguages() { - const proxy = await this.client.proxy(LANGUAGE_IFACE); - const locales = proxy.SupportedLocales; - const labels = await proxy.LabelsForLocales(); - return locales.map((locale, index) => { - // labels structure is [[en_lang, en_territory], [native_lang, native_territory]] - const [[en_lang,], [,]] = labels[index]; - return { id: locale, name: en_lang }; - }); - } - - /** - * Returns the languages selected for installation - * - * @return {Promise>} IDs of the selected languages - */ - async getSelectedLanguages() { - const proxy = await this.client.proxy(LANGUAGE_IFACE); - return proxy.Locales; - } - - /** - * Set the languages to install - * - * @param {string} langIDs - Identifier of languages to install - * @return {Promise} - */ - async setLanguages(langIDs) { - const proxy = await this.client.proxy(LANGUAGE_IFACE); - proxy.Locales = langIDs; - } - - /** - * Returns the current backend locale - * - * @return {Promise} the locale string - */ - async getUILanguage() { - const proxy = await this.client.proxy(LANGUAGE_IFACE); - return proxy.UILocale; - } - - /** - * Set the backend language - * - * @param {String} lang the locale string - * @return {Promise} - */ - async setUILanguage(lang) { - const proxy = await this.client.proxy(LANGUAGE_IFACE); - proxy.UILocale = lang; - } - - /** - * Register a callback to run when properties in the Language object change - * - * @param {(language: string) => void} handler - function to call when the language change - * @return {import ("./dbus").RemoveFn} function to disable the callback - */ - onLanguageChange(handler) { - return this.client.onObjectChanged(LANGUAGE_PATH, LANGUAGE_IFACE, changes => { - const selected = changes.Locales.v[0]; - handler(selected); - }); - } -} - -export { LanguageClient }; diff --git a/web/src/client/manager.js b/web/src/client/manager.js index ee847521fc..fcd223c505 100644 --- a/web/src/client/manager.js +++ b/web/src/client/manager.js @@ -86,7 +86,7 @@ class ManagerBaseClient { */ async fetchLogs() { const proxy = await this.client.proxy(MANAGER_IFACE); - const path = await proxy.CollectLogs("root"); + const path = await proxy.CollectLogs(); const file = cockpit.file(path, { binary: true }); return file.read(); } diff --git a/web/src/client/storage.js b/web/src/client/storage.js index 9f771b596c..7290b74bc3 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -228,8 +228,10 @@ class ProposalManager { * @property {string} bootDevice * @property {string} encryptionPassword * @property {boolean} lvm + * @property {string} spacePolicy * @property {string[]} systemVGDevices * @property {Volume[]} volumes + * @property {StorageDevice[]} installationDevices * * @typedef {object} Volume * @property {string} mountPath @@ -311,6 +313,8 @@ class ProposalManager { if (!proxy) return undefined; + const systemDevices = await this.system.getDevices(); + const buildResult = (proxy) => { const buildAction = dbusAction => { return { @@ -320,13 +324,33 @@ class ProposalManager { }; }; + const buildInstallationDevices = (proxy, devices) => { + const findDevice = (devices, name) => { + const device = devices.find(d => d.name === name); + + if (device === undefined) console.log("D-Bus object not found: ", name); + + return device; + }; + + const names = proxy.SystemVGDevices.filter(n => n !== proxy.BootDevice).concat([proxy.BootDevice]); + // #findDevice returns undefined if no device is found with the given name. + return names.map(dev => findDevice(devices, dev)).filter(dev => dev !== undefined); + }; + return { settings: { bootDevice: proxy.BootDevice, lvm: proxy.LVM, + spacePolicy: proxy.SpacePolicy, systemVGDevices: proxy.SystemVGDevices, encryptionPassword: proxy.EncryptionPassword, volumes: proxy.Volumes.map(this.buildVolume), + // NOTE: strictly speaking, installation devices does not belong to the settings. It + // should be a separate method instead of an attribute in the settings object. + // Nevertheless, it was added here for simplicity and to avoid passing more props in some + // react components. Please, do not use settings as a jumble. + installationDevices: buildInstallationDevices(proxy, systemDevices) }, actions: proxy.Actions.map(buildAction) }; @@ -341,7 +365,7 @@ class ProposalManager { * @param {ProposalSettings} settings * @returns {Promise} 0 on success, 1 on failure */ - async calculate({ bootDevice, encryptionPassword, lvm, systemVGDevices, volumes }) { + async calculate({ bootDevice, encryptionPassword, lvm, spacePolicy, systemVGDevices, volumes }) { const dbusVolume = (volume) => { return removeUndefinedCockpitProperties({ MountPath: { t: "s", v: volume.mountPath }, @@ -358,6 +382,7 @@ class ProposalManager { BootDevice: { t: "s", v: bootDevice }, EncryptionPassword: { t: "s", v: encryptionPassword }, LVM: { t: "b", v: lvm }, + SpacePolicy: { t: "s", v: spacePolicy }, SystemVGDevices: { t: "as", v: systemVGDevices }, Volumes: { t: "aa{sv}", v: volumes?.map(dbusVolume) } }); diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 739c5b52a2..8c3826221f 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -155,6 +155,7 @@ const contexts = { LVM: true, SystemVGDevices: ["/dev/sda", "/dev/sdb"], EncryptionPassword: "00000", + SpacePolicy: "delete", Volumes: [ { MountPath: { t: "s", v: "/" }, @@ -811,6 +812,7 @@ describe("#proposal", () => { describe("if there is a proposal", () => { beforeEach(() => { + contexts.withSystemDevices(); contexts.withProposal(); client = new StorageClient(); }); @@ -818,11 +820,12 @@ describe("#proposal", () => { it("returns the proposal settings and actions", async () => { const { settings, actions } = await client.proposal.getResult(); - expect(settings).toStrictEqual({ + expect(settings).toMatchObject({ bootDevice: "/dev/sda", lvm: true, systemVGDevices: ["/dev/sda", "/dev/sdb"], encryptionPassword: "00000", + spacePolicy: "delete", volumes: [ { mountPath: "/", @@ -861,6 +864,10 @@ describe("#proposal", () => { ] }); + expect(settings.installationDevices.map(d => d.name).sort()).toStrictEqual( + ["/dev/sda", "/dev/sdb"].sort() + ); + expect(actions).toStrictEqual([ { text: "Mount /dev/sdb1 as root", subvol: false, delete: false } ]); diff --git a/web/src/components/core/InstallButton.jsx b/web/src/components/core/InstallButton.jsx index fb5eb9af70..55e23fdd94 100644 --- a/web/src/components/core/InstallButton.jsx +++ b/web/src/components/core/InstallButton.jsx @@ -131,9 +131,12 @@ const InstallButton = ({ onClick }) => { const open = async () => { if (onClick) onClick(); const canInstall = await client.manager.canInstall(); - if (canInstall) setHasIssues(await client.issues.any()); setIsOpen(true); setError(!canInstall); + if (canInstall) { + const issues = await client.issues(); + setHasIssues(Object.values(issues).some(n => n.length > 0)); + } }; const close = () => setIsOpen(false); const install = () => client.manager.startInstallation(); diff --git a/web/src/components/core/InstallButton.test.jsx b/web/src/components/core/InstallButton.test.jsx index 08858affaf..9ee5bfc039 100644 --- a/web/src/components/core/InstallButton.test.jsx +++ b/web/src/components/core/InstallButton.test.jsx @@ -31,19 +31,18 @@ jest.mock("~/client", () => ({ createClient: jest.fn() })); -describe("when the button is clicked and there are not errors", () => { - let hasIssues = false; +let issues; +describe("when the button is clicked and there are not errors", () => { beforeEach(() => { + issues = {}; createClient.mockImplementation(() => { return { manager: { startInstallation: startInstallationFn, canInstall: () => Promise.resolve(true), }, - issues: { - any: () => Promise.resolve(hasIssues) - } + issues: jest.fn().mockResolvedValue({ ...issues }) }; }); }); @@ -74,7 +73,16 @@ describe("when the button is clicked and there are not errors", () => { describe("if there are issues", () => { beforeEach(() => { - hasIssues = true; + issues = { + product: [], + storage: [ + { description: "storage issue 1", details: "Details 1", source: "system", severity: "warn" }, + { description: "storage issue 2", details: "Details 2", source: "config", severity: "error" } + ], + software: [ + { description: "software issue 1", details: "Details 1", source: "system", severity: "warn" } + ] + }; }); it("shows a link to go to the issues page", async () => { @@ -87,10 +95,6 @@ describe("when the button is clicked and there are not errors", () => { }); describe("if there are not issues", () => { - beforeEach(() => { - hasIssues = false; - }); - it("does not show a link to go to the issues page", async () => { const { user } = installerRender(); const button = await screen.findByRole("button", { name: "Install" }); diff --git a/web/src/components/core/ListSearch.jsx b/web/src/components/core/ListSearch.jsx new file mode 100644 index 0000000000..1b53ce73e9 --- /dev/null +++ b/web/src/components/core/ListSearch.jsx @@ -0,0 +1,61 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; + +import { _ } from "~/i18n"; +import { noop, useDebounce } from "~/utils"; + +const search = (elements, term) => { + const value = term.toLowerCase(); + + const match = (element) => { + return Object.values(element) + .join('') + .toLowerCase() + .includes(value); + }; + + return elements.filter(match); +}; + +/** + * Input field for searching in a given list of elements. + * @component + * + * @param {object} props + * @param {string} [props.placeholder] + * @param {object[]} [props.elements] - List of elements in which to search. + * @param {(elements: object[]) => void} - Callback to be called with the filtered list of elements. + */ +export default function ListSearch({ + placeholder = _("Search"), + elements = [], + onChange: onChangeProp = noop +}) { + const searchHandler = useDebounce(term => onChangeProp(search(elements, term)), 500); + + const onChange = (e) => searchHandler(e.target.value); + + return ( + + ); +} diff --git a/web/src/components/core/ListSearch.test.jsx b/web/src/components/core/ListSearch.test.jsx new file mode 100644 index 0000000000..6ea805cc59 --- /dev/null +++ b/web/src/components/core/ListSearch.test.jsx @@ -0,0 +1,100 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React, { useState } from "react"; +import { screen, waitFor } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import { ListSearch } from "~/components/core"; + +const fruits = [ + { name: "Apple", color: "red", size: "medium" }, + { name: "Banana", color: "yellow", size: "medium" }, + { name: "Grape", color: "green", size: "small" }, + { name: "Pear", color: "green", size: "medium" } +]; + +const FruitList = ({ fruits }) => { + const [filteredFruits, setFilteredFruits] = useState(fruits); + + return ( + <> + +
    + {filteredFruits.map((f, i) =>
  • {f.name}
  • )} +
+ + ); +}; + +it("searches for elements matching the given term (case-insensitive)", async () => { + const { user } = plainRender(); + + const searchInput = screen.getByRole("search"); + + // Search for "medium" size fruit + await user.type(searchInput, "medium"); + await waitFor(() => ( + expect(screen.queryByRole("option", { name: /grape/ })).not.toBeInTheDocument()) + ); + screen.getByRole("option", { name: "Apple" }); + screen.getByRole("option", { name: "Banana" }); + screen.getByRole("option", { name: "Pear" }); + + // Search for "green" fruit + await user.clear(searchInput); + await user.type(searchInput, "Green"); + await waitFor(() => ( + expect(screen.queryByRole("option", { name: "Apple" })).not.toBeInTheDocument()) + ); + await waitFor(() => ( + expect(screen.queryByRole("option", { name: "Banana" })).not.toBeInTheDocument()) + ); + screen.getByRole("option", { name: "Grape" }); + screen.getByRole("option", { name: "Pear" }); + + // Search for known fruit + await user.clear(searchInput); + await user.type(searchInput, "ap"); + await waitFor(() => ( + expect(screen.queryByRole("option", { name: "Banana" })).not.toBeInTheDocument()) + ); + await waitFor(() => ( + expect(screen.queryByRole("option", { name: "Pear" })).not.toBeInTheDocument()) + ); + screen.getByRole("option", { name: "Apple" }); + screen.getByRole("option", { name: "Grape" }); + + // Search for unknown fruit + await user.clear(searchInput); + await user.type(searchInput, "tomato"); + await waitFor(() => ( + expect(screen.queryByRole("option", { name: "Apple" })).not.toBeInTheDocument()) + ); + await waitFor(() => ( + expect(screen.queryByRole("option", { name: "Banana" })).not.toBeInTheDocument()) + ); + await waitFor(() => ( + expect(screen.queryByRole("option", { name: "Grape" })).not.toBeInTheDocument()) + ); + await waitFor(() => ( + expect(screen.queryByRole("option", { name: "Pear" })).not.toBeInTheDocument()) + ); +}); diff --git a/web/src/components/core/LogsButton.jsx b/web/src/components/core/LogsButton.jsx index 1c7987ce13..a4140931fb 100644 --- a/web/src/components/core/LogsButton.jsx +++ b/web/src/components/core/LogsButton.jsx @@ -27,7 +27,7 @@ import { Alert, Button } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; import { _ } from "~/i18n"; -const FILENAME = "y2logs.tar.xz"; +const FILENAME = "agama-installation-logs.tar.bzip2"; const FILETYPE = "application/x-xz"; /** diff --git a/web/src/components/core/LogsButton.test.jsx b/web/src/components/core/LogsButton.test.jsx index 0b643d3be0..cfb9c3ccb3 100644 --- a/web/src/components/core/LogsButton.test.jsx +++ b/web/src/components/core/LogsButton.test.jsx @@ -119,7 +119,7 @@ describe("LogsButton", () => { // And test what we're looking for expect(document.createElement).toHaveBeenCalledWith('a'); expect(anchorMock).toHaveAttribute("href", "fake-blob-url"); - expect(anchorMock).toHaveAttribute("download", expect.stringMatching(/y2logs/)); + expect(anchorMock).toHaveAttribute("download", expect.stringMatching(/agama-installation-logs/)); expect(anchorMock.click).toHaveBeenCalled(); // Be polite and restore document.createElement function, diff --git a/web/src/components/core/Page.jsx b/web/src/components/core/Page.jsx index 227182e70e..a9c0a2ece2 100644 --- a/web/src/components/core/Page.jsx +++ b/web/src/components/core/Page.jsx @@ -87,7 +87,7 @@ export default function Page({ return ( <> { title && {title} } - { icon && } + { icon && } { action || -const initialState = { - languages: [], - language: "" + + } + /> + + ); }; -export default function LanguageSelector() { - const { language: client } = useInstallerClient(); - const { cancellablePromise } = useCancellablePromise(); - const [state, setState] = useState(initialState); - const { languages, language } = state; +/** + * Section for configuring timezone. + * @component + */ +const TimezoneSection = () => { + const { selectedTimezone } = useL10n(); - const updateState = ({ ...payload }) => { - setState(previousState => ({ ...previousState, ...payload })); + return ( +
+ +

{(selectedTimezone?.parts || []).join(' - ')}

+ {_("Change time zone")} + + } + else={ + <> +

{_("Time zone not selected yet")}

+ {_("Select time zone")} + + } + /> +
+ ); +}; + +/** + * Popup for selecting a locale. + * @component + * + * @param {object} props + * @param {function} props.onFinish - Callback to be called when the locale is correctly selected. + * @param {function} props.onCancel - Callback to be called when the locale selection is canceled. + */ +const LocalePopup = ({ onFinish = noop, onCancel = noop }) => { + const { l10n } = useInstallerClient(); + const { locales, selectedLocales } = useL10n(); + const { selectedProduct } = useProduct(); + const [localeId, setLocaleId] = useState(selectedLocales[0]?.id); + + const sortedLocales = locales.sort((locale1, locale2) => { + const localeText = l => [l.name, l.territory].join('').toLowerCase(); + return localeText(locale1) > localeText(locale2) ? 1 : -1; + }); + + const onSubmit = async (e) => { + e.preventDefault(); + + const [locale] = selectedLocales; + + if (localeId !== locale?.id) { + await l10n.setLocales([localeId]); + } + + onFinish(); }; - useEffect(() => { - const loadLanguages = async () => { - const languages = await cancellablePromise(client.getLanguages()); - const [language] = await cancellablePromise(client.getSelectedLanguages()); - updateState({ languages, language }); - }; - - loadLanguages().catch(console.error); - }, [client, cancellablePromise]); - - const accept = () => client.setLanguages([language]); - - const LanguageField = ({ selected }) => { - const selectorOptions = languages.map(lang => ( - - )); - - return ( - - updateState({ language: v })} - > - {selectorOptions} - - - ); + return ( + +
+ + + + + {_("Accept")} + + + +
+ ); +}; + +/** + * Button for opening the selection of locales. + * @component + * + * @param {object} props + * @param {React.ReactNode} props.children - Button children. + */ +const LocaleButton = ({ children }) => { + const [isPopupOpen, setIsPopupOpen] = useState(false); + + const openPopup = () => setIsPopupOpen(true); + const closePopup = () => setIsPopupOpen(false); + + return ( + <> + + + + } + /> + + ); +}; + +/** + * Section for configuring locales. + * @component + */ +const LocaleSection = () => { + const { selectedLocales } = useL10n(); + + const [locale] = selectedLocales; + + return ( +
+ +

{locale?.name} - {locale?.territory}

+ {_("Change language")} + + } + else={ + <> +

{_("Language not selected yet")}

+ {_("Select language")} + + } + /> +
+ ); +}; + +/** + * Popup for selecting a keymap. + * @component + * + * @param {object} props + * @param {function} props.onFinish - Callback to be called when the keymap is correctly selected. + * @param {function} props.onCancel - Callback to be called when the keymap selection is canceled. + */ +const KeymapPopup = ({ onFinish = noop, onCancel = noop }) => { + const { l10n } = useInstallerClient(); + const { keymaps, selectedKeymap } = useL10n(); + const { selectedProduct } = useProduct(); + const [keymapId, setKeymapId] = useState(selectedKeymap?.id); + + const sortedKeymaps = keymaps.sort((k1, k2) => k1.name > k2.name ? 1 : -1); + + const onSubmit = async (e) => { + e.preventDefault(); + + if (keymapId !== selectedKeymap?.id) { + await l10n.setKeymap(keymapId); + } + + onFinish(); }; return ( - // TRANSLATORS: page header - -
- + + + + + + {_("Accept")} + + + + + ); +}; + +/** + * Button for opening the selection of keymaps. + * @component + * + * @param {object} props + * @param {React.ReactNode} props.children - Button children. + */ +const KeymapButton = ({ children }) => { + const [isPopupOpen, setIsPopupOpen] = useState(false); + + const openPopup = () => setIsPopupOpen(true); + const closePopup = () => setIsPopupOpen(false); + + return ( + <> + + + + } + /> + + ); +}; + +/** + * Section for configuring keymaps. + * @component + */ +const KeymapSection = () => { + const { selectedKeymap } = useL10n(); + + return ( +
+ +

{selectedKeymap?.name}

+ {_("Change keyboard")} + + } + else={ + <> +

{_("Keyboard not selected yet")}

+ {_("Select keyboard")} + + } + /> +
+ ); +}; + +/** + * Page for configuring localization. + * @component + */ +export default function L10nPage() { + return ( + + + + ); } diff --git a/web/src/components/l10n/L10nPage.test.jsx b/web/src/components/l10n/L10nPage.test.jsx index 3f8fd0b724..7423b0b284 100644 --- a/web/src/components/l10n/L10nPage.test.jsx +++ b/web/src/components/l10n/L10nPage.test.jsx @@ -20,55 +20,384 @@ */ import React from "react"; -import { screen } from "@testing-library/react"; -import { installerRender, mockNavigateFn } from "~/test-utils"; +import { screen, waitFor, within } from "@testing-library/react"; + +import { installerRender } from "~/test-utils"; import { L10nPage } from "~/components/l10n"; import { createClient } from "~/client"; -const setLanguagesFn = jest.fn(); -const languages = [ - { id: "en_US", name: "English" }, - { id: "de_DE", name: "German" } +const locales = [ + { id: "de_DE.UTF8", name: "German", territory: "Germany" }, + { id: "en_US.UTF8", name: "English", territory: "United States" }, + { id: "es_ES.UTF8", name: "Spanish", territory: "Spain" } +]; + +const keymaps = [ + { id: "de", name: "German" }, + { id: "us", name: "English" }, + { id: "es", name: "Spanish" } +]; + +const timezones = [ + { id: "asia/bangkok", parts: ["Asia", "Bangkok"] }, + { id: "atlantic/canary", parts: ["Atlantic", "Canary"] }, + { id: "america/new_york", parts: ["Americas", "New York"] } ]; +let mockL10nClient; +let mockSelectedLocales; +let mockSelectedKeymap; +let mockSelectedTimezone; + jest.mock("~/client"); +jest.mock("~/context/l10n", () => ({ + ...jest.requireActual("~/context/l10n"), + useL10n: () => ({ + locales, + selectedLocales: mockSelectedLocales, + keymaps, + selectedKeymap: mockSelectedKeymap, + timezones, + selectedTimezone: mockSelectedTimezone + }) +})); + +jest.mock("~/context/product", () => ({ + ...jest.requireActual("~/context/product"), + useProduct: () => ({ + selectedProduct : { name: "Test" } + }) +})); + +createClient.mockImplementation(() => ( + { + l10n: mockL10nClient + } +)); + beforeEach(() => { - // if defined outside, the mock is cleared automatically - createClient.mockImplementation(() => { - return { - language: { - getLanguages: () => Promise.resolve(languages), - getSelectedLanguages: () => Promise.resolve(["en_US"]), - setLanguages: setLanguagesFn, - } - }; + mockL10nClient = { + setLocales: jest.fn().mockResolvedValue(), + setKeymap: jest.fn().mockResolvedValue(), + setTimezone: jest.fn().mockResolvedValue() + }; + + mockSelectedLocales = []; + mockSelectedKeymap = undefined; + mockSelectedTimezone = undefined; +}); + +it("renders a section for configuring the language", () => { + installerRender(); + screen.getByText("Language"); +}); + +describe("if there is no selected language", () => { + beforeEach(() => { + mockSelectedLocales = []; + }); + + it("renders a button for selecting a language", () => { + installerRender(); + screen.getByText("Language not selected yet"); + screen.getByRole("button", { name: "Select language" }); + }); +}); + +describe("if there is a selected language", () => { + beforeEach(() => { + mockSelectedLocales = [{ id: "es_ES.UTF8", name: "Spanish", territory: "Spain" }]; + }); + + it("renders a button for changing the language", () => { + installerRender(); + screen.getByText("Spanish - Spain"); + screen.getByRole("button", { name: "Change language" }); + }); +}); + +describe("when the button for changing the language is clicked", () => { + beforeEach(() => { + mockSelectedLocales = [{ id: "es_ES.UTF8", name: "Spanish", territory: "Spain" }]; + }); + + it("opens a popup for selecting the language", async () => { + const { user } = installerRender(); + + const button = screen.getByRole("button", { name: "Change language" }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + within(popup).getByText("Select language"); + within(popup).getByRole("option", { name: /German/ }); + within(popup).getByRole("option", { name: /English/ }); + within(popup).getByRole("option", { name: /Spanish/, selected: true }); + }); + + it("allows filtering languages", async () => { + const { user } = installerRender(); + + const button = screen.getByRole("button", { name: "Change language" }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + const searchInput = within(popup).getByRole("search"); + + await user.type(searchInput, "ish"); + + await waitFor(() => ( + expect(within(popup).queryByRole("option", { name: /German/ })).not.toBeInTheDocument()) + ); + within(popup).getByRole("option", { name: /English/ }); + within(popup).getByRole("option", { name: /Spanish/ }); + }); + + describe("if the popup is canceled", () => { + it("closes the popup without selecting a new language", async () => { + const { user } = installerRender(); + + const button = screen.getByRole("button", { name: "Change language" }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + const option = within(popup).getByRole("option", { name: /English/ }); + + await user.click(option); + const cancel = within(popup).getByRole("button", { name: "Cancel" }); + await user.click(cancel); + + expect(mockL10nClient.setLocales).not.toHaveBeenCalled(); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + + describe("if the popup is accepted", () => { + it("closes the popup selecting the new language", async () => { + const { user } = installerRender(); + + const button = screen.getByRole("button", { name: "Change language" }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + const option = within(popup).getByRole("option", { name: /English/ }); + + await user.click(option); + const accept = within(popup).getByRole("button", { name: "Accept" }); + await user.click(accept); + + expect(mockL10nClient.setLocales).toHaveBeenCalledWith(["en_US.UTF8"]); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); }); }); -it("displays the language selector", async () => { +it("renders a section for configuring the keyboard", () => { installerRender(); + screen.getByText("Keyboard"); +}); + +describe("if there is no selected keyboard", () => { + beforeEach(() => { + mockSelectedKeymap = undefined; + }); + + it("renders a button for selecting a keyboard", () => { + installerRender(); + screen.getByText("Keyboard not selected yet"); + screen.getByRole("button", { name: "Select keyboard" }); + }); +}); - await screen.findByLabelText("Language"); +describe("if there is a selected keyboard", () => { + beforeEach(() => { + mockSelectedKeymap = { id: "es", name: "Spanish" }; + }); + + it("renders a button for changing the keyboard", () => { + installerRender(); + screen.getByText("Spanish"); + screen.getByRole("button", { name: "Change keyboard" }); + }); }); -describe("when the user accept changes", () => { - it("changes the selected language", async () => { +describe("when the button for changing the keyboard is clicked", () => { + beforeEach(() => { + mockSelectedKeymap = { id: "es", name: "Spanish" }; + }); + + it("opens a popup for selecting the keyboard", async () => { const { user } = installerRender(); - const germanOption = await screen.findByRole("option", { name: "German" }); - const acceptButton = screen.getByRole("button", { name: "Accept" }); - await user.selectOptions(screen.getByLabelText("Language"), germanOption); - await user.click(acceptButton); + const button = screen.getByRole("button", { name: "Change keyboard" }); + await user.click(button); - expect(setLanguagesFn).toHaveBeenCalledWith(["de_DE"]); + const popup = await screen.findByRole("dialog"); + within(popup).getByText("Select keyboard"); + within(popup).getByRole("option", { name: /German/ }); + within(popup).getByRole("option", { name: /English/ }); + within(popup).getByRole("option", { name: /Spanish/, selected: true }); }); - it("navigates to the root path", async () => { + it("allows filtering keyboards", async () => { const { user } = installerRender(); - const acceptButton = screen.getByRole("button", { name: "Accept" }); - await user.click(acceptButton); - expect(mockNavigateFn).toHaveBeenCalledWith("/"); + const button = screen.getByRole("button", { name: "Change keyboard" }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + const searchInput = within(popup).getByRole("search"); + + await user.type(searchInput, "ish"); + + await waitFor(() => ( + expect(within(popup).queryByRole("option", { name: /German/ })).not.toBeInTheDocument()) + ); + within(popup).getByRole("option", { name: /English/ }); + within(popup).getByRole("option", { name: /Spanish/ }); + }); + + describe("if the popup is canceled", () => { + it("closes the popup without selecting a new keyboard", async () => { + const { user } = installerRender(); + + const button = screen.getByRole("button", { name: "Change keyboard" }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + const option = within(popup).getByRole("option", { name: /English/ }); + + await user.click(option); + const cancel = within(popup).getByRole("button", { name: "Cancel" }); + await user.click(cancel); + + expect(mockL10nClient.setKeymap).not.toHaveBeenCalled(); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + + describe("if the popup is accepted", () => { + it("closes the popup selecting the new keyboard", async () => { + const { user } = installerRender(); + + const button = screen.getByRole("button", { name: "Change keyboard" }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + const option = within(popup).getByRole("option", { name: /English/ }); + + await user.click(option); + const accept = within(popup).getByRole("button", { name: "Accept" }); + await user.click(accept); + + expect(mockL10nClient.setKeymap).toHaveBeenCalledWith("us"); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); +}); + +it("renders a section for configuring the time zone", () => { + installerRender(); + screen.getByText("Time zone"); +}); + +describe("if there is no selected time zone", () => { + beforeEach(() => { + mockSelectedTimezone = undefined; + }); + + it("renders a button for selecting a time zone", () => { + installerRender(); + screen.getByText("Time zone not selected yet"); + screen.getByRole("button", { name: "Select time zone" }); + }); +}); + +describe("if there is a selected time zone", () => { + beforeEach(() => { + mockSelectedTimezone = { id: "atlantic/canary", parts: ["Atlantic", "Canary"] }; + }); + + it("renders a button for changing the time zone", () => { + installerRender(); + screen.getByText("Atlantic - Canary"); + screen.getByRole("button", { name: "Change time zone" }); + }); +}); + +describe("when the button for changing the time zone is clicked", () => { + beforeEach(() => { + mockSelectedTimezone = { id: "atlantic/canary", parts: ["Atlantic", "Canary"] }; + }); + + it("opens a popup for selecting the time zone", async () => { + const { user } = installerRender(); + + const button = screen.getByRole("button", { name: "Change time zone" }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + within(popup).getByText("Select time zone"); + within(popup).getByRole("option", { name: /Bangkok/ }); + within(popup).getByRole("option", { name: /Canary/, selected: true }); + within(popup).getByRole("option", { name: /New York/ }); + }); + + it("allows filtering time zones", async () => { + const { user } = installerRender(); + + const button = screen.getByRole("button", { name: "Change time zone" }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + const searchInput = within(popup).getByRole("search"); + + await user.type(searchInput, "new"); + + await waitFor(() => ( + expect(within(popup).queryByRole("option", { name: /Bangkok/ })).not.toBeInTheDocument()) + ); + await waitFor(() => ( + expect(within(popup).queryByRole("option", { name: /Canary/ })).not.toBeInTheDocument()) + ); + within(popup).getByRole("option", { name: /New York/ }); + }); + + describe("if the popup is canceled", () => { + it("closes the popup without selecting a new time zone", async () => { + const { user } = installerRender(); + + const button = screen.getByRole("button", { name: "Change time zone" }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + const option = within(popup).getByRole("option", { name: /New York/ }); + + await user.click(option); + const cancel = within(popup).getByRole("button", { name: "Cancel" }); + await user.click(cancel); + + expect(mockL10nClient.setTimezone).not.toHaveBeenCalled(); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + + describe("if the popup is accepted", () => { + it("closes the popup selecting the new time zone", async () => { + const { user } = installerRender(); + + const button = screen.getByRole("button", { name: "Change time zone" }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + const option = within(popup).getByRole("option", { name: /Bangkok/ }); + + await user.click(option); + const accept = within(popup).getByRole("button", { name: "Accept" }); + await user.click(accept); + + expect(mockL10nClient.setTimezone).toHaveBeenCalledWith("asia/bangkok"); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); }); }); diff --git a/web/src/components/l10n/LanguageSwitcher.jsx b/web/src/components/l10n/LanguageSwitcher.jsx index e2fddf2d2b..76d1562d01 100644 --- a/web/src/components/l10n/LanguageSwitcher.jsx +++ b/web/src/components/l10n/LanguageSwitcher.jsx @@ -23,11 +23,11 @@ import React, { useCallback, useState } from "react"; import { Icon } from "../layout"; import { FormSelect, FormSelectOption } from "@patternfly/react-core"; import { _ } from "~/i18n"; -import { useL10n } from "~/context/l10n"; +import { useInstallerL10n } from "~/context/installerL10n"; import cockpit from "~/lib/cockpit"; export default function LanguageSwitcher() { - const { language, changeLanguage } = useL10n(); + const { language, changeLanguage } = useInstallerL10n(); const [selected, setSelected] = useState(null); const languages = cockpit.manifests.agama?.locales || []; diff --git a/web/src/components/l10n/LanguageSwitcher.test.jsx b/web/src/components/l10n/LanguageSwitcher.test.jsx index e844dbb9dc..f3ba15fcdb 100644 --- a/web/src/components/l10n/LanguageSwitcher.test.jsx +++ b/web/src/components/l10n/LanguageSwitcher.test.jsx @@ -40,9 +40,9 @@ jest.mock("~/lib/cockpit", () => ({ } })); -jest.mock("~/context/l10n", () => ({ - ...jest.requireActual("~/context/l10n"), - useL10n: () => ({ +jest.mock("~/context/installerL10n", () => ({ + ...jest.requireActual("~/context/installerL10n"), + useInstallerL10n: () => ({ language: mockLanguage, changeLanguage: mockChangeLanguageFn }) diff --git a/web/src/components/l10n/LocaleSelector.jsx b/web/src/components/l10n/LocaleSelector.jsx new file mode 100644 index 0000000000..12bae4867a --- /dev/null +++ b/web/src/components/l10n/LocaleSelector.jsx @@ -0,0 +1,102 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React, { useState } from "react"; + +import { _ } from "~/i18n"; +import { ListSearch } from "~/components/core"; +import { noop } from "~/utils"; + +/** + * @typedef {import ("~/clients/l10n").Locale} Locale + */ + +const ListBox = ({ children, ...props }) => { + return ( +
    {children}
+ ); +}; + +const ListBoxItem = ({ isSelected, children, onClick, ...props }) => { + if (isSelected) props['aria-selected'] = true; + + return ( +
  • + {children} +
  • + ); +}; + +/** + * Content for a locale item. + * @component + * + * @param {Object} props + * @param {Locale} props.locale + */ +const LocaleItem = ({ locale }) => { + return ( + <> +
    {locale.name}
    +
    {locale.territory}
    +
    {locale.id}
    + + ); +}; + +/** + * Component for selecting a locale. + * @component + * + * @param {Object} props + * @param {string} [props.value] - Id of the currently selected locale. + * @param {Locale[]} [props.locales] - Locales for selection. + * @param {(id: string) => void} [props.onChange] - Callback to be called when the selected locale + * changes. + */ +export default function LocaleSelector({ value, locales = [], onChange = noop }) { + const [filteredLocales, setFilteredLocales] = useState(locales); + + const searchHelp = _("Filter by language, territory or locale code"); + + return ( + <> +
    + +
    + + { filteredLocales.map((locale, index) => ( + onChange(locale.id)} + isSelected={locale.id === value} + > + + + ))} + + + ); +} diff --git a/web/src/components/l10n/TimezoneSelector.jsx b/web/src/components/l10n/TimezoneSelector.jsx new file mode 100644 index 0000000000..55ddc9b629 --- /dev/null +++ b/web/src/components/l10n/TimezoneSelector.jsx @@ -0,0 +1,122 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React, { useState } from "react"; + +import { _ } from "~/i18n"; +import { ListSearch } from "~/components/core"; +import { noop, timezoneTime } from "~/utils"; + +/** + * @typedef {import ("~/clients/l10n").Timezone} Timezone + */ + +const ListBox = ({ children, ...props }) => { + return ( +
      {children}
    + ); +}; + +const ListBoxItem = ({ isSelected, children, onClick, ...props }) => { + if (isSelected) props['aria-selected'] = true; + + return ( +
  • + {children} +
  • + ); +}; + +const timezoneDetails = (timezone) => { + const offset = timezone.utcOffset; + + if (offset === undefined) return timezone.id; + + let utc = "UTC"; + if (offset > 0) utc += `+${offset}`; + if (offset < 0) utc += `${offset}`; + + return `${timezone.id} ${utc}`; +}; + +/** + * Content for a timezone item + * @component + * + * @param {Object} props + * @param {Timezone} props.timezone + * @param {Date} props.date - Date to show a time. + */ +const TimezoneItem = ({ timezone, date }) => { + const [part1, ...restParts] = timezone.parts; + const time = timezoneTime(timezone.id, { date }) || ""; + + return ( + <> +
    {part1}
    +
    {restParts.join('-')}
    +
    {time || ""}
    +
    {timezone.details}
    + + ); +}; + +/** + * Component for selecting a timezone. + * @component + * + * @param {Object} props + * @param {string} [props.value] - Id of the currently selected timezone. + * @param {Locale[]} [props.timezones] - Timezones for selection. + * @param {(id: string) => void} [props.onChange] - Callback to be called when the selected timezone + * changes. + */ +export default function TimezoneSelector({ value, timezones = [], onChange = noop }) { + const displayTimezones = timezones.map(t => ({ ...t, details: timezoneDetails(t) })); + const [filteredTimezones, setFilteredTimezones] = useState(displayTimezones); + const date = new Date(); + + // TRANSLATORS: placeholder text for search input in the timezone selector. + const helpSearch = _("Filter by territory, time zone code or UTC offset"); + + return ( + <> +
    + +
    + + { filteredTimezones.map((timezone, index) => ( + onChange(timezone.id)} + isSelected={timezone.id === value} + > + + + ))} + + + ); +} diff --git a/web/src/components/l10n/index.js b/web/src/components/l10n/index.js index dfb127b56e..a809d1485f 100644 --- a/web/src/components/l10n/index.js +++ b/web/src/components/l10n/index.js @@ -19,5 +19,8 @@ * find current contact information at www.suse.com. */ +export { default as KeymapSelector } from "./KeymapSelector"; export { default as L10nPage } from "./L10nPage"; export { default as LanguageSwitcher } from "./LanguageSwitcher"; +export { default as LocaleSelector } from "./LocaleSelector"; +export { default as TimezoneSelector } from "./TimezoneSelector"; diff --git a/web/src/components/layout/Icon.jsx b/web/src/components/layout/Icon.jsx index 42c32748af..de3091efb4 100644 --- a/web/src/components/layout/Icon.jsx +++ b/web/src/components/layout/Icon.jsx @@ -38,11 +38,13 @@ import Error from "@icons/error.svg?component"; import ExpandMore from "@icons/expand_more.svg?component"; import Folder from "@icons/folder.svg?component"; import FolderOff from "@icons/folder_off.svg?component"; +import Globe from "@icons/globe.svg?component"; import HardDrive from "@icons/hard_drive.svg?component"; import Help from "@icons/help.svg?component"; import HomeStorage from "@icons/home_storage.svg?component"; import Info from "@icons/info.svg?component"; import Inventory from "@icons/inventory_2.svg?component"; +import Keyboard from "@icons/keyboard.svg?component"; import Lan from "@icons/lan.svg?component"; import ListAlt from "@icons/list_alt.svg?component"; import Lock from "@icons/lock.svg?component"; @@ -53,11 +55,13 @@ import MoreVert from "@icons/more_vert.svg?component"; import Person from "@icons/person.svg?component"; import Problem from "@icons/problem.svg?component"; import Refresh from "@icons/refresh.svg?component"; +import Schedule from "@icons/schedule.svg?component"; import SettingsApplications from "@icons/settings_applications.svg?component"; import SettingsEthernet from "@icons/settings_ethernet.svg?component"; import SettingsFill from "@icons/settings-fill.svg?component"; import SignalCellularAlt from "@icons/signal_cellular_alt.svg?component"; import Storage from "@icons/storage.svg?component"; +import Sync from "@icons/sync.svg?component"; import TaskAlt from "@icons/task_alt.svg?component"; import Terminal from "@icons/terminal.svg?component"; import Translate from "@icons/translate.svg?component"; @@ -91,11 +95,13 @@ const icons = { expand_more: ExpandMore, folder: Folder, folder_off: FolderOff, + globe: Globe, hard_drive: HardDrive, help: Help, home_storage: HomeStorage, info: Info, inventory_2: Inventory, + keyboard: Keyboard, lan: Lan, loading: Loading, list_alt: ListAlt, @@ -107,11 +113,13 @@ const icons = { person: Person, problem: Problem, refresh: Refresh, + schedule: Schedule, settings: SettingsFill, settings_applications: SettingsApplications, settings_ethernet: SettingsEthernet, signal_cellular_alt: SignalCellularAlt, storage: Storage, + sync: Sync, task_alt: TaskAlt, terminal: Terminal, translate: Translate, @@ -148,7 +156,7 @@ const icons = { * @param {object} [props.otherProps] Other props sent to SVG icon. * */ -export default function Icon({ name, className = "", size = 32, ...otherProps }) { +export default function Icon({ name, className = "", size = 28, ...otherProps }) { if (!name) { console.error(`Icon called without name. '${name}' given instead. Rendering nothing.`); return null; diff --git a/web/src/components/overview/L10nSection.jsx b/web/src/components/overview/L10nSection.jsx index 1429b7f1a8..de6d1b5eb5 100644 --- a/web/src/components/overview/L10nSection.jsx +++ b/web/src/components/overview/L10nSection.jsx @@ -19,73 +19,46 @@ * find current contact information at www.suse.com. */ -import React, { useEffect, useState } from "react"; +import React from "react"; import { Text } from "@patternfly/react-core"; -import { Em, Section, SectionSkeleton } from "~/components/core"; -import { useCancellablePromise } from "~/utils"; -import { useInstallerClient } from "~/context/installer"; +import { Em, If, Section, SectionSkeleton } from "~/components/core"; +import { useL10n } from "~/context/l10n"; import { _ } from "~/i18n"; -const initialState = { - busy: true, - language: undefined, - errors: [] -}; - -export default function L10nSection({ showErrors }) { - const [state, setState] = useState(initialState); - const { language: client } = useInstallerClient(); - const { cancellablePromise } = useCancellablePromise(); - - const updateState = ({ ...payload }) => { - setState(previousState => ({ ...previousState, ...payload })); - }; - - useEffect(() => { - const loadLanguages = async () => { - const languages = await cancellablePromise(client.getLanguages()); - const [language] = await cancellablePromise(client.getSelectedLanguages()); - updateState({ languages, language, busy: false }); - }; - - // TODO: use these errors? - loadLanguages().catch(console.error); +const Content = ({ locales }) => { + // Only considering the first locale. + const locale = locales[0]; - return client.onLanguageChange(language => { - updateState({ language }); - }); - }, [client, cancellablePromise]); + // TRANSLATORS: %s will be replaced by a language name and territory, example: + // "English (United States)". + const [msg1, msg2] = _("The system will use %s as its default language.").split("%s"); - const errors = showErrors ? state.errors : []; - - const SectionContent = () => { - const { busy, languages, language } = state; - - if (busy) return ; + return ( + + {msg1}{`${locale.name} (${locale.territory})`}{msg2} + + ); +}; - const selected = languages.find(lang => lang.id === language); +export default function L10nSection() { + const { selectedLocales } = useL10n(); - // TRANSLATORS: %s will be replaced by a language name and code, - // example: "English (en_US.UTF-8)" - const [msg1, msg2] = _("The system will use %s as its default language.").split("%s"); - return ( - - {msg1}{`${selected.name} (${selected.id})`}{msg2} - - ); - }; + const isLoading = selectedLocales.length === 0; return (
    - + } + else={} + />
    ); } diff --git a/web/src/components/overview/L10nSection.test.jsx b/web/src/components/overview/L10nSection.test.jsx index ec282d6fc2..e5da1c1088 100644 --- a/web/src/components/overview/L10nSection.test.jsx +++ b/web/src/components/overview/L10nSection.test.jsx @@ -27,49 +27,50 @@ import { createClient } from "~/client"; jest.mock("~/client"); -const languages = [ - { id: "en_US", name: "English" }, - { id: "de_DE", name: "German" } +const locales = [ + { id: "en_US", name: "English", territory: "United States" }, + { id: "de_DE", name: "German", territory: "Germany" } ]; -let onLanguageChangeFn = jest.fn(); - -const languageMock = { - getLanguages: () => Promise.resolve(languages), - getSelectedLanguages: () => Promise.resolve(["en_US"]), +const l10nClientMock = { + locales: jest.fn().mockResolvedValue(locales), + getLocales: jest.fn().mockResolvedValue(["en_US"]), + getUILocale: jest.fn().mockResolvedValue("en_US"), + keymaps: jest.fn().mockResolvedValue([]), + getKeymap: jest.fn().mockResolvedValue(undefined), + timezones: jest.fn().mockResolvedValue([]), + getTimezone: jest.fn().mockResolvedValue(undefined), + onLocalesChange: jest.fn(), + onKeymapChange: jest.fn(), + onTimezoneChange: jest.fn() }; beforeEach(() => { // if defined outside, the mock is cleared automatically createClient.mockImplementation(() => { return { - language: { - ...languageMock, - onLanguageChange: onLanguageChangeFn - } + l10n: l10nClientMock }; }); }); -it("displays the selected language", async () => { - installerRender(); +it("displays the selected locale", async () => { + installerRender(, { withL10n: true }); - await screen.findByText("English (en_US)"); + await screen.findByText("English (United States)"); }); -describe("when the Language Selection changes", () => { +describe("when the selected locales change", () => { it("updates the proposal", async () => { const [mockFunction, callbacks] = createCallbackMock(); - onLanguageChangeFn = mockFunction; + l10nClientMock.onLocalesChange = mockFunction; - installerRender(); - await screen.findByText("English (en_US)"); + installerRender(, { withL10n: true }); + await screen.findByText("English (United States)"); const [cb] = callbacks; - act(() => { - cb("de_DE"); - }); + act(() => cb(["de_DE"])); - await screen.findByText("German (de_DE)"); + await screen.findByText("German (Germany)"); }); }); diff --git a/web/src/components/overview/SoftwareSection.jsx b/web/src/components/overview/SoftwareSection.jsx index fbcd81cee8..6046a9ab2f 100644 --- a/web/src/components/overview/SoftwareSection.jsx +++ b/web/src/components/overview/SoftwareSection.jsx @@ -34,6 +34,7 @@ const initialState = { errors: [], errorsRead: false, size: "", + patterns: {}, progress: { message: _("Reading software repositories"), current: 0, total: 0, finished: false } }; @@ -51,8 +52,8 @@ const reducer = (state, action) => { case "UPDATE_PROPOSAL": { if (state.busy) return state; - const { errors, size } = action.payload; - return { ...state, errors, size, errorsRead: true }; + const { errors, size, patterns } = action.payload; + return { ...state, errors, size, patterns, errorsRead: true }; } default: { @@ -82,8 +83,9 @@ export default function SoftwareSection({ showErrors }) { const updateProposal = async () => { const errors = await cancellablePromise(client.getIssues()); const size = await cancellablePromise(client.getUsedSpace()); + const patterns = await cancellablePromise(client.patterns(true)); - dispatch({ type: "UPDATE_PROPOSAL", payload: { errors, size } }); + dispatch({ type: "UPDATE_PROPOSAL", payload: { errors, size, patterns } }); }; updateProposal(); @@ -142,7 +144,8 @@ export default function SoftwareSection({ showErrors }) { icon="apps" loading={state.busy} errors={errors.map(toValidationError)} - path="/software" + // do not display the pattern selector when there are no patterns to display + path={Object.keys(state.patterns).length > 0 ? "/software" : null} > diff --git a/web/src/components/overview/SoftwareSection.test.jsx b/web/src/components/overview/SoftwareSection.test.jsx index d21df0e84c..d0fdf55711 100644 --- a/web/src/components/overview/SoftwareSection.test.jsx +++ b/web/src/components/overview/SoftwareSection.test.jsx @@ -29,9 +29,20 @@ import { SoftwareSection } from "~/components/overview"; jest.mock("~/client"); +const kdePattern = { + kde: [ + "Graphical Environments", + "Packages providing the Plasma desktop environment and applications from KDE.", + "./pattern-kde", + "KDE Applications and Plasma 5 Desktop", + "1110" + ] +}; + let getStatusFn = jest.fn().mockResolvedValue(IDLE); let getProgressFn = jest.fn().mockResolvedValue({}); let getIssuesFn = jest.fn().mockResolvedValue([]); +let patternsFn = jest.fn().mockResolvedValue(kdePattern); beforeEach(() => { createClient.mockImplementation(() => { @@ -42,6 +53,7 @@ beforeEach(() => { getIssues: getIssuesFn, onStatusChange: noop, onProgressChange: noop, + patterns: patternsFn, getUsedSpace: jest.fn().mockResolvedValue("500 MB") }, }; @@ -51,6 +63,7 @@ beforeEach(() => { describe("when the proposal is calculated", () => { beforeEach(() => { getStatusFn = jest.fn().mockResolvedValue(IDLE); + patternsFn = jest.fn().mockResolvedValue(kdePattern); }); it("renders the required space", async () => { @@ -59,6 +72,28 @@ describe("when the proposal is calculated", () => { await screen.findByText("500 MB"); }); + describe("patterns are available", () => { + it("the header is a link", async () => { + const { container } = installerRender(); + // wait until the component is fully rendered + await screen.findByText("Installation will take"); + expect(container.querySelector("h2 a[href='/software']")).not.toBeNull(); + }); + }); + + describe("no patterns are available", () => { + beforeEach(() => { + patternsFn = jest.fn().mockResolvedValue({}); + }); + + it("the header is a plain text", async () => { + const { container } = installerRender(); + // wait until the component is fully rendered + await screen.findByText("Installation will take"); + expect(container.querySelector("h2 a")).toBeNull(); + }); + }); + describe("and there are errors", () => { beforeEach(() => { getIssuesFn = jest.fn().mockResolvedValue([{ description: "Could not install..." }]); diff --git a/web/src/components/overview/StorageSection.jsx b/web/src/components/overview/StorageSection.jsx index e79d5275d0..adbe1978d8 100644 --- a/web/src/components/overview/StorageSection.jsx +++ b/web/src/components/overview/StorageSection.jsx @@ -29,6 +29,12 @@ import { deviceLabel } from "~/components/storage/utils"; import { Em, ProgressText, Section } from "~/components/core"; import { _ } from "~/i18n"; +/** + * Text explaining the storage proposal + * + * FIXME: this needs to be basically rewritten. See + * https://github.com/openSUSE/agama/discussions/778#discussioncomment-7715244 + */ const ProposalSummary = ({ proposal }) => { const { availableDevices = [], result = {} } = proposal; @@ -36,12 +42,32 @@ const ProposalSummary = ({ proposal }) => { if (!bootDevice) return {_("No device selected yet")}; const device = availableDevices.find(d => d.name === bootDevice); - const label = device ? deviceLabel(device) : bootDevice; - // TRANSLATORS: %s will be replaced by the device name and its size, - // example: "/dev/sda, 20 GiB" - const [msg1, msg2] = _("Install using device %s and deleting all its content").split("%s"); + const fullMsg = (policy) => { + switch (policy) { + case "resize": + // TRANSLATORS: %s will be replaced by the device name and its size, + // example: "/dev/sda, 20 GiB" + return _("Install using device %s shrinking existing partitions as needed"); + case "keep": + // TRANSLATORS: %s will be replaced by the device name and its size, + // example: "/dev/sda, 20 GiB" + return _("Install using device %s without modifying existing partitions"); + case "delete": + // TRANSLATORS: %s will be replaced by the device name and its size, + // example: "/dev/sda, 20 GiB" + return _("Install using device %s and deleting all its content"); + } + + console.log(`Unknown space policy: ${policy}`); + // TRANSLATORS: %s will be replaced by the device name and its size, + // example: "/dev/sda, 20 GiB" + return _("Install using device %s"); + }; + + const [msg1, msg2] = fullMsg(result.settings?.spacePolicy).split("%s"); + return ( {msg1}{label}{msg2} diff --git a/web/src/components/overview/StorageSection.test.jsx b/web/src/components/overview/StorageSection.test.jsx index 25fbfb599a..9c29e02f34 100644 --- a/web/src/components/overview/StorageSection.test.jsx +++ b/web/src/components/overview/StorageSection.test.jsx @@ -38,7 +38,8 @@ const availableDevices = [ const proposalResult = { settings: { bootDevice: "/dev/sda", - lvm: false + lvm: false, + spacePolicy: "delete" }, actions: [] }; @@ -89,6 +90,32 @@ describe("when there is a proposal", () => { await screen.findByText(/and deleting all its content/); }); + describe("and the space policy is set to 'resize'", () => { + beforeEach(() => { + const result = { settings: { spacePolicy: "resize", bootDevice: "/dev/sda" } }; + storage.proposal.getResult = jest.fn().mockResolvedValue(result); + }); + + it("indicates that partitions may be shrunk", async () => { + installerRender(); + + await screen.findByText(/shrinking existing partitions as needed/); + }); + }); + + describe("and the space policy is set to 'keep'", () => { + beforeEach(() => { + const result = { settings: { spacePolicy: "keep", bootDevice: "/dev/sda" } }; + storage.proposal.getResult = jest.fn().mockResolvedValue(result); + }); + + it("indicates that partitions will be kept", async () => { + installerRender(); + + await screen.findByText(/without modifying existing partitions/); + }); + }); + describe("and there is no boot device", () => { beforeEach(() => { const result = { settings: { bootDevice: "" } }; diff --git a/web/src/components/product/ProductPage.jsx b/web/src/components/product/ProductPage.jsx index 77c671f855..fb10aa993b 100644 --- a/web/src/components/product/ProductPage.jsx +++ b/web/src/components/product/ProductPage.jsx @@ -236,7 +236,7 @@ const RegisteredWarningPopup = ({ isOpen = false, onAccept = noop }) => {

    - {_("Accept")} + {_("Close")} diff --git a/web/src/components/product/ProductPage.test.jsx b/web/src/components/product/ProductPage.test.jsx index 11a89fba2c..618d3e8994 100644 --- a/web/src/components/product/ProductPage.test.jsx +++ b/web/src/components/product/ProductPage.test.jsx @@ -247,7 +247,7 @@ describe("when the button for changing the product is clicked", () => { const popup = await screen.findByRole("dialog"); within(popup).getByText(/must be deregistered/); - const accept = within(popup).getByRole("button", { name: "Accept" }); + const accept = within(popup).getByRole("button", { name: "Close" }); await user.click(accept); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); diff --git a/web/src/components/storage/ProposalSettingsSection.jsx b/web/src/components/storage/ProposalSettingsSection.jsx index 3142d9c515..0321ab042c 100644 --- a/web/src/components/storage/ProposalSettingsSection.jsx +++ b/web/src/components/storage/ProposalSettingsSection.jsx @@ -29,7 +29,11 @@ import { import { _ } from "~/i18n"; import { If, PasswordAndConfirmationInput, Section, Popup } from "~/components/core"; -import { DeviceList, DeviceSelector, ProposalVolumes } from "~/components/storage"; +import { + DeviceList, DeviceSelector, + ProposalVolumes, + SpacePolicyButton, SpacePolicySelector, SpacePolicyDisksHint +} from "~/components/storage"; import { deviceLabel } from '~/components/storage/utils'; import { Icon } from "~/components/layout"; import { noop } from "~/utils"; @@ -228,7 +232,7 @@ const LVMSettingsForm = ({ const BootDevice = () => { const bootDevice = devices.find(d => d.name === settings.bootDevice); - return ; + return ; }; return ( @@ -509,6 +513,103 @@ const EncryptionPasswordField = ({ ); }; +/** + * Form for configuring the space policy. + * @component + * + * @param {object} props + * @param {string} props.id - Form ID. + * @param {ProposalSettings} props.settings - Settings used for calculating a proposal. + * @param {onSubmitFn} [props.onSubmit=noop] - On submit callback. + * + * @callback onSubmitFn + * @param {string} policy - Name of the selected policy. + */ +const SpacePolicyForm = ({ + id, + policy, + onSubmit: onSubmitProp = noop +}) => { + const [spacePolicy, setSpacePolicy] = useState(policy); + + const onSubmit = (e) => { + e.preventDefault(); + onSubmitProp(spacePolicy); + }; + + return ( +
    + + + ); +}; + +/** + * Allows to select SpacePolicy. + * @component + * + * @param {object} props + * @param {ProposalSettings} props.settings - Settings used for calculating a proposal. + * @param {boolean} [props.isLoading=false] - Whether to show the selector as loading. + * @param {onChangeFn} [props.onChange=noop] - On change callback. + * + * @callback onChangeFn + * @param {string} policy + */ +const SpacePolicyField = ({ + settings, + isLoading = false, + onChange = noop +}) => { + const [isFormOpen, setIsFormOpen] = useState(false); + const [spacePolicy, setSpacePolicy] = useState(settings.spacePolicy); + + const openForm = () => setIsFormOpen(true); + const closeForm = () => setIsFormOpen(false); + + const onSubmitForm = (policy) => { + onChange(policy); + setSpacePolicy(policy); + closeForm(); + }; + + if (isLoading) return ; + + const description = _("Select how to make free space in the disks selected for allocating the \ + file systems."); + + return ( +
    + {/* TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" */} + {_("Find space")} + + +
    + + +
    + + + {_("Accept")} + + + +
    +
    + ); +}; + /** * Section for editing the proposal settings * @component @@ -546,6 +647,10 @@ export default function ProposalSettingsSection({ onChange({ encryptionPassword: password }); }; + const changeSpacePolicy = (policy) => { + onChange({ spacePolicy: policy }); + }; + const changeVolumes = (volumes) => { onChange({ volumes }); }; @@ -581,6 +686,11 @@ export default function ProposalSettingsSection({ isLoading={isLoading} onChange={changeVolumes} /> + ); } diff --git a/web/src/components/storage/ProposalSettingsSection.test.jsx b/web/src/components/storage/ProposalSettingsSection.test.jsx index 9652a1bc9d..ec9d1b08cb 100644 --- a/web/src/components/storage/ProposalSettingsSection.test.jsx +++ b/web/src/components/storage/ProposalSettingsSection.test.jsx @@ -20,7 +20,7 @@ */ import React from "react"; -import { screen, within } from "@testing-library/react"; +import { screen, waitFor, within } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { ProposalSettingsSection } from "~/components/storage"; @@ -503,3 +503,167 @@ describe("Encryption field", () => { }); }); }); + +describe("Space policy field", () => { + beforeEach(() => { + props.settings = { installationDevices: [vda] }; + }); + + describe("if there is no space policy", () => { + beforeEach(() => { + props.settings.spacePolicy = undefined; + }); + + it("renders loading content", async () => { + plainRender(); + + await waitFor(() => ( + expect(screen.queryByText(/Find space/)).not.toBeInTheDocument()) + ); + screen.getAllByText("PFSkeleton"); + }); + }); + + describe("if the space policy is set to 'delete'", () => { + beforeEach(() => { + props.settings.spacePolicy = "delete"; + }); + + it("renders the text about deleting content", () => { + plainRender(); + + screen.getByText("Find space"); + screen.getByRole("button", { name: "deleting all content of the installation device" }); + }); + + describe("if there are more than one disk", () => { + beforeEach(() => { + props.settings.installationDevices = [vda, md0, md1]; + }); + + it("indicates the number of disks", () => { + plainRender(); + + screen.getByText("Find space"); + screen.getByRole("button", { name: "deleting all content of the 3 selected disks" }); + }); + }); + }); + + describe("if the space policy is set to 'resize'", () => { + beforeEach(() => { + props.settings.spacePolicy = "resize"; + }); + + it("renders the text about resizing content", () => { + plainRender(); + + screen.getByText("Find space"); + screen.getByRole("button", { name: "shrinking partitions of the installation device" }); + }); + + describe("if there are more than one disk", () => { + beforeEach(() => { + props.settings.installationDevices = [vda, md0, md1]; + }); + + it("indicates the number of disks", () => { + plainRender(); + + screen.getByText("Find space"); + screen.getByRole("button", { name: "shrinking partitions of the 3 selected disks" }); + }); + }); + }); + + describe("if the space policy is set to 'keep'", () => { + beforeEach(() => { + props.settings.spacePolicy = "keep"; + }); + + it("renders the text about keeping content", () => { + plainRender(); + + screen.getByText("Find space"); + screen.getByRole("button", { name: "without modifying any partition" }); + }); + }); + + describe("when the button for changing the space policy is clicked", () => { + beforeEach(() => { + props.settings.installationDevices = [vda, md0]; + props.settings.spacePolicy = "keep"; + props.onChange = jest.fn(); + }); + + it("opens a popup for selecting the space policy", async () => { + const { user } = plainRender(); + + const button = screen.getByRole("button", { name: /without modifying/ }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + within(popup).getByText("Space Policy"); + within(popup).getByRole("option", { name: /Delete current content/ }); + within(popup).getByRole("option", { name: /Shrink existing partitions/ }); + within(popup).getByRole("option", { name: /Use available space/, selected: true }); + }); + + it("allows to show the installation devices", async () => { + const { user } = plainRender(); + + const button = screen.getByRole("button", { name: /without modifying/ }); + await user.click(button); + const popup = await screen.findByRole("dialog"); + + await waitFor(() => ( + expect(within(popup).queryByText("/dev/vda")).not.toBeVisible() && + expect(within(popup).queryByText("/dev/md0")).not.toBeVisible() + )); + + const toggle = within(popup).getByRole("button", { name: /This will affect/ }); + await user.click(toggle); + + expect(within(popup).getByText("/dev/vda")).toBeVisible(); + expect(within(popup).getByText("/dev/md0")).toBeVisible(); + }); + + describe("if the popup is canceled", () => { + it("closes the popup without selecting a new space policy", async () => { + const { user } = plainRender(); + + const button = screen.getByRole("button", { name: /without modifying/ }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + const option = within(popup).getByRole("option", { name: /Shrink/ }); + + await user.click(option); + const cancel = within(popup).getByRole("button", { name: "Cancel" }); + await user.click(cancel); + + expect(props.onChange).not.toHaveBeenCalled(); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + + describe("if the popup is accepted", () => { + it("closes the popup selecting the new space policy", async () => { + const { user } = plainRender(); + + const button = screen.getByRole("button", { name: /without modifying/ }); + await user.click(button); + + const popup = await screen.findByRole("dialog"); + const option = within(popup).getByRole("option", { name: /Shrink/ }); + + await user.click(option); + const accept = within(popup).getByRole("button", { name: "Accept" }); + await user.click(accept); + + expect(props.onChange).toHaveBeenCalledWith({ spacePolicy: "resize" }); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + }); + }); +}); diff --git a/web/src/components/storage/ProposalVolumes.jsx b/web/src/components/storage/ProposalVolumes.jsx index 77d9a2e1c9..140e58e092 100644 --- a/web/src/components/storage/ProposalVolumes.jsx +++ b/web/src/components/storage/ProposalVolumes.jsx @@ -207,6 +207,7 @@ const VolumeRow = ({ columns, volume, options, isLoading, onEdit, onDelete }) => const text = `${volume.fsType} ${options.lvm ? _("logical volume") : _("partition")}`; const lockIcon = ; const snapshotsIcon = ; + const transactionalIcon = ; return (
    @@ -216,7 +217,7 @@ const VolumeRow = ({ columns, volume, options, isLoading, onEdit, onDelete }) => {/* TRANSLATORS: filesystem flag, it allows creating snapshots */} {_("with snapshots")}} /> {/* TRANSLATORS: flag for transactional file system */} - {_("transactional")}} /> + {_("transactional")}} />
    ); }; diff --git a/web/src/components/storage/device-utils.jsx b/web/src/components/storage/device-utils.jsx index 30900d6017..baf38ffb1a 100644 --- a/web/src/components/storage/device-utils.jsx +++ b/web/src/components/storage/device-utils.jsx @@ -32,14 +32,13 @@ import { deviceSize } from "~/components/storage/utils"; * @typedef {import ("~/clients/storage").StorageDevice} StorageDevice */ -const ListBox = ({ children, ...props }) =>
      {children}
    ; +const ListBox = ({ children, ...props }) =>
      {children}
    ; const ListBoxItem = ({ isSelected, children, onClick, ...props }) => { if (isSelected) props['aria-selected'] = true; return (
  • @@ -215,7 +214,7 @@ const DeviceItem = ({ device }) => { return ( <> - + @@ -229,11 +228,11 @@ const DeviceItem = ({ device }) => { * @param {Object} props * @param {StorageDevice[]} props.devices - Devices to show. */ -const DeviceList = ({ devices }) => { +const DeviceList = ({ devices, ...props }) => { return ( - + { devices.map(device => ( - + ))} @@ -268,13 +267,14 @@ const DeviceSelector = ({ devices, selected, isMultiple = false, onChange = noop }; return ( - + { devices.map(device => ( onOptionClick(device.name)} isSelected={isSelected(device.name)} - className="cursor-pointer" + data-type="storage-device" > diff --git a/web/src/components/storage/device-utils.test.jsx b/web/src/components/storage/device-utils.test.jsx index b23f6af0d8..572ac643f4 100644 --- a/web/src/components/storage/device-utils.test.jsx +++ b/web/src/components/storage/device-utils.test.jsx @@ -231,11 +231,22 @@ const renderOptions = (Component) => { describe("DeviceList", renderOptions(DeviceList)); describe("DeviceList", () => { - it("renders all options as selected", () => { - plainRender(); + describe("with isSelected prop", () => { + it("renders all devices as selected", () => { + plainRender(); - const selectedOptions = screen.queryAllByRole("option", { selected: true }); - expect(selectedOptions.length).toEqual(3); + const devices = screen.queryAllByText(/\/dev\//, { selected: true }); + expect(devices.length).toEqual(3); + }); + }); + + describe("without isSelected prop", () => { + it("renders all devices as not selected", () => { + plainRender(); + + const devices = screen.queryAllByText(/\/dev\//, { selected: false }); + expect(devices.length).toEqual(3); + }); }); }); diff --git a/web/src/components/storage/index.js b/web/src/components/storage/index.js index bb9b0d03dd..5dd66c988f 100644 --- a/web/src/components/storage/index.js +++ b/web/src/components/storage/index.js @@ -31,4 +31,5 @@ export { default as ZFCPPage } from "./ZFCPPage"; export { default as ZFCPDiskForm } from "./ZFCPDiskForm"; export { default as ISCSIPage } from "./ISCSIPage"; export { DeviceList, DeviceSelector } from "./device-utils"; +export { SpacePolicyButton, SpacePolicySelector, SpacePolicyDisksHint } from "./space-policy-utils"; export { default as VolumeForm } from "./VolumeForm"; diff --git a/web/src/components/storage/space-policy-utils.jsx b/web/src/components/storage/space-policy-utils.jsx new file mode 100644 index 0000000000..fbb5fc5275 --- /dev/null +++ b/web/src/components/storage/space-policy-utils.jsx @@ -0,0 +1,195 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React, { useState } from "react"; + +import { _, n_ } from "~/i18n"; +import { sprintf } from "sprintf-js"; +import { noop } from "~/utils"; +import { Button, ExpandableSection, Hint, HintBody } from "@patternfly/react-core"; +import { DeviceList } from "~/components/storage"; + +const ListBox = ({ children, ...props }) =>
      {children}
    ; + +const ListBoxItem = ({ isSelected, children, onClick, ...props }) => { + if (isSelected) props['aria-selected'] = true; + + return ( +
  • + {children} +
  • + ); +}; + +/** + * Content for a space policy item + * @component + * + * @param {Object} props + * @param {Locale} props.locale + */ +const PolicyItem = ({ policy }) => { + const Title = () => { + let text; + + switch (policy) { + case "delete": + // TRANSLATORS: automatic actions to find space for installation in the target disk(s) + text = _("Delete current content"); + break; + case "resize": + // TRANSLATORS: automatic actions to find space for installation in the target disk(s) + text = _("Shrink existing partitions"); + break; + case "keep": + // TRANSLATORS: automatic actions to find space for installation in the target disk(s) + text = _("Use available space"); + break; + } + + return
    {text}
    ; + }; + + const Description = () => { + let text; + + switch (policy) { + case "delete": + text = _("All partitions will be removed and any data in the disks will be lost."); + break; + case "resize": + text = _("The data is kept, but the current partitions will be resized as needed to make enough space."); + break; + case "keep": + text = _("The data is kept and existing partitions will not be modified. \ +Only the space that is not assigned to any partition will be used."); + break; + } + + return

    {text}

    ; + }; + + return ( + <> + + <Description /> + </> + ); +}; + +/** + * Component for selecting a policy to make space. + * @component + * + * @param {Object} props + * @param {string} [props.value] - Id of the currently selected policy. + * @param {(id: string) => void} [props.onChange] - Callback to be called when the selected policy + * changes. + */ +const SpacePolicySelector = ({ value, onChange = noop }) => { + return ( + <ListBox aria-label={_("Select a mechanism to make space")} role="listbox"> + { ["delete", "resize", "keep"].map(policy => ( + <ListBoxItem + key={policy} + role="option" + onClick={() => onChange(policy)} + isSelected={policy === value} + > + <PolicyItem policy={policy} /> + </ListBoxItem> + ))} + </ListBox> + ); +}; + +const SpacePolicyButton = ({ policy, devices, onClick = noop }) => { + const Text = () => { + const num = devices.length; + + switch (policy) { + case "delete": + return sprintf( + // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence + // would read as "Find space deleting all content[...]" + n_( + "deleting all content of the installation device", + "deleting all content of the %d selected disks", + num), + num + ); + case "resize": + return sprintf( + // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence + // would read as "Find space shrinking partitions[...]" + n_( + "shrinking partitions of the installation device", + "shrinking partitions of the %d selected disks", + num), + num + ); + case "keep": + // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence + // would read as "Find space without modifying any partition". + return _("without modifying any partition"); + } + console.log("Unsupported value " + policy); + return "error"; + }; + + return <Button variant="link" isInline onClick={onClick}><Text /></Button>; +}; + +const SpacePolicyDisksHint = ({ devices }) => { + const [isExpanded, setIsExpanded] = useState(false); + + const label = (num) => { + return sprintf( + n_( + "This will only affect the installation device", + "This will affect the %d disks selected for installation", + num + ), + num + ); + }; + + const num = devices.length; + + return ( + <Hint> + <HintBody> + <ExpandableSection + isExpanded={isExpanded} + onToggle={() => setIsExpanded(!isExpanded)} + toggleText={label(num)} + > + <DeviceList devices={devices} /> + </ExpandableSection> + </HintBody> + </Hint> + ); +}; + +export { SpacePolicyButton, SpacePolicySelector, SpacePolicyDisksHint }; diff --git a/web/src/context/agama.jsx b/web/src/context/agama.jsx index 0aa16e811b..5ee0b7d22e 100644 --- a/web/src/context/agama.jsx +++ b/web/src/context/agama.jsx @@ -23,6 +23,7 @@ import React from "react"; import { InstallerClientProvider } from "./installer"; +import { InstallerL10nProvider } from "./installerL10n"; import { L10nProvider } from "./l10n"; import { ProductProvider } from "./product"; import { NotificationProvider } from "./notification"; @@ -36,13 +37,15 @@ import { NotificationProvider } from "./notification"; function AgamaProviders({ children }) { return ( <InstallerClientProvider> - <L10nProvider> - <ProductProvider> - <NotificationProvider> - {children} - </NotificationProvider> - </ProductProvider> - </L10nProvider> + <InstallerL10nProvider> + <L10nProvider> + <ProductProvider> + <NotificationProvider> + {children} + </NotificationProvider> + </ProductProvider> + </L10nProvider> + </InstallerL10nProvider> </InstallerClientProvider> ); } diff --git a/web/src/context/installerL10n.jsx b/web/src/context/installerL10n.jsx new file mode 100644 index 0000000000..482843a274 --- /dev/null +++ b/web/src/context/installerL10n.jsx @@ -0,0 +1,262 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +// @ts-check + +import React, { useCallback, useEffect, useState } from "react"; +import { useCancellablePromise, locationReload, setLocationSearch } from "~/utils"; +import cockpit from "../lib/cockpit"; +import { useInstallerClient } from "./installer"; + +const L10nContext = React.createContext(null); + +/** + * Installer localization context. + * + * @typedef {object} L10nContext + * @property {string|undefined} language - Current language. + * @property {(language: string) => void} changeLanguage - Function to change the current language. + * + * @return {L10nContext} + */ +function useInstallerL10n() { + const context = React.useContext(L10nContext); + + if (!context) { + throw new Error("useInstallerL10n must be used within a InstallerL10nContext"); + } + + return context; +} + +/** + * Current language according to Cockpit (in xx_XX format). + * + * It takes the language from the CockpitLang cookie. + * + * @return {string|undefined} Undefined if language is not set. + */ +function cockpitLanguage() { + // language from cookie, empty string if not set (regexp taken from Cockpit) + // https://github.com/cockpit-project/cockpit/blob/98a2e093c42ea8cd2431cf15c7ca0e44bb4ce3f1/pkg/shell/shell-modals.jsx#L91 + const languageString = decodeURIComponent(document.cookie.replace(/(?:(?:^|.*;\s*)CockpitLang\s*=\s*([^;]*).*$)|^.*$/, "$1")); + if (languageString) { + return languageString.toLowerCase(); + } +} + +/** + * Helper function for storing the Cockpit language. + * + * Automatically converts the language from xx_XX to xx-xx, as it is the one used by Cockpit. + * + * @param {string} language - The new locale (e.g., "cs", "cs_CZ"). + * @return {boolean} True if the locale was changed. + */ +function storeCockpitLanguage(language) { + const current = cockpitLanguage(); + if (current === language) return false; + + // Code taken from Cockpit. + const cookie = "CockpitLang=" + encodeURIComponent(language) + "; path=/; expires=Sun, 16 Jul 3567 06:23:41 GMT"; + document.cookie = cookie; + window.localStorage.setItem("cockpit.lang", language); + return true; +} + +/** + * Returns the language tag from the query string. + * + * Query supports 'xx-xx', 'xx_xx', 'xx-XX' and 'xx_XX' formats. + * + * @return {string|undefined} Undefined if not set. + */ +function languageFromQuery() { + const lang = (new URLSearchParams(window.location.search)).get("lang"); + if (!lang) return undefined; + + const [language, country] = lang.toLowerCase().split(/[-_]/); + return (country) ? `${language}-${country}` : language; +} + +/** + * Generates a RFC 5646 (or BCP 78) language tag from a locale. + * + * @param {string} locale + * @return {string} + * + * @private + * @see https://datatracker.ietf.org/doc/html/rfc5646 + * @see https://www.rfc-editor.org/info/bcp78 + */ +function languageFromLocale(locale) { + return locale.replace("_", "-").toLowerCase(); +} + +/** + * Converts a RFC 5646 language tag to a locale. + * + * @param {string} language + * @return {string} + * + * @private + * @see https://datatracker.ietf.org/doc/html/rfc5646 + * @see https://www.rfc-editor.org/info/bcp78 + */ +function languageToLocale(language) { + const [lang, country] = language.split("-"); + return (country) ? `${lang}_${country.toUpperCase()}` : lang; +} + +/** + * List of RFC 5646 (or BCP 78) language tags from the navigator. + * + * @return {Array<string>} + */ +function navigatorLanguages() { + return navigator.languages.map(l => l.toLowerCase()); +} + +/** + * Returns the first supported language from the given list. + * + * @param {Array<string>} languages + * @return {string|undefined} Undefined if none of the given languages is supported. + */ +function findSupportedLanguage(languages) { + const supported = Object.keys(cockpit.manifests.agama?.locales || {}); + + for (const candidate of languages) { + const [language, country] = candidate.split("-"); + + const match = supported.find(s => { + const [supportedLanguage, supportedCountry] = s.split("-"); + if (language === supportedLanguage) { + return country === undefined || country === supportedCountry; + } else { + return false; + } + }); + if (match) return match; + } +} + +/** + * Reloads the page. + * + * It uses the window.location.replace instead of the reload function synchronizing the "lang" + * argument from the URL if present. + * + * @param {string} newLanguage + */ +function reload(newLanguage) { + const query = new URLSearchParams(window.location.search); + if (query.has("lang") && query.get("lang") !== newLanguage) { + query.set("lang", newLanguage); + // Setting location search with a different value makes the browser to navigate to the new URL. + setLocationSearch(query.toString()); + } else { + locationReload(); + } +} + +/** + * This provider sets the installer locale. By default, it uses the URL "lang" query parameter or + * the preferred locale from the browser and synchronizes the UI and the backend locales. To + * activate a new locale it reloads the whole page. + * + * Additionally, it offers a function to change the current locale. + * + * The format of the language tag in the query parameter follows the + * [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646) specification. + * + * @param {object} props + * @param {React.ReactNode} [props.children] - Content to display within the wrapper. + * + * @see useInstallerL10n + */ +function InstallerL10nProvider({ children }) { + const client = useInstallerClient(); + const [language, setLanguage] = useState(undefined); + const [backendPending, setBackendPending] = useState(false); + const { cancellablePromise } = useCancellablePromise(); + + const storeInstallerLanguage = useCallback(async (newLanguage) => { + if (!client) { + setBackendPending(true); + return false; + } + + const locale = await cancellablePromise(client.l10n.getUILocale()); + const currentLanguage = languageFromLocale(locale); + + if (currentLanguage !== newLanguage) { + // FIXME: fallback to en-US if the language is not supported. + await cancellablePromise(client.l10n.setUILocale(languageToLocale(newLanguage))); + return true; + } + + return false; + }, [client, cancellablePromise]); + + const changeLanguage = useCallback(async (lang) => { + const wanted = lang || languageFromQuery(); + + if (wanted === "xx" || wanted === "xx-xx") { + cockpit.language = wanted; + setLanguage(wanted); + return; + } + + const current = cockpitLanguage(); + const candidateLanguages = [wanted, current].concat(navigatorLanguages()).filter(l => l); + const newLanguage = findSupportedLanguage(candidateLanguages) || "en-us"; + + let mustReload = storeCockpitLanguage(newLanguage); + mustReload = await storeInstallerLanguage(newLanguage) || mustReload; + + if (mustReload) { + reload(newLanguage); + } else { + setLanguage(newLanguage); + } + }, [storeInstallerLanguage, setLanguage]); + + useEffect(() => { + if (!language) changeLanguage(); + }, [changeLanguage, language]); + + useEffect(() => { + if (!client || !backendPending) return; + + storeInstallerLanguage(language); + setBackendPending(false); + }, [client, language, backendPending, storeInstallerLanguage]); + + return ( + <L10nContext.Provider value={{ language, changeLanguage }}>{children}</L10nContext.Provider> + ); +} + +export { + InstallerL10nProvider, + useInstallerL10n +}; diff --git a/web/src/context/l10n.test.jsx b/web/src/context/installerL10n.test.jsx similarity index 77% rename from web/src/context/l10n.test.jsx rename to web/src/context/installerL10n.test.jsx index dd932b6102..fa3cf79d7b 100644 --- a/web/src/context/l10n.test.jsx +++ b/web/src/context/installerL10n.test.jsx @@ -25,17 +25,17 @@ import React from "react"; import { render, waitFor, screen } from "@testing-library/react"; -import { L10nProvider } from "~/context/l10n"; +import { InstallerL10nProvider } from "~/context/installerL10n"; import { InstallerClientProvider } from "./installer"; import * as utils from "~/utils"; -const getUILanguageFn = jest.fn().mockResolvedValue(); -const setUILanguageFn = jest.fn().mockResolvedValue(); +const getUILocaleFn = jest.fn().mockResolvedValue(); +const setUILocaleFn = jest.fn().mockResolvedValue(); const client = { - language: { - getUILanguage: getUILanguageFn, - setUILanguage: setUILanguageFn + l10n: { + getUILocale: getUILocaleFn, + setUILocale: setUILocaleFn }, onDisconnect: jest.fn() }; @@ -72,7 +72,7 @@ const TranslatedContent = () => { return <>{text[lang]}</>; }; -describe("L10nProvider", () => { +describe("InstallerL10nProvider", () => { beforeAll(() => { jest.spyOn(utils, "locationReload").mockImplementation(utils.noop); jest.spyOn(utils, "setLocationSearch"); @@ -95,13 +95,13 @@ describe("L10nProvider", () => { describe("when the Cockpit language is already set", () => { beforeEach(() => { document.cookie = "CockpitLang=en-us; path=/;"; - getUILanguageFn.mockResolvedValueOnce("en_US"); + getUILocaleFn.mockResolvedValueOnce("en_US"); }); it("displays the children content and does not reload", async () => { render( <InstallerClientProvider client={client}> - <L10nProvider><TranslatedContent /></L10nProvider> + <InstallerL10nProvider><TranslatedContent /></InstallerL10nProvider> </InstallerClientProvider> ); @@ -115,14 +115,14 @@ describe("L10nProvider", () => { describe("when the Cockpit language is set to an unsupported language", () => { beforeEach(() => { document.cookie = "CockpitLang=de-de; path=/;"; - getUILanguageFn.mockResolvedValueOnce("de_DE"); - getUILanguageFn.mockResolvedValueOnce("es_ES"); + getUILocaleFn.mockResolvedValueOnce("de_DE"); + getUILocaleFn.mockResolvedValueOnce("es_ES"); }); it("uses the first supported language from the browser", async () => { render( <InstallerClientProvider client={client}> - <L10nProvider><TranslatedContent /></L10nProvider> + <InstallerL10nProvider><TranslatedContent /></InstallerL10nProvider> </InstallerClientProvider> ); @@ -131,27 +131,27 @@ describe("L10nProvider", () => { // renders again after reloading render( <InstallerClientProvider client={client}> - <L10nProvider><TranslatedContent /></L10nProvider> + <InstallerL10nProvider><TranslatedContent /></InstallerL10nProvider> </InstallerClientProvider> ); await waitFor(() => screen.getByText("hola")); - expect(setUILanguageFn).toHaveBeenCalledWith("es_ES"); + expect(setUILocaleFn).toHaveBeenCalledWith("es_ES"); }); }); describe("when the Cockpit language is not set", () => { beforeEach(() => { // Ensure both, UI and backend mock languages, are in sync since - // client.setUILanguage is mocked too. + // client.setUILocale is mocked too. // See navigator.language in the beforeAll at the top of the file. - getUILanguageFn.mockResolvedValue("es_ES"); + getUILocaleFn.mockResolvedValue("es_ES"); }); it("sets the preferred language from browser and reloads", async () => { render( <InstallerClientProvider client={client}> - <L10nProvider><TranslatedContent /></L10nProvider> + <InstallerL10nProvider><TranslatedContent /></InstallerL10nProvider> </InstallerClientProvider> ); @@ -160,7 +160,7 @@ describe("L10nProvider", () => { // renders again after reloading render( <InstallerClientProvider client={client}> - <L10nProvider><TranslatedContent /></L10nProvider> + <InstallerL10nProvider><TranslatedContent /></InstallerL10nProvider> </InstallerClientProvider> ); await waitFor(() => screen.getByText("hola")); @@ -174,7 +174,7 @@ describe("L10nProvider", () => { it("sets the first which language matches", async () => { render( <InstallerClientProvider client={client}> - <L10nProvider><TranslatedContent /></L10nProvider> + <InstallerL10nProvider><TranslatedContent /></InstallerL10nProvider> </InstallerClientProvider> ); @@ -183,7 +183,7 @@ describe("L10nProvider", () => { // renders again after reloading render( <InstallerClientProvider client={client}> - <L10nProvider><TranslatedContent /></L10nProvider> + <InstallerL10nProvider><TranslatedContent /></InstallerL10nProvider> </InstallerClientProvider> ); await waitFor(() => screen.getByText("hola!")); @@ -200,19 +200,19 @@ describe("L10nProvider", () => { describe("when the Cockpit language is already set to 'cs-cz'", () => { beforeEach(() => { document.cookie = "CockpitLang=cs-cz; path=/;"; - getUILanguageFn.mockResolvedValueOnce("cs_CZ"); + getUILocaleFn.mockResolvedValueOnce("cs_CZ"); }); it("displays the children content and does not reload", async () => { render( <InstallerClientProvider client={client}> - <L10nProvider><TranslatedContent /></L10nProvider> + <InstallerL10nProvider><TranslatedContent /></InstallerL10nProvider> </InstallerClientProvider> ); // children are displayed await screen.findByText("ahoj"); - expect(setUILanguageFn).not.toHaveBeenCalled(); + expect(setUILocaleFn).not.toHaveBeenCalled(); expect(document.cookie).toEqual("CockpitLang=cs-cz"); expect(utils.locationReload).not.toHaveBeenCalled(); @@ -223,15 +223,15 @@ describe("L10nProvider", () => { describe("when the Cockpit language is set to 'en-us'", () => { beforeEach(() => { document.cookie = "CockpitLang=en-us; path=/;"; - getUILanguageFn.mockResolvedValueOnce("en_US"); - getUILanguageFn.mockResolvedValueOnce("cs_CZ"); - setUILanguageFn.mockResolvedValue(); + getUILocaleFn.mockResolvedValueOnce("en_US"); + getUILocaleFn.mockResolvedValueOnce("cs_CZ"); + setUILocaleFn.mockResolvedValue(); }); it("sets the 'cs-cz' language and reloads", async () => { render( <InstallerClientProvider client={client}> - <L10nProvider><TranslatedContent /></L10nProvider> + <InstallerL10nProvider><TranslatedContent /></InstallerL10nProvider> </InstallerClientProvider> ); @@ -240,26 +240,26 @@ describe("L10nProvider", () => { // renders again after reloading render( <InstallerClientProvider client={client}> - <L10nProvider><TranslatedContent /></L10nProvider> + <InstallerL10nProvider><TranslatedContent /></InstallerL10nProvider> </InstallerClientProvider> ); await waitFor(() => screen.getByText("ahoj")); - expect(setUILanguageFn).toHaveBeenCalledWith("cs_CZ"); + expect(setUILocaleFn).toHaveBeenCalledWith("cs_CZ"); }); }); describe("when the Cockpit language is not set", () => { beforeEach(() => { - getUILanguageFn.mockResolvedValueOnce("en_US"); - getUILanguageFn.mockResolvedValueOnce("cs_CZ"); - setUILanguageFn.mockResolvedValue(); + getUILocaleFn.mockResolvedValueOnce("en_US"); + getUILocaleFn.mockResolvedValueOnce("cs_CZ"); + setUILocaleFn.mockResolvedValue(); }); it("sets the 'cs-cz' language and reloads", async () => { render( <InstallerClientProvider client={client}> - <L10nProvider><TranslatedContent /></L10nProvider> + <InstallerL10nProvider><TranslatedContent /></InstallerL10nProvider> </InstallerClientProvider> ); @@ -268,12 +268,12 @@ describe("L10nProvider", () => { // reload the component render( <InstallerClientProvider client={client}> - <L10nProvider><TranslatedContent /></L10nProvider> + <InstallerL10nProvider><TranslatedContent /></InstallerL10nProvider> </InstallerClientProvider> ); await waitFor(() => screen.getByText("ahoj")); - expect(setUILanguageFn).toHaveBeenCalledWith("cs_CZ"); + expect(setUILocaleFn).toHaveBeenCalledWith("cs_CZ"); }); }); }); diff --git a/web/src/context/l10n.jsx b/web/src/context/l10n.jsx index d3a725b2fc..7948ed70c6 100644 --- a/web/src/context/l10n.jsx +++ b/web/src/context/l10n.jsx @@ -19,251 +19,106 @@ * find current contact information at www.suse.com. */ -// @ts-check - -import React, { useCallback, useEffect, useState } from "react"; -import { useCancellablePromise, locationReload, setLocationSearch } from "~/utils"; -import cockpit from "../lib/cockpit"; +import React, { useContext, useEffect, useState } from "react"; +import { useCancellablePromise } from "~/utils"; import { useInstallerClient } from "./installer"; -const L10nContext = React.createContext(null); - /** - * @typedef {object} L10nContext - * @property {string|undefined} language - current language - * @property {(lang: string) => void} changeLanguage - function to change the current language - * - * @return {L10nContext} L10n context + * @typedef {import ("~/clients/l10n").Locale} Locale + * @typedef {import ("~/clients/l10n").Keymap} Keymap + * @typedef {import ("~/clients/l10n").Timezone} Timezone */ -function useL10n() { - const context = React.useContext(L10nContext); - if (!context) { - throw new Error("useL10n must be used within a L10nContext"); - } - - return context; -} +const L10nContext = React.createContext({}); -/** - * Returns the current locale according to Cockpit - * - * It takes the locale from the CockpitLang cookie. - * - * @return {string|undefined} language tag in xx_XX format or undefined if - * it was not set. - */ -function cockpitLanguage() { - // language from cookie, empty string if not set (regexp taken from Cockpit) - // https://github.com/cockpit-project/cockpit/blob/98a2e093c42ea8cd2431cf15c7ca0e44bb4ce3f1/pkg/shell/shell-modals.jsx#L91 - const languageString = decodeURIComponent(document.cookie.replace(/(?:(?:^|.*;\s*)CockpitLang\s*=\s*([^;]*).*$)|^.*$/, "$1")); - if (languageString) { - return languageString.toLowerCase(); - } -} - -/** - * Helper function for storing the Cockpit language. - * - * This function automatically converts the language tag from xx_XX to xx-xx, - * as it is the one used by Cockpit. - * - * @param {string} lang the new language tag (like "cs", "cs_CZ",...) - * @return {boolean} returns true if the locale changed; false otherwise - */ -function storeUILanguage(lang) { - const current = cockpitLanguage(); - if (current === lang) { - return false; - } - // code taken from Cockpit - const cookie = "CockpitLang=" + encodeURIComponent(lang) + "; path=/; expires=Sun, 16 Jul 3567 06:23:41 GMT"; - document.cookie = cookie; - window.localStorage.setItem("cockpit.lang", lang); - return true; -} - -/** - * Returns the language from the query string. - * - * @return {string|undefined} language tag in 'xx-xx' format (or just 'xx') or undefined if it was - * not set. It supports 'xx-xx', 'xx_xx', 'xx-XX' and 'xx_XX'. - */ -function languageFromQuery() { - const lang = (new URLSearchParams(window.location.search)).get("lang"); - if (!lang) return undefined; +function L10nProvider({ children }) { + const client = useInstallerClient(); + const { cancellablePromise } = useCancellablePromise(); + const [timezones, setTimezones] = useState(); + const [selectedTimezone, setSelectedTimezone] = useState(); + const [locales, setLocales] = useState(); + const [selectedLocales, setSelectedLocales] = useState(); + const [keymaps, setKeymaps] = useState(); + const [selectedKeymap, setSelectedKeymap] = useState(); - const [language, country] = lang.toLowerCase().split(/[-_]/); - return (country) ? `${language}-${country}` : language; -} + useEffect(() => { + const load = async () => { + const timezones = await cancellablePromise(client.l10n.timezones()); + const selectedTimezone = await cancellablePromise(client.l10n.getTimezone()); + const locales = await cancellablePromise(client.l10n.locales()); + const selectedLocales = await cancellablePromise(client.l10n.getLocales()); + const keymaps = await cancellablePromise(client.l10n.keymaps()); + const selectedKeymap = await cancellablePromise(client.l10n.getKeymap()); + setTimezones(timezones); + setSelectedTimezone(selectedTimezone); + setLocales(locales); + setSelectedLocales(selectedLocales); + setKeymaps(keymaps); + setSelectedKeymap(selectedKeymap); + }; + + if (client) { + load().catch(console.error); + } + }, [cancellablePromise, client, setKeymaps, setLocales, setSelectedKeymap, setSelectedLocales, setSelectedTimezone, setTimezones]); -/** - * Converts a language tag from the backend to a one compatible with RFC 5646 or - * BCP 78 - * - * @param {string} tag - language tag from the backend - * @return {string} Language tag compatible with RFC 5646 or BCP 78 - * - * @private - * @see https://datatracker.ietf.org/doc/html/rfc5646 - * @see https://www.rfc-editor.org/info/bcp78 - */ -function languageFromBackend(tag) { - return tag.replace("_", "-").toLowerCase(); -} + useEffect(() => { + if (!client) return; -/** - * Converts a language tag compatible with RFC 5646 to the format used by the backend - * - * @param {string} tag - language tag from the backend - * @return {string} Language tag compatible with the backend - * - * @private - * @see https://datatracker.ietf.org/doc/html/rfc5646 - * @see https://www.rfc-editor.org/info/bcp78 - */ -function languageToBackend(tag) { - const [language, country] = tag.split("-"); - return (country) ? `${language}_${country.toUpperCase()}` : language; -} + return client.l10n.onTimezoneChange(setSelectedTimezone); + }, [client, setSelectedTimezone]); -/** - * Returns the list of languages from the navigator in RFC 5646 (or BCP 78) - * format - * - * @return {Array<string>} List of languages from the navigator - */ -function navigatorLanguages() { - return navigator.languages.map(l => l.toLowerCase()); -} + useEffect(() => { + if (!client) return; -/** - * Returns the first supported language from the given list. - * - * @param {Array<string>} languages - Candidate languages - * @return {string|undefined} First supported language or undefined if none - * of the given languages is supported. - */ -function findSupportedLanguage(languages) { - const supported = Object.keys(cockpit.manifests.agama?.locales || {}); + return client.l10n.onLocalesChange(setSelectedLocales); + }, [client, setSelectedLocales]); - for (const candidate of languages) { - const [language, country] = candidate.split("-"); + useEffect(() => { + if (!client) return; - const match = supported.find(s => { - const [supportedLanguage, supportedCountry] = s.split("-"); - if (language === supportedLanguage) { - return country === undefined || country === supportedCountry; - } else { - return false; - } - }); - if (match) return match; - } -} + return client.l10n.onKeymapChange(setSelectedKeymap); + }, [client, setSelectedKeymap]); -/** - * Reloads the page - * - * It uses the window.location.replace instead of the reload function - * synchronizing the "lang" argument from the URL if present. - * - * @param {string} newLanguage - */ -function reload(newLanguage) { - const query = new URLSearchParams(window.location.search); - if (query.has("lang") && query.get("lang") !== newLanguage) { - query.set("lang", newLanguage); - // Setting location search with a different value makes the browser to navigate - // to the new URL. - setLocationSearch(query.toString()); - } else { - locationReload(); - } + const value = { timezones, selectedTimezone, locales, selectedLocales, keymaps, selectedKeymap }; + return <L10nContext.Provider value={value}>{children}</L10nContext.Provider>; } /** - * This provider sets the application language. By default, it uses the - * URL "lang" query parameter or the preferred language from the browser and - * synchronizes the UI and the backend languages. To activate a new language it - * reloads the whole page. - * - * Additionally, it offers a function to change the current language. - * - * The format of the language tag follows the - * [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646) specification. - * - * @param {object} props - * @param {React.ReactNode} [props.children] - content to display within the wrapper - * @param {import("~/client").InstallerClient} [props.client] - client + * Localization context. + * @function * - * @see useL10n + * @typedef {object} L10nContext + * @property {Locale[]} locales + * @property {Keymap[]} keymaps + * @property {Timezone[]} timezones + * @property {Locale[]} selectedLocales + * @property {Keymap|undefined} selectedKeymap + * @property {Timezone|undefined} selectedTimezone + * + * @returns {L10nContext} */ -function L10nProvider({ children }) { - const client = useInstallerClient(); - const [language, setLanguage] = useState(undefined); - const [backendPending, setBackendPending] = useState(false); - const { cancellablePromise } = useCancellablePromise(); - - const storeBackendLanguage = useCallback(async languageString => { - if (!client) { - setBackendPending(true); - return false; - } - - const currentLang = await cancellablePromise(client.language.getUILanguage()); - const normalizedLang = languageFromBackend(currentLang); - - if (normalizedLang !== languageString) { - // FIXME: fallback to en-US if the language is not supported. - await cancellablePromise( - client.language.setUILanguage(languageToBackend(languageString)) - ); - return true; - } - return false; - }, [client, cancellablePromise]); - - const changeLanguage = useCallback(async lang => { - const wanted = lang || languageFromQuery(); - - if (wanted === "xx" || wanted === "xx-xx") { - cockpit.language = wanted; - setLanguage(wanted); - return; - } - - const current = cockpitLanguage(); - const candidateLanguages = [wanted, current].concat(navigatorLanguages()) - .filter(l => l); - const newLanguage = findSupportedLanguage(candidateLanguages) || "en-us"; - - let mustReload = storeUILanguage(newLanguage); - mustReload = await storeBackendLanguage(newLanguage) || mustReload; - if (mustReload) { - reload(newLanguage); - } else { - setLanguage(newLanguage); - } - }, [storeBackendLanguage, setLanguage]); +function useL10n() { + const context = useContext(L10nContext); - useEffect(() => { - if (!language) changeLanguage(); - }, [changeLanguage, language]); + if (!context) { + throw new Error("useL10n must be used within a L10nProvider"); + } - useEffect(() => { - if (!client || !backendPending) return; + const { + timezones = [], + selectedTimezone: selectedTimezoneId, + locales = [], + selectedLocales: selectedLocalesId = [], + keymaps = [], + selectedKeymap: selectedKeymapId + } = context; - storeBackendLanguage(language); - setBackendPending(false); - }, [client, language, backendPending, storeBackendLanguage]); + const selectedTimezone = timezones.find(t => t.id === selectedTimezoneId); + const selectedLocales = selectedLocalesId.map(id => locales.find(l => l.id === id)); + const selectedKeymap = keymaps.find(k => k.id === selectedKeymapId); - return ( - <L10nContext.Provider value={{ language, changeLanguage }}>{children}</L10nContext.Provider> - ); + return { timezones, selectedTimezone, locales, selectedLocales, keymaps, selectedKeymap }; } -export { - L10nProvider, - useL10n -}; +export { L10nProvider, useL10n }; diff --git a/web/src/context/product.jsx b/web/src/context/product.jsx index 4f476f710a..1a4387c190 100644 --- a/web/src/context/product.jsx +++ b/web/src/context/product.jsx @@ -23,6 +23,11 @@ import React, { useContext, useEffect, useState } from "react"; import { useCancellablePromise } from "~/utils"; import { useInstallerClient } from "./installer"; +/** + * @typedef {import ("~/clients/software").Product} Product + * @typedef {import ("~/clients/software").Registration} Registration + */ + const ProductContext = React.createContext([]); function ProductProvider({ children }) { @@ -64,6 +69,18 @@ function ProductProvider({ children }) { return <ProductContext.Provider value={value}>{children}</ProductContext.Provider>; } +/** + * Product context. + * @function + * + * @typedef {object} ProductContext + * @property {Product[]} products + * @property {Product|null} selectedProduct + * @property {string} selectedId + * @property {Registration} registration + * + * @returns {ProductContext} + */ function useProduct() { const context = useContext(ProductContext); diff --git a/web/src/manifest.json b/web/src/manifest.json index 4fba146b5f..225e2391bf 100644 --- a/web/src/manifest.json +++ b/web/src/manifest.json @@ -10,10 +10,10 @@ }, "locales": { "en-us": "English", - "ja-jp": "日本語", "fr-fr": "Français", + "ja-jp": "日本語", "nl-nl": "Nederlands", - "sv-se": "Svenska", - "es-es": "Español" + "es-es": "Español", + "sv-se": "Svenska" } } \ No newline at end of file diff --git a/web/src/test-utils.js b/web/src/test-utils.js index 1848f5a0e0..8e7a5f143d 100644 --- a/web/src/test-utils.js +++ b/web/src/test-utils.js @@ -36,6 +36,7 @@ import { NotificationProvider } from "~/context/notification"; import { Layout } from "~/components/layout"; import { noop } from "./utils"; import cockpit from "./lib/cockpit"; +import { InstallerL10nProvider } from "./context/installerL10n"; import { L10nProvider } from "./context/l10n"; /** @@ -84,9 +85,11 @@ const Providers = ({ children, withL10n }) => { if (withL10n) { return ( <InstallerClientProvider client={client}> - <L10nProvider> - {children} - </L10nProvider> + <InstallerL10nProvider> + <L10nProvider> + {children} + </L10nProvider> + </InstallerL10nProvider> </InstallerClientProvider> ); } diff --git a/web/src/utils.js b/web/src/utils.js index 3cd85fb228..1b3c9dab65 100644 --- a/web/src/utils.js +++ b/web/src/utils.js @@ -162,6 +162,47 @@ const useLocalStorage = (storageKey, fallbackState) => { return [value, setValue]; }; +/** + * Debounce hook. + * @function + * + * Source {@link https://designtechworld.medium.com/create-a-custom-debounce-hook-in-react-114f3f245260} + * + * @param {function} callback - Function to be called after some delay. + * @param {number} delay - Delay in milliseconds. + * @returns {function} + * + * @example + * + * const log = useDebounce(console.log, 1000); + * log("test ", 1) // The message will be logged after at least 1 second. + * log("test ", 2) // Subsequent calls cancels pending calls. + */ +const useDebounce = (callback, delay) => { + const timeoutRef = useRef(null); + + useEffect(() => { + // Cleanup the previous timeout on re-render + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + const debouncedCallback = (...args) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = setTimeout(() => { + callback(...args); + }, delay); + }; + + return debouncedCallback; +}; + const hex = (value) => { const sanitizedValue = value.replaceAll(".", ""); return parseInt(sanitizedValue, 16); @@ -208,14 +249,67 @@ const setLocationSearch = (query) => { window.location.search = query; }; +/** + * Time for the given timezone. + * + * @param {string} timezone - E.g., "Atlantic/Canary". + * @param {object} [options] + * @param {Date} options.date - Date to take the time from. + * + * @returns {string|undefined} - Time in 24 hours format (e.g., "23:56"). Undefined for an unknown + * timezone. + */ +const timezoneTime = (timezone, { date = new Date() }) => { + try { + const formatter = new Intl.DateTimeFormat( + "en-US", + { timeZone: timezone, timeStyle: "short", hour12: false } + ); + + return formatter.format(date); + } catch (e) { + if (e instanceof RangeError) return undefined; + + throw e; + } +}; + +/** + * UTC offset for the given timezone. + * + * @param {string} timezone - E.g., "Atlantic/Canary". + * @returns {number|undefined} - undefined for an unknown timezone. + */ +const timezoneUTCOffset = (timezone) => { + try { + const date = new Date(); + const dateLocaleString = date.toLocaleString( + "en-US", + { timeZone: timezone, timeZoneName: "short" } + ); + const [timezoneName] = dateLocaleString.split(' ').slice(-1); + const dateString = date.toString(); + const offset = Date.parse(`${dateString} UTC`) - Date.parse(`${dateString} ${timezoneName}`); + + return offset / 3600000; + } catch (e) { + if (e instanceof RangeError) return undefined; + + throw e; + } +}; + export { noop, partition, classNames, useCancellablePromise, useLocalStorage, + useDebounce, hex, toValidationError, locationReload, - setLocationSearch + setLocationSearch, + timezoneTime, + timezoneUTCOffset };