Skip to content

Commit fb4d9e5

Browse files
authored
Merge pull request #25 from DEEIX-AI/locale_lang
feat: use browser locale and optional cookie persistence
2 parents f78d44b + 53b7b75 commit fb4d9e5

3 files changed

Lines changed: 54 additions & 12 deletions

File tree

frontend/i18n/app-i18n-provider.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import * as React from "react";
44
import { NextIntlClientProvider } from "next-intl";
55

6-
import { DEFAULT_LOCALE, LOCALE_COOKIE_NAME, normalizeAppLocale, type AppLocale } from "@/i18n/config";
6+
import { DEFAULT_LOCALE, LOCALE_COOKIE_NAME, normalizeAppLocale, resolveBrowserLocale, type AppLocale } from "@/i18n/config";
77
import { DEFAULT_MESSAGES, loadLocaleMessages, type AppMessages } from "@/i18n/messages";
88

99
type AppI18nContextValue = {
@@ -13,20 +13,27 @@ type AppI18nContextValue = {
1313

1414
const AppI18nContext = React.createContext<AppI18nContextValue | null>(null);
1515

16-
function readLocaleCookie(): AppLocale {
16+
function readLocaleCookie(): AppLocale | null {
1717
if (typeof document === "undefined") {
18-
return DEFAULT_LOCALE;
18+
return null;
1919
}
2020
const raw = document.cookie
2121
.split(";")
2222
.map((part) => part.trim())
2323
.find((part) => part.startsWith(`${LOCALE_COOKIE_NAME}=`));
2424
if (!raw) {
25-
return DEFAULT_LOCALE;
25+
return null;
2626
}
2727
return normalizeAppLocale(decodeURIComponent(raw.slice(LOCALE_COOKIE_NAME.length + 1)));
2828
}
2929

30+
function readBrowserLocale(): AppLocale {
31+
if (typeof navigator === "undefined") {
32+
return DEFAULT_LOCALE;
33+
}
34+
return resolveBrowserLocale(navigator.languages?.length ? navigator.languages : [navigator.language]);
35+
}
36+
3037
function writeLocaleCookie(locale: AppLocale): void {
3138
if (typeof document === "undefined") {
3239
return;
@@ -46,10 +53,12 @@ export function AppI18nProvider({ children }: { children: React.ReactNode }) {
4653
const [messages, setMessages] = React.useState<AppMessages>(DEFAULT_MESSAGES);
4754
const localeRef = React.useRef<AppLocale>(DEFAULT_LOCALE);
4855

49-
const setLocale = React.useCallback(async (nextLocale: AppLocale) => {
56+
const applyLocale = React.useCallback(async (nextLocale: AppLocale, persist: boolean) => {
5057
const normalized = normalizeAppLocale(nextLocale);
5158
if (normalized === localeRef.current) {
52-
writeLocaleCookie(normalized);
59+
if (persist) {
60+
writeLocaleCookie(normalized);
61+
}
5362
applyDocumentLocale(normalized);
5463
return;
5564
}
@@ -58,13 +67,20 @@ export function AppI18nProvider({ children }: { children: React.ReactNode }) {
5867
localeRef.current = normalized;
5968
setLocaleState(normalized);
6069
setMessages(nextMessages);
61-
writeLocaleCookie(normalized);
70+
if (persist) {
71+
writeLocaleCookie(normalized);
72+
}
6273
applyDocumentLocale(normalized);
6374
}, []);
6475

76+
const setLocale = React.useCallback(async (nextLocale: AppLocale) => {
77+
await applyLocale(nextLocale, true);
78+
}, [applyLocale]);
79+
6580
React.useEffect(() => {
66-
void setLocale(readLocaleCookie());
67-
}, [setLocale]);
81+
const cookieLocale = readLocaleCookie();
82+
void applyLocale(cookieLocale ?? readBrowserLocale(), cookieLocale !== null);
83+
}, [applyLocale]);
6884

6985
const value = React.useMemo<AppI18nContextValue>(
7086
() => ({

frontend/i18n/config.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,26 @@ export const APP_LOCALE_LABELS: Record<AppLocale, string> = {
1212

1313
export function normalizeAppLocale(value: string | null | undefined): AppLocale {
1414
const normalized = String(value ?? "").trim();
15-
return APP_LOCALES.includes(normalized as AppLocale) ? (normalized as AppLocale) : DEFAULT_LOCALE;
15+
const canonical = normalized.replace("_", "-");
16+
const lower = canonical.toLowerCase();
17+
if (lower === "zh" || lower.startsWith("zh-")) {
18+
return "zh-CN";
19+
}
20+
if (lower === "en" || lower.startsWith("en-")) {
21+
return "en-US";
22+
}
23+
return APP_LOCALES.includes(canonical as AppLocale) ? (canonical as AppLocale) : DEFAULT_LOCALE;
24+
}
25+
26+
export function resolveBrowserLocale(languages: readonly string[] | undefined): AppLocale {
27+
for (const language of languages ?? []) {
28+
const normalized = String(language ?? "").trim().toLowerCase().replace("_", "-");
29+
if (normalized === "zh" || normalized.startsWith("zh-")) {
30+
return "zh-CN";
31+
}
32+
if (normalized === "en" || normalized.startsWith("en-")) {
33+
return "en-US";
34+
}
35+
}
36+
return DEFAULT_LOCALE;
1637
}

frontend/i18n/resolve-error-message.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import enErrors from "@/i18n/messages/en-US/errors.json";
22
import zhErrors from "@/i18n/messages/zh-CN/errors.json";
3-
import { DEFAULT_LOCALE, LOCALE_COOKIE_NAME, normalizeAppLocale, type AppLocale } from "@/i18n/config";
3+
import { DEFAULT_LOCALE, LOCALE_COOKIE_NAME, normalizeAppLocale, resolveBrowserLocale, type AppLocale } from "@/i18n/config";
44
import { ApiError } from "@/shared/api/http-client";
55

66
const ERROR_MESSAGES: Record<AppLocale, unknown> = {
@@ -182,7 +182,12 @@ function readClientLocale(): AppLocale {
182182
.map((item) => item.trim())
183183
.find((item) => item.startsWith(`${LOCALE_COOKIE_NAME}=`))
184184
?.slice(LOCALE_COOKIE_NAME.length + 1);
185-
return normalizeAppLocale(cookieValue ? decodeURIComponent(cookieValue) : undefined);
185+
if (cookieValue) {
186+
return normalizeAppLocale(decodeURIComponent(cookieValue));
187+
}
188+
return typeof navigator === "undefined"
189+
? DEFAULT_LOCALE
190+
: resolveBrowserLocale(navigator.languages?.length ? navigator.languages : [navigator.language]);
186191
}
187192

188193
function lookupErrorMessage(locale: AppLocale, errorCode: string): string | undefined {

0 commit comments

Comments
 (0)