feat(i18n): refactor language constants and improve GP pipeline

- Extract SUPPORTED_LANGUAGES into src/frontend/src/constants/languages.ts
- Update LanguageForm to import from shared constants, add aria-label
- Add settings.languageSelectAriaLabel key to en.json
- Update LanguageSelector component to use shared SUPPORTED_LANGUAGES
- Fix gp_client and download_translations scripts
- Update GP upload/download/nightly GitHub Actions workflows
- Add GP script tests
This commit is contained in:
RamGopalSrikar
2026-03-26 17:12:28 -04:00
parent e691da85f3
commit a4540bbcdf
15 changed files with 541 additions and 37 deletions

View File

@ -1,9 +1,6 @@
name: GP Download Translations
on:
push:
branches:
- feat/globalization-pipeline # temp: remove before merge
schedule:
- cron: "0 6 * * *" # Daily at 6am UTC
workflow_dispatch:

View File

@ -4,7 +4,6 @@ on:
push:
branches:
- main
- feat/globalization-pipeline # temp: remove before merge
paths:
- "src/frontend/src/locales/en.json"
workflow_dispatch:

View File

@ -210,6 +210,7 @@ jobs:
name: Download translations from Globalization Pipeline
needs: [validate-inputs, resolve-release-branch, create-nightly-tag]
runs-on: ubuntu-latest
continue-on-error: true
environment: GP-test
permissions:
contents: write
@ -246,9 +247,9 @@ jobs:
git push
frontend-tests-linux:
if: always() && github.repository == 'langflow-ai/langflow' && !inputs.skip_frontend_tests && needs.download-gp-bundle.result != 'cancelled'
if: always() && github.repository == 'langflow-ai/langflow' && !inputs.skip_frontend_tests
name: Run Frontend Tests - Linux
needs: [resolve-release-branch, create-nightly-tag, download-gp-bundle]
needs: [resolve-release-branch, create-nightly-tag]
uses: ./.github/workflows/typescript_test.yml
with:
tests_folder: "tests"
@ -262,9 +263,9 @@ jobs:
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
frontend-tests-windows:
if: always() && github.repository == 'langflow-ai/langflow' && !inputs.skip_frontend_tests && needs.download-gp-bundle.result != 'cancelled'
if: always() && github.repository == 'langflow-ai/langflow' && !inputs.skip_frontend_tests
name: Run Frontend Tests - Windows
needs: [resolve-release-branch, create-nightly-tag, download-gp-bundle]
needs: [resolve-release-branch, create-nightly-tag]
# Windows tests are non-blocking - the release-nightly-build job only checks
# frontend-tests-linux.result, allowing Windows tests to fail without blocking the build.
# This gives us visibility into Windows-specific issues while we stabilize the tests.
@ -305,10 +306,10 @@ jobs:
# ref: ${{ needs.create-nightly-tag.outputs.tag }}
release-nightly-build:
if: github.repository == 'langflow-ai/langflow' && always() && needs.frontend-tests-linux.result != 'failure' && needs.backend-unit-tests.result != 'failure' && needs.download-gp-bundle.result != 'failure'
if: github.repository == 'langflow-ai/langflow' && always() && needs.frontend-tests-linux.result != 'failure' && needs.backend-unit-tests.result != 'failure'
name: Run Nightly Langflow Build
needs:
[validate-inputs, create-nightly-tag, frontend-tests-linux, frontend-tests-windows, backend-unit-tests, download-gp-bundle]
[validate-inputs, create-nightly-tag, frontend-tests-linux, frontend-tests-windows, backend-unit-tests]
uses: ./.github/workflows/release_nightly.yml
with:
build_docker_base: true

View File

@ -6,6 +6,7 @@ Usage:
import argparse
import json
import sys
from pathlib import Path
from gp_client import TARGET_LANGS, get_strings
@ -19,6 +20,7 @@ def main():
output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)
failed = []
for lang in TARGET_LANGS:
print(f"Downloading '{lang}' translations...")
try:
@ -44,6 +46,11 @@ def main():
except Exception as e: # noqa: BLE001
print(f" Error downloading '{lang}': {e}")
failed.append(lang)
if failed:
print(f"\nFAILED languages: {failed}")
sys.exit(1)
print("\nDone.")

View File

@ -22,6 +22,7 @@ GP_INSTANCE = os.getenv("GP_INSTANCE", "langflow-test")
GP_BUNDLE = os.getenv("GP_BUNDLE", "langflow-ui")
TARGET_LANGS = ["fr", "ja", "es", "de", "pt", "zh-Hans"]
REQUEST_TIMEOUT = 30
VERIFY_SSL = os.getenv("GP_VERIFY_SSL", "true").lower() != "false"
def get_headers(url, method, body=None):
@ -57,7 +58,7 @@ def get_headers(url, method, body=None):
def list_bundles():
"""List all bundles in the GP instance."""
url = f"{BASE_URL}/{GP_INSTANCE}/v2/bundles"
response = requests.get(url, headers=get_headers(url, "GET"), verify=False, timeout=REQUEST_TIMEOUT) # noqa: S501
response = requests.get(url, headers=get_headers(url, "GET"), verify=VERIFY_SSL, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
return response.json()
@ -70,7 +71,7 @@ def create_bundle(source_lang="en"):
url,
headers=get_headers(url, "PUT", body),
json=body,
verify=False, # noqa: S501
verify=VERIFY_SSL,
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
@ -84,7 +85,7 @@ def upload_strings(strings, lang="en"):
url,
headers=get_headers(url, "PUT", strings),
json=strings,
verify=False, # noqa: S501
verify=VERIFY_SSL,
timeout=REQUEST_TIMEOUT,
)
response.raise_for_status()
@ -94,6 +95,6 @@ def upload_strings(strings, lang="en"):
def get_strings(lang):
"""Download translated strings for a language from GP."""
url = f"{BASE_URL}/{GP_INSTANCE}/v2/bundles/{GP_BUNDLE}/{lang}"
response = requests.get(url, headers=get_headers(url, "GET"), verify=False, timeout=REQUEST_TIMEOUT) # noqa: S501
response = requests.get(url, headers=get_headers(url, "GET"), verify=VERIFY_SSL, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
return response.json()

View File

View File

@ -0,0 +1,100 @@
"""Tests for download_translations.py."""
import json
import sys
from unittest.mock import patch
import pytest
def _run_main(output_dir: str):
"""Helper to invoke download_translations.main() with an --output argument."""
with patch("sys.argv", ["download_translations.py", "--output", output_dir]):
import importlib
import scripts.gp.download_translations as dl_mod # noqa: PLC0415
importlib.reload(dl_mod)
dl_mod.main()
SAMPLE_RESPONSE = {
"resourceStrings": {
"hello": {"value": "Bonjour"},
"bye": {"value": "Au revoir"},
}
}
class TestDownloadTranslations:
def test_writes_json_files_for_each_language(self, tmp_path):
with (
patch("scripts.gp.download_translations.get_strings", return_value=SAMPLE_RESPONSE),
patch("scripts.gp.download_translations.TARGET_LANGS", ["fr", "es"]),
):
_run_main(str(tmp_path))
for lang in ["fr", "es"]:
out = tmp_path / f"{lang}.json"
assert out.exists()
data = json.loads(out.read_text(encoding="utf-8"))
assert data == {"hello": "Bonjour", "bye": "Au revoir"}
def test_skips_language_with_empty_strings(self, tmp_path):
def _get_strings(lang):
if lang == "ja":
return {"resourceStrings": {}}
return SAMPLE_RESPONSE
with (
patch("scripts.gp.download_translations.get_strings", side_effect=_get_strings),
patch("scripts.gp.download_translations.TARGET_LANGS", ["fr", "ja"]),
):
_run_main(str(tmp_path))
assert (tmp_path / "fr.json").exists()
assert not (tmp_path / "ja.json").exists()
def test_exits_with_error_on_partial_failure(self, tmp_path):
def _get_strings(lang):
if lang == "de":
raise ConnectionError("network error")
return SAMPLE_RESPONSE
with (
patch("scripts.gp.download_translations.get_strings", side_effect=_get_strings),
patch("scripts.gp.download_translations.TARGET_LANGS", ["fr", "de"]),
):
with pytest.raises(SystemExit) as exc_info:
_run_main(str(tmp_path))
assert exc_info.value.code == 1
# Successful language should still have been written
assert (tmp_path / "fr.json").exists()
def test_exits_cleanly_when_all_succeed(self, tmp_path):
with (
patch("scripts.gp.download_translations.get_strings", return_value=SAMPLE_RESPONSE),
patch("scripts.gp.download_translations.TARGET_LANGS", ["fr"]),
):
# Should not raise
_run_main(str(tmp_path))
assert (tmp_path / "fr.json").exists()
def test_handles_flat_string_values_in_response(self, tmp_path):
flat_response = {
"resourceStrings": {
"hello": "Hola",
"bye": "Adiós",
}
}
with (
patch("scripts.gp.download_translations.get_strings", return_value=flat_response),
patch("scripts.gp.download_translations.TARGET_LANGS", ["es"]),
):
_run_main(str(tmp_path))
data = json.loads((tmp_path / "es.json").read_text(encoding="utf-8"))
assert data == {"hello": "Hola", "bye": "Adiós"}

View File

@ -0,0 +1,226 @@
"""Tests for gp_client.py — HMAC auth and API operations."""
import base64
import hashlib
import hmac
import json
from unittest.mock import MagicMock, patch
import pytest
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _expected_signature(password: str, message: str) -> str:
sig = hmac.new(
bytes(password, "ISO-8859-1"),
msg=bytes(message, "ISO-8859-1"),
digestmod=hashlib.sha1,
).digest()
return base64.b64encode(sig).decode()
# ---------------------------------------------------------------------------
# get_headers
# ---------------------------------------------------------------------------
class TestGetHeaders:
def test_get_request_authorization_format(self):
with patch.dict(
"os.environ",
{"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"},
):
import importlib
import scripts.gp.gp_client as gp_client # noqa: PLC0415
importlib.reload(gp_client)
headers = gp_client.get_headers("https://example.com/api", "GET")
assert headers["Authorization"].startswith("GP-HMAC user123:")
assert "GP-Date" in headers
assert headers["accept"] == "application/json"
assert "Content-Type" not in headers
def test_put_request_includes_content_type(self):
with patch.dict(
"os.environ",
{"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"},
):
import importlib
import scripts.gp.gp_client as gp_client # noqa: PLC0415
importlib.reload(gp_client)
headers = gp_client.get_headers("https://example.com/api", "PUT", {"key": "val"})
assert headers["Content-Type"] == "application/json"
def test_patch_request_uses_merge_patch_content_type(self):
with patch.dict(
"os.environ",
{"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"},
):
import importlib
import scripts.gp.gp_client as gp_client # noqa: PLC0415
importlib.reload(gp_client)
headers = gp_client.get_headers("https://example.com/api", "PATCH", {})
assert headers["Content-Type"] == "application/merge-patch+json"
def test_hmac_signature_is_deterministic_for_same_inputs(self):
with patch.dict(
"os.environ",
{"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"},
):
import importlib
import scripts.gp.gp_client as gp_client # noqa: PLC0415
importlib.reload(gp_client)
with patch("scripts.gp.gp_client.datetime") as mock_dt:
mock_dt.now.return_value.strftime.return_value = "Thu, 26 Mar 2026 12:00:00 UTC"
h1 = gp_client.get_headers("https://example.com", "GET")
h2 = gp_client.get_headers("https://example.com", "GET")
assert h1["Authorization"] == h2["Authorization"]
# ---------------------------------------------------------------------------
# list_bundles
# ---------------------------------------------------------------------------
class TestListBundles:
def test_returns_parsed_json_on_success(self):
mock_response = MagicMock()
mock_response.json.return_value = {"bundleIds": ["langflow-ui"]}
with patch.dict(
"os.environ",
{"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"},
), patch("scripts.gp.gp_client.requests.get", return_value=mock_response) as mock_get:
import importlib
import scripts.gp.gp_client as gp_client # noqa: PLC0415
importlib.reload(gp_client)
result = gp_client.list_bundles()
mock_get.assert_called_once()
mock_response.raise_for_status.assert_called_once()
assert result == {"bundleIds": ["langflow-ui"]}
def test_raises_on_http_error(self):
import requests as req
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = req.exceptions.HTTPError("401 Unauthorized")
with patch.dict(
"os.environ",
{"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"},
), patch("scripts.gp.gp_client.requests.get", return_value=mock_response):
import importlib
import scripts.gp.gp_client as gp_client # noqa: PLC0415
importlib.reload(gp_client)
with pytest.raises(req.exceptions.HTTPError):
gp_client.list_bundles()
def test_raises_on_timeout(self):
import requests as req
with patch.dict(
"os.environ",
{"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"},
), patch(
"scripts.gp.gp_client.requests.get",
side_effect=req.exceptions.Timeout,
):
import importlib
import scripts.gp.gp_client as gp_client # noqa: PLC0415
importlib.reload(gp_client)
with pytest.raises(req.exceptions.Timeout):
gp_client.list_bundles()
# ---------------------------------------------------------------------------
# upload_strings
# ---------------------------------------------------------------------------
class TestUploadStrings:
def test_successful_upload(self):
mock_response = MagicMock()
mock_response.json.return_value = {"status": "ok"}
with patch.dict(
"os.environ",
{"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"},
), patch("scripts.gp.gp_client.requests.put", return_value=mock_response) as mock_put:
import importlib
import scripts.gp.gp_client as gp_client # noqa: PLC0415
importlib.reload(gp_client)
result = gp_client.upload_strings({"hello": "Hello"})
mock_put.assert_called_once()
assert result == {"status": "ok"}
def test_raises_on_server_error(self):
import requests as req
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = req.exceptions.HTTPError("500")
with patch.dict(
"os.environ",
{"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"},
), patch("scripts.gp.gp_client.requests.put", return_value=mock_response):
import importlib
import scripts.gp.gp_client as gp_client # noqa: PLC0415
importlib.reload(gp_client)
with pytest.raises(req.exceptions.HTTPError):
gp_client.upload_strings({"hello": "Hello"})
# ---------------------------------------------------------------------------
# get_strings
# ---------------------------------------------------------------------------
class TestGetStrings:
def test_returns_parsed_json(self):
mock_response = MagicMock()
mock_response.json.return_value = {
"resourceStrings": {"hello": {"value": "Bonjour"}}
}
with patch.dict(
"os.environ",
{"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"},
), patch("scripts.gp.gp_client.requests.get", return_value=mock_response):
import importlib
import scripts.gp.gp_client as gp_client # noqa: PLC0415
importlib.reload(gp_client)
result = gp_client.get_strings("fr")
assert result["resourceStrings"]["hello"]["value"] == "Bonjour"
def test_raises_on_403(self):
import requests as req
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = req.exceptions.HTTPError("403 Forbidden")
with patch.dict(
"os.environ",
{"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"},
), patch("scripts.gp.gp_client.requests.get", return_value=mock_response):
import importlib
import scripts.gp.gp_client as gp_client # noqa: PLC0415
importlib.reload(gp_client)
with pytest.raises(req.exceptions.HTTPError):
gp_client.get_strings("fr")

View File

@ -0,0 +1,76 @@
"""Tests for upload_strings.py."""
import json
import sys
from pathlib import Path
from unittest.mock import MagicMock, call, patch
import pytest
def _run_main(source_path: str):
"""Helper to invoke upload_strings.main() with a --source argument."""
with patch("sys.argv", ["upload_strings.py", "--source", source_path]):
import importlib
import scripts.gp.upload_strings as upload_mod # noqa: PLC0415
importlib.reload(upload_mod)
upload_mod.main()
class TestUploadStrings:
def test_uploads_strings_when_bundle_exists(self, tmp_path):
source = tmp_path / "en.json"
source.write_text(json.dumps({"hello": "Hello", "bye": "Bye"}), encoding="utf-8")
with (
patch("scripts.gp.upload_strings.list_bundles", return_value={"bundleIds": ["langflow-ui"]}),
patch("scripts.gp.upload_strings.create_bundle") as mock_create,
patch("scripts.gp.upload_strings.upload_strings") as mock_upload,
patch("scripts.gp.upload_strings.GP_BUNDLE", "langflow-ui"),
):
_run_main(str(source))
mock_create.assert_not_called()
mock_upload.assert_called_once_with({"hello": "Hello", "bye": "Bye"})
def test_creates_bundle_when_missing_then_uploads(self, tmp_path):
source = tmp_path / "en.json"
source.write_text(json.dumps({"hello": "Hello"}), encoding="utf-8")
with (
patch("scripts.gp.upload_strings.list_bundles", return_value={"bundleIds": []}),
patch("scripts.gp.upload_strings.create_bundle") as mock_create,
patch("scripts.gp.upload_strings.upload_strings") as mock_upload,
patch("scripts.gp.upload_strings.GP_BUNDLE", "langflow-ui"),
):
_run_main(str(source))
mock_create.assert_called_once()
mock_upload.assert_called_once_with({"hello": "Hello"})
def test_empty_json_file_uploads_empty_dict(self, tmp_path):
source = tmp_path / "en.json"
source.write_text("{}", encoding="utf-8")
with (
patch("scripts.gp.upload_strings.list_bundles", return_value={"bundleIds": ["langflow-ui"]}),
patch("scripts.gp.upload_strings.create_bundle"),
patch("scripts.gp.upload_strings.upload_strings") as mock_upload,
patch("scripts.gp.upload_strings.GP_BUNDLE", "langflow-ui"),
):
_run_main(str(source))
mock_upload.assert_called_once_with({})
def test_raises_when_source_file_missing(self, tmp_path):
missing = str(tmp_path / "missing.json")
with (
patch("scripts.gp.upload_strings.list_bundles", return_value={"bundleIds": []}),
patch("scripts.gp.upload_strings.create_bundle"),
patch("scripts.gp.upload_strings.upload_strings"),
):
with pytest.raises(FileNotFoundError):
_run_main(missing)

View File

@ -1,19 +1,10 @@
import { useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { SUPPORTED_LANGUAGES } from "@/constants/languages";
import { useTypesStore } from "@/stores/typesStore";
const LANGUAGES = [
{ code: "en", label: "English" },
{ code: "fr", label: "Français" },
{ code: "es", label: "Español" },
{ code: "de", label: "Deutsch" },
{ code: "pt", label: "Português" },
{ code: "ja", label: "日本語" },
{ code: "zh-Hans", label: "中文" },
];
export const LanguageSelector = () => {
const { i18n } = useTranslation();
const { t, i18n } = useTranslation();
const queryClient = useQueryClient();
const setTypes = useTypesStore((state) => state.setTypes);
@ -26,11 +17,12 @@ export const LanguageSelector = () => {
return (
<select
aria-label={t("settings.languageSelectAriaLabel")}
value={i18n.language}
onChange={(e) => handleChange(e.target.value)}
className="rounded border border-border bg-background px-1 py-0.5 text-sm text-foreground"
>
{LANGUAGES.map((lang) => (
{SUPPORTED_LANGUAGES.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.label}
</option>

View File

@ -0,0 +1,9 @@
export const SUPPORTED_LANGUAGES = [
{ code: "en", label: "English" },
{ code: "fr", label: "Français" },
{ code: "es", label: "Español" },
{ code: "de", label: "Deutsch" },
{ code: "pt", label: "Português" },
{ code: "ja", label: "日本語" },
{ code: "zh-Hans", label: "中文" },
] as const;

11
src/frontend/src/i18next.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import "i18next";
import type en from "./locales/en.json";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "translation";
resources: {
translation: typeof en;
};
}
}

View File

@ -176,6 +176,7 @@
"settings.languageTitle": "Language",
"settings.languageDescription": "Choose the display language for the Langflow interface.",
"settings.languageRecommended": "Recommended",
"settings.languageSelectAriaLabel": "Select language",
"settings.profilePictureTitle": "Profile Picture",
"settings.profilePictureDescription": "Choose the image that appears as your profile picture.",
"settings.apiKeysTitle": "Langflow API Keys",

View File

@ -0,0 +1,92 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SUPPORTED_LANGUAGES } from "@/constants/languages";
const mockChangeLanguage = jest.fn();
const mockInvalidateQueries = jest.fn();
const mockSetTypes = jest.fn();
jest.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
i18n: { changeLanguage: mockChangeLanguage, language: "en" },
}),
}));
jest.mock("@tanstack/react-query", () => ({
useQueryClient: () => ({ invalidateQueries: mockInvalidateQueries }),
}));
jest.mock("@/stores/typesStore", () => ({
useTypesStore: (selector: (s: { setTypes: typeof mockSetTypes }) => unknown) =>
selector({ setTypes: mockSetTypes }),
}));
jest.mock("@/components/ui/card", () => ({
Card: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
CardHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
CardTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
CardDescription: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
CardContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
import LanguageFormComponent from "../index";
describe("LanguageFormComponent", () => {
beforeEach(() => {
jest.clearAllMocks();
localStorage.clear();
});
it("renders all supported language options", () => {
render(<LanguageFormComponent />);
const select = screen.getByRole("combobox");
const options = select.querySelectorAll("option");
expect(options).toHaveLength(SUPPORTED_LANGUAGES.length);
SUPPORTED_LANGUAGES.forEach((lang) => {
expect(screen.getByRole("option", { name: new RegExp(lang.label) })).toBeInTheDocument();
});
});
it("calls i18n.changeLanguage with the selected language code", async () => {
const user = userEvent.setup();
render(<LanguageFormComponent />);
await user.selectOptions(screen.getByRole("combobox"), "fr");
expect(mockChangeLanguage).toHaveBeenCalledWith("fr");
});
it("saves selected language to localStorage", async () => {
const user = userEvent.setup();
render(<LanguageFormComponent />);
await user.selectOptions(screen.getByRole("combobox"), "ja");
expect(localStorage.getItem("languagePreference")).toBe("ja");
});
it("calls setTypes with empty object on language change", async () => {
const user = userEvent.setup();
render(<LanguageFormComponent />);
await user.selectOptions(screen.getByRole("combobox"), "de");
expect(mockSetTypes).toHaveBeenCalledWith({});
});
it("invalidates useGetTypes query on language change", async () => {
const user = userEvent.setup();
render(<LanguageFormComponent />);
await user.selectOptions(screen.getByRole("combobox"), "es");
expect(mockInvalidateQueries).toHaveBeenCalledWith({
queryKey: ["useGetTypes"],
});
});
it("shows recommended label for English option", () => {
render(<LanguageFormComponent />);
const enOption = screen.getByRole("option", { name: /English/i });
expect(enOption.textContent).toContain("settings.languageRecommended");
});
it("does not show recommended label for non-English options", () => {
render(<LanguageFormComponent />);
const frOption = screen.getByRole("option", { name: /Français/i });
expect(frOption.textContent).not.toContain("settings.languageRecommended");
});
});

View File

@ -1,5 +1,6 @@
import { useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { SUPPORTED_LANGUAGES } from "@/constants/languages";
import { useTypesStore } from "@/stores/typesStore";
import {
Card,
@ -9,16 +10,6 @@ import {
CardTitle,
} from "../../../../../../components/ui/card";
const LANGUAGES = [
{ code: "en", label: "English" },
{ code: "fr", label: "Français" },
{ code: "es", label: "Español" },
{ code: "de", label: "Deutsch" },
{ code: "pt", label: "Português" },
{ code: "ja", label: "日本語" },
{ code: "zh-Hans", label: "中文" },
];
const LanguageFormComponent = () => {
const { t, i18n } = useTranslation();
const queryClient = useQueryClient();
@ -39,11 +30,12 @@ const LanguageFormComponent = () => {
</CardHeader>
<CardContent>
<select
aria-label={t("settings.languageSelectAriaLabel")}
value={i18n.language}
onChange={(e) => handleChange(e.target.value)}
className="rounded border border-border bg-background px-2 py-1.5 text-sm text-foreground"
>
{LANGUAGES.map((lang) => (
{SUPPORTED_LANGUAGES.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.label}
{lang.code === "en"