diff --git a/apps/console/src/components/form/CountriesField.tsx b/apps/console/src/components/form/CountriesField.tsx index bc071d11e..2f1c2c7f8 100644 --- a/apps/console/src/components/form/CountriesField.tsx +++ b/apps/console/src/components/form/CountriesField.tsx @@ -63,9 +63,17 @@ type CountriesFieldInputProps = { }; function CountriesFieldInput(props: CountriesFieldInputProps) { - const { i18n } = useTranslation(); + const { t, i18n } = useTranslation(); const [animateBadge, setAnimateBadge] = useState(false); + const countryLabel = (code: CountryCode) => { + // GLOBAL is not a valid Intl region code; use the translated label. + if (code === "GLOBAL") { + return t("country.GLOBAL"); + } + return getCountryName(i18n.language, code); + }; + const addCountry = (code: string) => { setAnimateBadge(true); props.onValueChange([...props.value, code]); @@ -94,7 +102,7 @@ function CountriesFieldInput(props: CountriesFieldInputProps) { && "starting:opacity-0 starting:w-0 w-max transition-all duration-500 starting:bg-accent", )} > - {getCountryName(i18n.language, countryCode as CountryCode)} + {countryLabel(countryCode as CountryCode)}
@@ -110,6 +118,7 @@ function CountriesFieldInput(props: CountriesFieldInputProps) { (c: CountryCode) => !props.value.includes(c), )} onAdd={addCountry} + countryLabel={countryLabel} /> )} @@ -119,15 +128,16 @@ function CountriesFieldInput(props: CountriesFieldInputProps) { type CountryInputProps = { availableCountries: readonly CountryCode[]; onAdd: (code: string) => void; + countryLabel: (code: CountryCode) => string; }; -function CountryInput({ availableCountries, onAdd }: CountryInputProps) { - const { t, i18n } = useTranslation(); +function CountryInput({ availableCountries, onAdd, countryLabel }: CountryInputProps) { + const { t } = useTranslation(); const [search, setSearch] = useState(""); const [isOpen, setIsOpen] = useState(false); const countryOptions = availableCountries.map(code => ({ value: code, - label: getCountryName(i18n.language, code), + label: countryLabel(code), })); useEffect(() => { diff --git a/packages/helpers/src/countries.test.ts b/packages/helpers/src/countries.test.ts new file mode 100644 index 000000000..5fc2eab3b --- /dev/null +++ b/packages/helpers/src/countries.test.ts @@ -0,0 +1,43 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { describe, it, expect } from "vitest"; +import { getCountryName, getCountryOptions } from "./countries"; + +describe("getCountryName", () => { + it("returns a label for the GLOBAL pseudo-region", () => { + expect(getCountryName("en", "GLOBAL")).toBe("Global"); + }); + + it("returns localized names for ISO country codes", () => { + expect(getCountryName("en", "US")).toBe("United States"); + expect(getCountryName("en", "EU")).toBe("European Union"); + }); +}); + +describe("getCountryOptions", () => { + it("includes GLOBAL without throwing", () => { + const options = getCountryOptions("en"); + expect(options.find(option => option.value === "GLOBAL")).toEqual({ + value: "GLOBAL", + label: "Global", + }); + }); +}); diff --git a/packages/helpers/src/countries.ts b/packages/helpers/src/countries.ts index ef2599490..351d149ec 100644 --- a/packages/helpers/src/countries.ts +++ b/packages/helpers/src/countries.ts @@ -50,8 +50,22 @@ export const countries = [ export type CountryCode = typeof countries[number]; +// Pseudo-regions accepted by our CountryCode enum but not by Intl.DisplayNames. +const pseudoRegionNames: Partial> = { + GLOBAL: "Global", +}; + export function getCountryName(language: string, code: CountryCode): string { - return new Intl.DisplayNames(language, { type: "region" }).of(code) ?? code; + const pseudoName = pseudoRegionNames[code]; + if (pseudoName) { + return pseudoName; + } + + try { + return new Intl.DisplayNames(language, { type: "region" }).of(code) ?? code; + } catch { + return code; + } } export function getCountryOptions(lang: string) {