mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-02 22:02:13 +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,
|
||||
} from "@/components/ui/select";
|
||||
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 { cn } from "@/lib/utils";
|
||||
|
||||
@@ -89,7 +89,11 @@ export function AppearanceSettingsPage() {
|
||||
>
|
||||
<Select
|
||||
value={locale}
|
||||
onValueChange={(value) => changeLocale(value as Locale)}
|
||||
onValueChange={(value) => {
|
||||
if (isLocale(value)) {
|
||||
changeLocale(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[220px]">
|
||||
<SelectValue />
|
||||
|
||||
@@ -7,7 +7,13 @@ import { getLocaleFromCookie, setLocaleInCookie } from "./cookies";
|
||||
import { enUS } from "./locales/en-US";
|
||||
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> = {
|
||||
"en-US": enUS,
|
||||
@@ -17,7 +23,7 @@ const translations: Record<Locale, Translations> = {
|
||||
export function useI18n() {
|
||||
const { locale, setLocale } = useI18nContext();
|
||||
|
||||
const t = translations[locale];
|
||||
const t = translations[locale] ?? translations[DEFAULT_LOCALE];
|
||||
|
||||
const changeLocale = (newLocale: Locale) => {
|
||||
setLocale(newLocale);
|
||||
@@ -26,12 +32,19 @@ export function useI18n() {
|
||||
|
||||
// Initialize locale on mount
|
||||
useEffect(() => {
|
||||
const saved = getLocaleFromCookie() as Locale | null;
|
||||
if (!saved) {
|
||||
const detected = detectLocale();
|
||||
setLocale(detected);
|
||||
setLocaleInCookie(detected);
|
||||
const saved = getLocaleFromCookie();
|
||||
if (saved) {
|
||||
const normalizedSaved = normalizeLocale(saved);
|
||||
setLocale(normalizedSaved);
|
||||
if (saved !== normalizedSaved) {
|
||||
setLocaleInCookie(normalizedSaved);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const detected = detectLocale();
|
||||
setLocale(detected);
|
||||
setLocaleInCookie(detected);
|
||||
}, [setLocale]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
export { enUS } from "./locales/en-US";
|
||||
export { zhCN } from "./locales/zh-CN";
|
||||
export type { Translations } from "./locales/types";
|
||||
|
||||
export type Locale = "en-US" | "zh-CN";
|
||||
|
||||
// Helper function to detect browser locale
|
||||
export function detectLocale(): Locale {
|
||||
if (typeof window === "undefined") {
|
||||
return "en-US";
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
export {
|
||||
DEFAULT_LOCALE,
|
||||
SUPPORTED_LOCALES,
|
||||
detectLocale,
|
||||
isLocale,
|
||||
normalizeLocale,
|
||||
} from "./locale";
|
||||
export type { Locale } from "./locale";
|
||||
|
||||
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";
|
||||
|
||||
export type Locale = "en-US" | "zh-CN";
|
||||
import { normalizeLocale, type Locale } from "./locale";
|
||||
|
||||
export async function detectLocaleServer(): Promise<Locale> {
|
||||
const cookieStore = await cookies();
|
||||
const locale = cookieStore.get("locale")?.value ?? "en-US";
|
||||
return locale as Locale;
|
||||
let locale = cookieStore.get("locale")?.value;
|
||||
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