diff --git a/rust/agama-lib/src/localization/proxies.rs b/rust/agama-lib/src/localization/proxies.rs index 0f37e70a76..401e95470d 100644 --- a/rust/agama-lib/src/localization/proxies.rs +++ b/rust/agama-lib/src/localization/proxies.rs @@ -42,6 +42,7 @@ use zbus::proxy; #[proxy( default_service = "org.opensuse.Agama1", + default_path = "/org/opensuse/Agama1/Locale", interface = "org.opensuse.Agama1.Locale", assume_defaults = true )] diff --git a/rust/agama-lib/src/proxies.rs b/rust/agama-lib/src/proxies.rs index e478d05cb3..8b7452ca1e 100644 --- a/rust/agama-lib/src/proxies.rs +++ b/rust/agama-lib/src/proxies.rs @@ -33,6 +33,6 @@ mod issues; pub use issues::IssuesProxy; mod locale; -pub use locale::LocaleProxy; +pub use locale::LocaleMixinProxy; pub mod jobs; diff --git a/rust/agama-lib/src/proxies/locale.rs b/rust/agama-lib/src/proxies/locale.rs index ce49c9c2b3..ea7d57f94a 100644 --- a/rust/agama-lib/src/proxies/locale.rs +++ b/rust/agama-lib/src/proxies/locale.rs @@ -1,4 +1,4 @@ -//! # D-Bus interface proxy for: `org.opensuse.Agama1.Locale` +//! # D-Bus interface proxy for: `org.opensuse.Agama1.LocaleMixin` //! //! This code was generated by `zbus-xmlgen` `5.0.0` from D-Bus introspection data. //! Source: `org.opensuse.Agama1.Manager.bus.xml`. @@ -20,11 +20,12 @@ //! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, use zbus::proxy; #[proxy( - default_service = "org.opensuse.Agama1", - interface = "org.opensuse.Agama1.Locale", + default_service = "org.opensuse.Agama.Manager1", + default_path = "/org/opensuse/Agama/Manager1", + interface = "org.opensuse.Agama1.LocaleMixin", assume_defaults = true )] -pub trait Locale { +pub trait LocaleMixin { /// SetLocale method fn set_locale(&self, locale: &str) -> zbus::Result<()>; } diff --git a/rust/agama-server/src/l10n/web.rs b/rust/agama-server/src/l10n/web.rs index 193ef0de6d..25d87df664 100644 --- a/rust/agama-server/src/l10n/web.rs +++ b/rust/agama-server/src/l10n/web.rs @@ -29,7 +29,7 @@ use crate::{ }; use agama_lib::{ error::ServiceError, localization::model::LocaleConfig, localization::LocaleProxy, - proxies::LocaleProxy as ManagerLocaleProxy, + proxies::LocaleMixinProxy as ManagerLocaleProxy, }; use agama_locale_data::LocaleId; use axum::{ diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 3ff0c91ccd..df33050e4d 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Nov 13 12:03:02 UTC 2024 - Imobach Gonzalez Sosa + +- Properly update the localization settings of the D-Bus services + (bsc#1233159, bsc#1233160). + ------------------------------------------------------------------- Mon Nov 11 09:52:59 UTC 2024 - Imobach Gonzalez Sosa diff --git a/service/lib/agama/dbus/interfaces/locale.rb b/service/lib/agama/dbus/interfaces/locale.rb index 506cbc4c8c..6a9b358ee6 100644 --- a/service/lib/agama/dbus/interfaces/locale.rb +++ b/service/lib/agama/dbus/interfaces/locale.rb @@ -35,7 +35,7 @@ module Locale include Yast::I18n include Yast::Logger - LOCALE_INTERFACE = "org.opensuse.Agama1.Locale" + LOCALE_INTERFACE = "org.opensuse.Agama1.LocaleMixin" def self.included(base) base.class_eval do diff --git a/service/lib/agama/storage/manager.rb b/service/lib/agama/storage/manager.rb index 2cd5546abf..d7ea94436c 100644 --- a/service/lib/agama/storage/manager.rb +++ b/service/lib/agama/storage/manager.rb @@ -262,6 +262,7 @@ def system_issues # @return [Array] def probing_issues y2storage_issues = Y2Storage::StorageManager.instance.raw_probed.probing_issues + return [] if y2storage_issues.nil? y2storage_issues.map do |y2storage_issue| Issue.new(y2storage_issue.message, diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 0579572f9a..5a0eb97197 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Nov 13 12:14:06 UTC 2024 - Imobach Gonzalez Sosa + +- Do not crash when trying to change the language of the storage + service before the "config" phase (gh#agama-project/agama#1746). + ------------------------------------------------------------------- Tue Nov 5 16:11:35 UTC 2024 - Martin Vidner diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 8f1188efa8..69f7354082 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,14 @@ +------------------------------------------------------------------- +Wed Nov 13 12:06:41 UTC 2024 - Imobach Gonzalez Sosa + +- Several translation fixes (gh#agama-project/agama#1746): + - Use the correct capitalization for RFC 5646 language tags + (e.g., "pt-BR" instead of "pt-BR" instead of "pt-br") (bsc#1233160). + - Translate the products descriptions when the user changes + the language (gh#agama-project/agama#1724). + - Fallback to a similar language if the given one is not supported + (e.g., "es" for "es-AR") (gh#agama-project/agama#860). + ------------------------------------------------------------------- Wed Nov 6 06:06:51 UTC 2024 - Michal Filka diff --git a/web/share/locales.json b/web/share/locales.json index ff8ab3a492..3ce13e0d69 100644 --- a/web/share/locales.json +++ b/web/share/locales.json @@ -1,65 +1,65 @@ { - "af": "za", - "am": "et", - "ar": "eg", - "ast": "es", - "be": "by", - "bg": "bg", - "bn": "in", - "bs": "ba", - "ca": "es", - "cs": "cz", - "cy": "gb", - "da": "dk", - "de": "de", - "en": "us", - "el": "gr", - "es": "es", - "et": "ee", - "eu": "es", - "fa": "ir", - "fi": "fi", - "fr": "fr", - "gl": "es", - "gu": "in", - "he": "il", - "hi": "in", - "hr": "hr", - "hu": "hu", - "id": "id", - "it": "it", - "ja": "jp", - "ka": "kz", - "km": "kh", - "kn": "in", - "ko": "kr", - "lt": "lt", - "lv": "lv", - "mk": "mk", - "mr": "in", - "ms": "my", - "my": "mm", - "nb": "no", - "nds": "de", - "ne": "np", - "nl": "nl", - "nn": "no", - "pa": "in", - "pl": "pl", - "pt": "pt", - "ro": "ro", - "ru": "ru", - "si": "lk", - "sk": "sk", - "sl": "si", - "sq": "al", - "sr": "rs", - "sv": "se", - "ta": "in", - "tg": "tj", - "th": "th", - "tr": "tr", - "uk": "ua", - "vi": "vn", - "zu": "za" + "af": "ZA", + "am": "ET", + "ar": "EG", + "ast": "ES", + "be": "BY", + "bg": "BG", + "bn": "IN", + "bs": "BA", + "ca": "ES", + "cs": "CZ", + "cy": "GB", + "da": "DK", + "de": "DE", + "en": "US", + "el": "GR", + "es": "ES", + "et": "EE", + "eu": "ES", + "fa": "IR", + "fi": "FI", + "fr": "FR", + "gl": "ES", + "gu": "IN", + "he": "IL", + "hi": "IN", + "hr": "HR", + "hu": "HU", + "id": "ID", + "it": "IT", + "ja": "JP", + "ka": "KZ", + "km": "KH", + "kn": "IN", + "ko": "KR", + "lt": "LT", + "lv": "LV", + "mk": "MK", + "mr": "IN", + "ms": "MY", + "my": "MM", + "nb": "NO", + "nds": "DE", + "ne": "NP", + "nl": "NL", + "nn": "NO", + "pa": "IN", + "pl": "PL", + "pt": "PT", + "ro": "RO", + "ru": "RU", + "si": "LK", + "sk": "SK", + "sl": "SI", + "sq": "AL", + "sr": "RS", + "sv": "SE", + "ta": "IN", + "tg": "TJ", + "th": "TH", + "tr": "TR", + "uk": "UA", + "vi": "VN", + "zu": "ZA" } diff --git a/web/share/update-languages.py b/web/share/update-languages.py index 41971b43d3..b7e1c15aa9 100755 --- a/web/share/update-languages.py +++ b/web/share/update-languages.py @@ -24,6 +24,7 @@ # from argparse import ArgumentParser +from typing import Optional from langtable import language_name from pathlib import Path import json @@ -32,9 +33,9 @@ class Locale: language: str - territory: str + territory: Optional[str] - def __init__(self, language: str, territory: str = None): + def __init__(self, language: str, territory: Optional[str] = None): self.language = language self.territory = territory @@ -44,13 +45,13 @@ def code(self): def name(self, include_territory: bool = False): if include_territory: return language_name(languageId=self.language, - territoryId=self.territory.upper()) + territoryId=self.territory) else: return language_name(languageId=self.language) class PoFile: - path: str + path: Path locale: Locale def __init__(self, path: Path): @@ -85,7 +86,7 @@ class Languages: def __init__(self): self.content = dict() - def update(self, po_files, lang2territory: str, threshold: int): + def update(self, po_files, lang2territory, threshold: int): """ Generate the list of supported locales @@ -97,7 +98,7 @@ def update(self, po_files, lang2territory: str, threshold: int): :param threshold Percentage of the strings that must be covered to include the locale in the manifest """ - supported = [Locale("en", "us")] + supported = [Locale("en", "US")] for path in po_files: po_file = PoFile(path) diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index 3c4aa32bbb..e0fc16b0b8 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -34,7 +34,7 @@ jest.mock("~/api/l10n", () => ({ ...jest.requireActual("~/api/l10n"), fetchConfig: jest.fn().mockResolvedValue({ uiKeymap: "en", - uiLocale: "en_us", + uiLocale: "en_US", }), updateConfig: jest.fn(), })); @@ -98,7 +98,7 @@ jest.mock("~/components/product/ProductSelectionProgress", () => () =>
Prod describe("App", () => { beforeEach(() => { // setting the language through a cookie - document.cookie = "agamaLang=en-us; path=/;"; + document.cookie = "agamaLang=en-US; path=/;"; (createClient as jest.Mock).mockImplementation(() => { return {}; }); diff --git a/web/src/components/core/InstallerOptions.tsx b/web/src/components/core/InstallerOptions.tsx index cb109ee4fc..0e9e0bf966 100644 --- a/web/src/components/core/InstallerOptions.tsx +++ b/web/src/components/core/InstallerOptions.tsx @@ -63,7 +63,7 @@ export default function InstallerOptions({ isOpen = false, onClose }: InstallerO const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); setInProgress(true); - changeKeymap(keymap); + await changeKeymap(keymap); changeLanguage(language) .then(close) .catch(() => setInProgress(false)); diff --git a/web/src/context/installerL10n.test.jsx b/web/src/context/installerL10n.test.tsx similarity index 83% rename from web/src/context/installerL10n.test.jsx rename to web/src/context/installerL10n.test.tsx index 3c18780203..e97d324920 100644 --- a/web/src/context/installerL10n.test.jsx +++ b/web/src/context/installerL10n.test.tsx @@ -45,30 +45,33 @@ jest.mock("~/api/l10n", () => ({ })); const client = { + isConnected: jest.fn().mockResolvedValue(true), + isRecoverable: jest.fn(), onConnect: jest.fn(), onDisconnect: jest.fn(), + onEvent: jest.fn(), }; jest.mock("~/languages.json", () => ({ - "es-ar": "Español (Argentina)", - "cs-cz": "čeština", - "en-us": "English (US)", - "es-es": "Español", + "es-AR": "Español (Argentina)", + "cs-CZ": "čeština", + "en-US": "English (US)", + "es-ES": "Español", })); // Helper component that displays a translated message depending on the // agamaLang value. const TranslatedContent = () => { const text = { - "cs-cz": "ahoj", - "en-us": "hello", - "es-es": "hola", - "es-ar": "hola!", + "cs-CZ": "ahoj", + "en-US": "hello", + "es-ES": "hola", + "es-AR": "hola!", }; const regexp = /agamaLang=([^;]+)/; const found = document.cookie.match(regexp); - if (!found) return <>{text["en-us"]}; + if (!found) return <>{text["en-US"]}; const [, lang] = found; return <>{text[lang]}; @@ -80,8 +83,7 @@ describe("InstallerL10nProvider", () => { jest.spyOn(utils, "setLocationSearch"); mockUpdateConfigFn.mockResolvedValue(true); - delete window.navigator; - window.navigator = { languages: ["es-es", "cs-cz"] }; + jest.spyOn(window.navigator, "languages", "get").mockReturnValue(["es-ES", "cs-CZ"]); }); // remove the language cookie after each test @@ -97,7 +99,7 @@ describe("InstallerL10nProvider", () => { describe("when the language is already set", () => { beforeEach(() => { - document.cookie = "agamaLang=en-us; path=/;"; + document.cookie = "agamaLang=en-US; path=/;"; mockFetchConfigFn.mockResolvedValue({ uiLocale: "en_US.UTF-8" }); }); @@ -119,9 +121,8 @@ describe("InstallerL10nProvider", () => { describe("when the language is set to an unsupported language", () => { beforeEach(() => { - document.cookie = "agamaLang=de-de; path=/;"; - mockFetchConfigFn.mockResolvedValueOnce({ uiLocale: "de_DE.UTF-8" }); - mockFetchConfigFn.mockResolvedValue({ uiLocale: "es_ES.UTF-8" }); + document.cookie = "agamaLang=de-DE; path=/;"; + mockFetchConfigFn.mockResolvedValue({ uiLocale: "de_DE.UTF-8" }); }); it("uses the first supported language from the browser", async () => { @@ -181,7 +182,7 @@ describe("InstallerL10nProvider", () => { describe("when the browser language does not contain the full locale", () => { beforeEach(() => { - window.navigator = { languages: ["es", "cs-cz"] }; + jest.spyOn(window.navigator, "languages", "get").mockReturnValue(["es", "cs-CZ"]); }); it("sets the first which language matches", async () => { @@ -214,9 +215,9 @@ describe("InstallerL10nProvider", () => { history.replaceState(history.state, null, `http://localhost/?lang=cs-CZ`); }); - describe("when the language is already set to 'cs-cz'", () => { + describe("when the language is already set to 'cs-CZ'", () => { beforeEach(() => { - document.cookie = "agamaLang=cs-cz; path=/;"; + document.cookie = "agamaLang=cs-CZ; path=/;"; mockFetchConfigFn.mockResolvedValue({ uiLocale: "cs_CZ.UTF-8" }); }); @@ -233,21 +234,19 @@ describe("InstallerL10nProvider", () => { await screen.findByText("ahoj"); expect(mockUpdateConfigFn).not.toHaveBeenCalled(); - expect(document.cookie).toMatch(/agamaLang=cs-cz/); + expect(document.cookie).toMatch(/agamaLang=cs-CZ/); expect(utils.locationReload).not.toHaveBeenCalled(); expect(utils.setLocationSearch).not.toHaveBeenCalled(); }); }); - describe("when the language is set to 'en-us'", () => { + describe("when the language is set to 'en-US'", () => { beforeEach(() => { - document.cookie = "agamaLang=en-us; path=/;"; - mockFetchConfigFn.mockResolvedValueOnce({ uiLocale: "en_US" }); - mockFetchConfigFn.mockResolvedValueOnce({ uiLocale: "cs_CZ" }); - mockUpdateConfigFn.mockResolvedValue(); + document.cookie = "agamaLang=en-US; path=/;"; + mockFetchConfigFn.mockResolvedValue({ uiLocale: "en_US" }); }); - it("sets the 'cs-cz' language and reloads", async () => { + it("sets the 'cs-CZ' language and reloads", async () => { render( @@ -256,8 +255,6 @@ describe("InstallerL10nProvider", () => { , ); - await waitFor(() => expect(utils.setLocationSearch).toHaveBeenCalledWith("lang=cs-cz")); - // renders again after reloading render( @@ -274,12 +271,10 @@ describe("InstallerL10nProvider", () => { describe("when the language is not set", () => { beforeEach(() => { - mockFetchConfigFn.mockResolvedValueOnce({ uiLocale: "en_US.UTF-8" }); - mockFetchConfigFn.mockResolvedValue({ uiLocale: "cs_CZ.UTF-8" }); - mockUpdateConfigFn.mockResolvedValue(); + mockFetchConfigFn.mockResolvedValue({ uiLocale: "en_US.UTF-8" }); }); - it("sets the 'cs-cz' language and reloads", async () => { + it("sets the 'cs-CZ' language and reloads", async () => { render( @@ -288,8 +283,6 @@ describe("InstallerL10nProvider", () => { , ); - await waitFor(() => expect(utils.setLocationSearch).toHaveBeenCalledWith("lang=cs-cz")); - // reload the component render( diff --git a/web/src/context/installerL10n.jsx b/web/src/context/installerL10n.tsx similarity index 61% rename from web/src/context/installerL10n.jsx rename to web/src/context/installerL10n.tsx index ec10ce6947..3e3dfed660 100644 --- a/web/src/context/installerL10n.jsx +++ b/web/src/context/installerL10n.tsx @@ -21,10 +21,9 @@ */ // cspell:ignore localectl setxkbmap xorg -// @ts-check import React, { useCallback, useEffect, useState } from "react"; -import { useCancellablePromise, locationReload, setLocationSearch } from "~/utils"; +import { locationReload, setLocationSearch } from "~/utils"; import { useInstallerClientStatus } from "./installer"; import agama from "~/agama"; import supportedLanguages from "~/languages.json"; @@ -34,16 +33,15 @@ const L10nContext = React.createContext(null); /** * Installer localization context. - * - * @typedef {object} L10nContext - * @property {string|undefined} language - Current language. - * @property {string|undefined} keymap - Current keymap. - * @property {(language: string) => Promise} changeLanguage - Function to change the current language. - * @property {(keymap: string) => Promise} changeKeymap - Function to change the current keymap. - * - * @return {L10nContext} */ -function useInstallerL10n() { +interface L10nContext { + language: string | undefined; + keymap: string | undefined; + changeLanguage: (language: string) => Promise; + changeKeymap: (keymap: string) => Promise; +} + +function useInstallerL10n(): L10nContext { const context = React.useContext(L10nContext); if (!context) { @@ -58,17 +56,14 @@ function useInstallerL10n() { * * It takes the language from the agamaLang cookie. * - * @return {string|undefined} Undefined if language is not set. + * @return Undefined if language is not set. */ -function agamaLanguage() { +function agamaLanguage(): string | undefined { // 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( + return decodeURIComponent( document.cookie.replace(/(?:(?:^|.*;\s*)agamaLang\s*=\s*([^;]*).*$)|^.*$/, "$1"), ); - if (languageString) { - return languageString.toLowerCase(); - } } /** @@ -76,10 +71,10 @@ function agamaLanguage() { * * Automatically converts the language from xx_XX to xx-xx, as it is the one used by Agama. * - * @param {string} language - The new locale (e.g., "cs", "cs_CZ"). - * @return {boolean} True if the locale was changed. + * @param language - The new locale (e.g., "cs", "cs_CZ"). + * @return True if the locale was changed. */ -function storeAgamaLanguage(language) { +function storeAgamaLanguage(language: string): boolean { const current = agamaLanguage(); if (current === language) return false; @@ -88,15 +83,6 @@ function storeAgamaLanguage(language) { "agamaLang=" + encodeURIComponent(language) + "; path=/; expires=Sun, 16 Jul 3567 06:23:41 GMT"; document.cookie = cookie; - // for backward compatibility, CockpitLang cookie is needed to load correct po.js content from Cockpit - // TODO: remove after dropping Cockpit completely - const cockpit_cookie = - "CockpitLang=" + - encodeURIComponent(language) + - "; path=/; expires=Sun, 16 Jul 3567 06:23:41 GMT"; - document.cookie = cockpit_cookie; - window.localStorage.setItem("cockpit.lang", language); - return true; } @@ -105,29 +91,29 @@ function storeAgamaLanguage(language) { * * Query supports 'xx-xx', 'xx_xx', 'xx-XX' and 'xx_XX' formats. * - * @return {string|undefined} Undefined if not set. + * @return Undefined if not set. */ -function languageFromQuery() { +function languageFromQuery(): string | undefined { const lang = new URLSearchParams(window.location.search).get("lang"); if (!lang) return undefined; - const [language, country] = lang.toLowerCase().split(/[-_]/); - return country ? `${language}-${country}` : language; + const [language, country] = lang.split(/[-_]/); + return country ? `${language.toLowerCase()}-${country.toUpperCase()}` : language; } /** * Generates a RFC 5646 (or BCP 78) language tag from a locale. * - * @param {string} locale - * @return {string} + * @param locale + * @return RFC 5646 language tag (e.g., "en-US") * * @private * @see https://datatracker.ietf.org/doc/html/rfc5646 * @see https://www.rfc-editor.org/info/bcp78 */ -function languageFromLocale(locale) { +function languageFromLocale(locale: string): string { const [language] = locale.split("."); - return language.replace("_", "-").toLowerCase(); + return language.replace("_", "-"); } /** @@ -135,35 +121,26 @@ function languageFromLocale(locale) { * * It forces the encoding to "UTF-8". * - * @param {string} language - * @return {string} + * @param language as a RFC 5646 language tag (e.g., "en-US") + * @return locale (e.g., "en_US.UTF-8") * * @private * @see https://datatracker.ietf.org/doc/html/rfc5646 * @see https://www.rfc-editor.org/info/bcp78 */ -function languageToLocale(language) { +function languageToLocale(language: string): string { const [lang, country] = language.split("-"); const locale = country ? `${lang}_${country.toUpperCase()}` : lang; return `${locale}.UTF-8`; } -/** - * List of RFC 5646 (or BCP 78) language tags from the navigator. - * - * @return {Array} - */ -function navigatorLanguages() { - return navigator.languages.map((l) => l.toLowerCase()); -} - /** * Returns the first supported language from the given list. * - * @param {Array} languages - * @return {string|undefined} Undefined if none of the given languages is supported. + * @param languages - list of RFC 5646 language tags (e.g., ["en-US", "en"]) to check + * @return Undefined if none of the given languages is supported. */ -function findSupportedLanguage(languages) { +function findSupportedLanguage(languages: Array): string | undefined { const supported = Object.keys(supportedLanguages); for (const candidate of languages) { @@ -187,9 +164,9 @@ function findSupportedLanguage(languages) { * It uses the window.location.replace instead of the reload function synchronizing the "lang" * argument from the URL if present. * - * @param {string} newLanguage + * @param newLanguage - new language to use. */ -function reload(newLanguage) { +function reload(newLanguage: string) { const query = new URLSearchParams(window.location.search); if (query.has("lang") && query.get("lang") !== newLanguage) { query.set("lang", newLanguage); @@ -210,55 +187,44 @@ function reload(newLanguage) { * 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. + * @param props + * @param [props.children] - Content to display within the wrapper. * * @see useInstallerL10n */ -function InstallerL10nProvider({ children }) { +function InstallerL10nProvider({ children }: { children?: React.ReactNode }) { const { connected } = useInstallerClientStatus(); const [language, setLanguage] = useState(undefined); const [keymap, setKeymap] = useState(undefined); - const [backendPending, setBackendPending] = useState(false); - const { cancellablePromise } = useCancellablePromise(); - - const storeInstallerLanguage = useCallback( - async (newLanguage) => { - if (!connected) { - setBackendPending(true); - return false; - } - - const config = await cancellablePromise(fetchConfig()); - const currentLanguage = languageFromLocale(config.uiLocale); - if (currentLanguage !== newLanguage) { - // FIXME: fallback to en-US if the language is not supported. - await cancellablePromise(updateConfig({ uiLocale: languageToLocale(newLanguage) })); - return true; - } + const syncBackendLanguage = useCallback(async () => { + const config = await fetchConfig(); + const backendLanguage = languageFromLocale(config.uiLocale); + if (backendLanguage === language) return; - return false; - }, - [connected, cancellablePromise], - ); + // FIXME: fallback to en-US if the language is not supported. + await updateConfig({ uiLocale: languageToLocale(language) }); + }, [language]); const changeLanguage = useCallback( - async (lang) => { + async (lang?: string) => { const wanted = lang || languageFromQuery(); - if (wanted === "xx" || wanted === "xx-xx") { + // Just for development purposes + if (wanted === "xx" || wanted === "xx-XX") { agama.language = wanted; setLanguage(wanted); return; } - const current = agamaLanguage(); - const candidateLanguages = [wanted, current].concat(navigatorLanguages()).filter((l) => l); - const newLanguage = findSupportedLanguage(candidateLanguages) || "en-us"; - - let mustReload = storeAgamaLanguage(newLanguage); - mustReload = (await storeInstallerLanguage(newLanguage)) || mustReload; + const candidateLanguages = [ + wanted, + wanted?.split("-")[0], // fallback to the language (e.g., "es" for "es-AR") + agamaLanguage(), + ...navigator.languages, + ].filter((l) => l); + const newLanguage = findSupportedLanguage(candidateLanguages) || "en-US"; + const mustReload = storeAgamaLanguage(newLanguage); if (mustReload) { reload(newLanguage); @@ -266,15 +232,15 @@ function InstallerL10nProvider({ children }) { setLanguage(newLanguage); } }, - [storeInstallerLanguage, setLanguage], + [setLanguage], ); const changeKeymap = useCallback( - async (id) => { + async (id: string) => { if (!connected) return; setKeymap(id); - updateConfig({ uiKeymap: id }); + await updateConfig({ uiKeymap: id }); }, [setKeymap, connected], ); @@ -284,14 +250,14 @@ function InstallerL10nProvider({ children }) { }, [changeLanguage, language]); useEffect(() => { - if (!connected || !backendPending) return; + if (!connected || !language) return; - storeInstallerLanguage(language); - setBackendPending(false); - }, [connected, language, backendPending, storeInstallerLanguage]); + syncBackendLanguage(); + }, [connected, language, syncBackendLanguage]); useEffect(() => { if (!connected) return; + fetchConfig().then((c) => setKeymap(c.uiKeymap)); }, [setKeymap, connected]); diff --git a/web/src/languages.json b/web/src/languages.json index ed1dec31eb..11b8afb8a3 100644 --- a/web/src/languages.json +++ b/web/src/languages.json @@ -1,15 +1,15 @@ { - "ca-es": "Català", - "cs-cz": "Čeština", - "de-de": "Deutsch", - "en-us": "English", - "es-es": "Español", - "fr-fr": "Français", - "ja-jp": "日本語", + "ca-ES": "Català", + "cs-CZ": "Čeština", + "de-DE": "Deutsch", + "en-US": "English", + "es-ES": "Español", + "fr-FR": "Français", + "ja-JP": "日本語", "nb-NO": "Norsk bokmål", "pt-BR": "Português", - "ru-ru": "Русский", - "sv-se": "Svenska", - "tr-tr": "Türkçe", + "ru-RU": "Русский", + "sv-SE": "Svenska", + "tr-TR": "Türkçe", "zh-Hans": "中文" } \ No newline at end of file diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index 9ad08b7ec3..6030489bd7 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -188,6 +188,10 @@ const useProductChanges = () => { if (event.type === "ProductChanged") { queryClient.invalidateQueries({ queryKey: ["software/config"] }); } + + if (event.type === "LocaleChanged") { + queryClient.invalidateQueries({ queryKey: ["software/products"] }); + } }); }, [client, queryClient]); };