mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-19 04:14:46 +08:00
fix(i18n): normalize locale and prevent undefined translations (#914)
* fix(i18n): guard locale input and add safe translation fallback * refactor(i18n): isolate locale utils and normalize server cookie decode --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { enUS, zhCN, type Locale } from "@/core/i18n";
|
import { enUS, isLocale, zhCN, type Locale } from "@/core/i18n";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -89,7 +89,11 @@ export function AppearanceSettingsPage() {
|
|||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
value={locale}
|
value={locale}
|
||||||
onValueChange={(value) => changeLocale(value as Locale)}
|
onValueChange={(value) => {
|
||||||
|
if (isLocale(value)) {
|
||||||
|
changeLocale(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[220px]">
|
<SelectTrigger className="w-[220px]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ import { getLocaleFromCookie, setLocaleInCookie } from "./cookies";
|
|||||||
import { enUS } from "./locales/en-US";
|
import { enUS } from "./locales/en-US";
|
||||||
import { zhCN } from "./locales/zh-CN";
|
import { zhCN } from "./locales/zh-CN";
|
||||||
|
|
||||||
import { detectLocale, type Locale, type Translations } from "./index";
|
import {
|
||||||
|
DEFAULT_LOCALE,
|
||||||
|
detectLocale,
|
||||||
|
normalizeLocale,
|
||||||
|
type Locale,
|
||||||
|
type Translations,
|
||||||
|
} from "./index";
|
||||||
|
|
||||||
const translations: Record<Locale, Translations> = {
|
const translations: Record<Locale, Translations> = {
|
||||||
"en-US": enUS,
|
"en-US": enUS,
|
||||||
@@ -17,7 +23,7 @@ const translations: Record<Locale, Translations> = {
|
|||||||
export function useI18n() {
|
export function useI18n() {
|
||||||
const { locale, setLocale } = useI18nContext();
|
const { locale, setLocale } = useI18nContext();
|
||||||
|
|
||||||
const t = translations[locale];
|
const t = translations[locale] ?? translations[DEFAULT_LOCALE];
|
||||||
|
|
||||||
const changeLocale = (newLocale: Locale) => {
|
const changeLocale = (newLocale: Locale) => {
|
||||||
setLocale(newLocale);
|
setLocale(newLocale);
|
||||||
@@ -26,12 +32,19 @@ export function useI18n() {
|
|||||||
|
|
||||||
// Initialize locale on mount
|
// Initialize locale on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saved = getLocaleFromCookie() as Locale | null;
|
const saved = getLocaleFromCookie();
|
||||||
if (!saved) {
|
if (saved) {
|
||||||
const detected = detectLocale();
|
const normalizedSaved = normalizeLocale(saved);
|
||||||
setLocale(detected);
|
setLocale(normalizedSaved);
|
||||||
setLocaleInCookie(detected);
|
if (saved !== normalizedSaved) {
|
||||||
|
setLocaleInCookie(normalizedSaved);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const detected = detectLocale();
|
||||||
|
setLocale(detected);
|
||||||
|
setLocaleInCookie(detected);
|
||||||
}, [setLocale]);
|
}, [setLocale]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,23 +1,11 @@
|
|||||||
export { enUS } from "./locales/en-US";
|
export { enUS } from "./locales/en-US";
|
||||||
export { zhCN } from "./locales/zh-CN";
|
export { zhCN } from "./locales/zh-CN";
|
||||||
export type { Translations } from "./locales/types";
|
export type { Translations } from "./locales/types";
|
||||||
|
export {
|
||||||
export type Locale = "en-US" | "zh-CN";
|
DEFAULT_LOCALE,
|
||||||
|
SUPPORTED_LOCALES,
|
||||||
// Helper function to detect browser locale
|
detectLocale,
|
||||||
export function detectLocale(): Locale {
|
isLocale,
|
||||||
if (typeof window === "undefined") {
|
normalizeLocale,
|
||||||
return "en-US";
|
} from "./locale";
|
||||||
}
|
export type { Locale } from "./locale";
|
||||||
|
|
||||||
const browserLang =
|
|
||||||
navigator.language ||
|
|
||||||
(navigator as unknown as { userLanguage: string }).userLanguage;
|
|
||||||
|
|
||||||
// Check if browser language is Chinese (zh, zh-CN, zh-TW, etc.)
|
|
||||||
if (browserLang.toLowerCase().startsWith("zh")) {
|
|
||||||
return "zh-CN";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "en-US";
|
|
||||||
}
|
|
||||||
|
|||||||
36
frontend/src/core/i18n/locale.ts
Normal file
36
frontend/src/core/i18n/locale.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export const SUPPORTED_LOCALES = ["en-US", "zh-CN"] as const;
|
||||||
|
export type Locale = (typeof SUPPORTED_LOCALES)[number];
|
||||||
|
export const DEFAULT_LOCALE: Locale = "en-US";
|
||||||
|
|
||||||
|
export function isLocale(value: string): value is Locale {
|
||||||
|
return (SUPPORTED_LOCALES as readonly string[]).includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeLocale(locale: string | null | undefined): Locale {
|
||||||
|
if (!locale) {
|
||||||
|
return DEFAULT_LOCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocale(locale)) {
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locale.toLowerCase().startsWith("zh")) {
|
||||||
|
return "zh-CN";
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_LOCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to detect browser locale
|
||||||
|
export function detectLocale(): Locale {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return DEFAULT_LOCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const browserLang =
|
||||||
|
navigator.language ||
|
||||||
|
(navigator as unknown as { userLanguage: string }).userLanguage;
|
||||||
|
|
||||||
|
return normalizeLocale(browserLang);
|
||||||
|
}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
export type Locale = "en-US" | "zh-CN";
|
import { normalizeLocale, type Locale } from "./locale";
|
||||||
|
|
||||||
export async function detectLocaleServer(): Promise<Locale> {
|
export async function detectLocaleServer(): Promise<Locale> {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const locale = cookieStore.get("locale")?.value ?? "en-US";
|
let locale = cookieStore.get("locale")?.value;
|
||||||
return locale as Locale;
|
if (locale !== undefined) {
|
||||||
|
try {
|
||||||
|
locale = decodeURIComponent(locale);
|
||||||
|
} catch {
|
||||||
|
// Keep raw cookie value when decoding fails.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeLocale(locale);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user