diff --git a/packages/translation/src/client/index.ts b/packages/translation/src/client/index.ts index 990267981..62b684d6b 100644 --- a/packages/translation/src/client/index.ts +++ b/packages/translation/src/client/index.ts @@ -6,7 +6,7 @@ import type { SupportedLanguage } from "../config"; import type englishTranslation from "../lang/en.json"; export { useChangeLocale } from "./use-change-locale"; -export { useCurrentLocale } from "./use-current-locale"; +export { useCurrentIntlLocale, useCurrentLocale } from "./use-current-locale"; declare module "next-intl" { interface AppConfig { diff --git a/packages/translation/src/client/use-current-locale.ts b/packages/translation/src/client/use-current-locale.ts index a0e796010..6a8ec03a5 100644 --- a/packages/translation/src/client/use-current-locale.ts +++ b/packages/translation/src/client/use-current-locale.ts @@ -1,3 +1,7 @@ import { useLocale } from "next-intl"; +import { getIntlLocale } from "../config"; + export const useCurrentLocale = useLocale; + +export const useCurrentIntlLocale = () => getIntlLocale(useLocale()); diff --git a/packages/translation/src/config.spec.ts b/packages/translation/src/config.spec.ts new file mode 100644 index 000000000..55c3adcbb --- /dev/null +++ b/packages/translation/src/config.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; + +import { getIntlLocale } from "./config"; + +describe("getIntlLocale", () => { + it("maps Homarr-specific locale keys to canonical Intl locales", () => { + expect(getIntlLocale("cn")).toBe("zh-CN"); + expect(getIntlLocale("zh")).toBe("zh-TW"); + expect(getIntlLocale("cr")).toBe("en"); + }); + + it("preserves locale keys that Intl already understands", () => { + expect(getIntlLocale("fr")).toBe("fr"); + expect(getIntlLocale("de-CH")).toBe("de-CH"); + }); +}); diff --git a/packages/translation/src/config.ts b/packages/translation/src/config.ts index ea3bc7dca..883bc80c1 100644 --- a/packages/translation/src/config.ts +++ b/packages/translation/src/config.ts @@ -438,5 +438,13 @@ export type LanguageIconDefinition = export { fallbackLocale } from "./languages"; +const intlLocaleOverrides: Partial> = { + cn: "zh-CN", + cr: "en", + zh: "zh-TW", +}; + +export const getIntlLocale = (locale: SupportedLanguage): string => intlLocaleOverrides[locale] ?? locale; + export const isLocaleRTL = (locale: SupportedLanguage) => "isRTL" in localeConfigurations[locale] && localeConfigurations[locale].isRTL; diff --git a/packages/widgets/src/archive-team-warrior/component.tsx b/packages/widgets/src/archive-team-warrior/component.tsx index 83cf74289..35cb3049e 100644 --- a/packages/widgets/src/archive-team-warrior/component.tsx +++ b/packages/widgets/src/archive-team-warrior/component.tsx @@ -5,7 +5,7 @@ import { Avatar, Badge, Card, Group, ScrollArea, SimpleGrid, Stack, Text } from import { clientApi } from "@homarr/api/client"; import { formatByteRate } from "@homarr/common"; import { getIconUrl } from "@homarr/definitions"; -import { useCurrentLocale, useScopedI18n } from "@homarr/translation/client"; +import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; @@ -47,7 +47,7 @@ const ArchiveTeamWarriorWidgetContent = ({ isAdvanced: boolean; }) => { const t = useScopedI18n("widget.archiveTeamWarrior"); - const locale = useCurrentLocale(); + const locale = useCurrentIntlLocale(); const [data] = clientApi.widget.archiveTeamWarrior.getStatus.useSuspenseQuery({ integrationId }); const status = data.status; diff --git a/packages/widgets/src/media-releases/component.tsx b/packages/widgets/src/media-releases/component.tsx index e90743f27..c04e155de 100644 --- a/packages/widgets/src/media-releases/component.tsx +++ b/packages/widgets/src/media-releases/component.tsx @@ -24,7 +24,7 @@ import { getIconUrl } from "@homarr/definitions"; import type { MediaRelease } from "@homarr/integrations/types"; import { mediaTypeConfigurations } from "@homarr/integrations/types"; import type { TranslationFunction } from "@homarr/translation"; -import { useCurrentLocale, useI18n } from "@homarr/translation/client"; +import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client"; import type { TablerIcon } from "@homarr/ui"; import { OverflowBadge } from "@homarr/ui"; @@ -78,7 +78,7 @@ const formatReleaseDate = (value: unknown, locale: string, compact: boolean) => }; const Item = ({ item, options, isAdvanced, width, height }: ItemProps) => { - const locale = useCurrentLocale(); + const locale = useCurrentIntlLocale(); const t = useI18n(); const length = formatLength(item.length, item.type, t); const isCompact = !isAdvanced && (width < 340 || height < 180); diff --git a/packages/widgets/src/smart-home/entity-state/component.tsx b/packages/widgets/src/smart-home/entity-state/component.tsx index 6aadfba46..385d27205 100644 --- a/packages/widgets/src/smart-home/entity-state/component.tsx +++ b/packages/widgets/src/smart-home/entity-state/component.tsx @@ -7,7 +7,7 @@ import { IconBinaryTree } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useIntegrationsWithInteractAccess } from "@homarr/auth/client"; import { useRegisterSpotlightContextActions } from "@homarr/spotlight"; -import { useCurrentLocale, useI18n } from "@homarr/translation/client"; +import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../../definition"; @@ -20,7 +20,7 @@ export default function SmartHomeEntityStateWidget({ displayMode, }: WidgetComponentProps<"smartHome-entityState">) { const t = useI18n(); - const locale = useCurrentLocale(); + const locale = useCurrentIntlLocale(); // It will always have at least one integration as otherwise the NoIntegrationSelectedError would be thrown in item-content.tsx // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const integrationId = integrationIds[0]!; diff --git a/packages/widgets/src/speedtest-tracker/latest-result.tsx b/packages/widgets/src/speedtest-tracker/latest-result.tsx index d2851aa43..968db8bbf 100644 --- a/packages/widgets/src/speedtest-tracker/latest-result.tsx +++ b/packages/widgets/src/speedtest-tracker/latest-result.tsx @@ -4,7 +4,7 @@ import { Group, SimpleGrid, Stack, Text } from "@mantine/core"; import { IconArrowDown, IconArrowUp, IconCircleCheck, IconCircleX, IconWaveSine } from "@tabler/icons-react"; import type { SpeedtestTrackerResult } from "@homarr/integrations/types"; -import { useCurrentLocale, useScopedI18n } from "@homarr/translation/client"; +import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client"; import { formatResultSpeed } from "./helpers"; import { SectionLabel } from "./section-label"; @@ -12,7 +12,7 @@ import { SpeedStatCard } from "./speed-stat-card"; export function LatestResultSection({ result, width = 500 }: { result: SpeedtestTrackerResult; width?: number }) { const t = useScopedI18n("widget.speedtestTracker"); - const locale = useCurrentLocale(); + const locale = useCurrentIntlLocale(); const timestamp = Intl.DateTimeFormat(locale, { month: "short", day: "numeric", diff --git a/packages/widgets/src/vpn/component.tsx b/packages/widgets/src/vpn/component.tsx index d9526923c..399264bbe 100644 --- a/packages/widgets/src/vpn/component.tsx +++ b/packages/widgets/src/vpn/component.tsx @@ -3,7 +3,7 @@ import { Center, Flex, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; -import { useCurrentLocale, useScopedI18n } from "@homarr/translation/client"; +import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; import { VpnIntegrationCard } from "./vpn-card"; @@ -21,7 +21,7 @@ export default function VpnWidget({ }); const t = useScopedI18n("widget.vpn"); const tCommon = useScopedI18n("common"); - const locale = useCurrentLocale(); + const locale = useCurrentIntlLocale(); const dense = displayMode === "compact" && (width < 240 || height < 120); if (isPending || integrations.length === 0) {