Quellcode durchsuchen

added seesion and button for the en/chinese locale change

fix負數倉
PC-20260115JRSN\Administrator vor 3 Wochen
Ursprung
Commit
b4054b88aa
7 geänderte Dateien mit 116 neuen und 9 gelöschten Zeilen
  1. +2
    -0
      src/components/AppBar/AppBar.tsx
  2. +60
    -0
      src/components/AppBar/LanguageSwitcher.tsx
  3. +17
    -1
      src/config/authConfig.ts
  4. +1
    -0
      src/i18n/en/common.json
  5. +12
    -8
      src/i18n/index.tsx
  6. +23
    -0
      src/i18n/locale.ts
  7. +1
    -0
      src/i18n/zh/common.json

+ 2
- 0
src/components/AppBar/AppBar.tsx Datei anzeigen

@@ -1,6 +1,7 @@
import MUIAppBar from "@mui/material/AppBar"; import MUIAppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
import React from "react"; import React from "react";
import LanguageSwitcher from "./LanguageSwitcher";
import Profile from "./Profile"; import Profile from "./Profile";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import NavigationToggle from "./NavigationToggle"; import NavigationToggle from "./NavigationToggle";
@@ -35,6 +36,7 @@ const AppBar: React.FC<AppBarProps> = ({ avatarImageSrc, profileName }) => {
gap: 1, gap: 1,
}} }}
> >
<LanguageSwitcher />
<Profile <Profile
avatarImageSrc={avatarImageSrc} avatarImageSrc={avatarImageSrc}
profileName={profileName} profileName={profileName}


+ 60
- 0
src/components/AppBar/LanguageSwitcher.tsx Datei anzeigen

@@ -0,0 +1,60 @@
"use client";

import React, { useRef } from "react";
import ToggleButton from "@mui/material/ToggleButton";
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { useTranslation } from "react-i18next";
import {
type AppLanguage,
isAppLanguage,
setLanguageCookie,
} from "@/i18n/locale";

const LanguageSwitcher: React.FC = () => {
const { i18n, t } = useTranslation("common");
const { update } = useSession();
const router = useRouter();
const inFlightRef = useRef(false);

const current: AppLanguage = isAppLanguage(i18n.language) ? i18n.language : "zh";

const onChange = async (_: React.MouseEvent<HTMLElement>, next: AppLanguage | null) => {
if (!next || next === current) return;
if (inFlightRef.current) return;
inFlightRef.current = true;
try {
setLanguageCookie(next);
await update({ locale: next });
router.refresh();
} finally {
inFlightRef.current = false;
}
};

return (
<ToggleButtonGroup
exclusive
size="small"
value={current}
onChange={onChange}
aria-label={t("Language")}
sx={{
mr: 0.5,
"& .MuiToggleButton-root": {
px: 1,
py: 0.25,
fontSize: "0.75rem",
lineHeight: 1.4,
textTransform: "none",
},
}}
>
<ToggleButton value="zh">中</ToggleButton>
<ToggleButton value="en">EN</ToggleButton>
</ToggleButtonGroup>
);
};

export default LanguageSwitcher;

+ 17
- 1
src/config/authConfig.ts Datei anzeigen

@@ -12,6 +12,7 @@ declare module "next-auth" {
id?: string; id?: string;
/** JWT expiry (seconds since epoch); used to avoid redirecting to dashboard when token is expired */ /** JWT expiry (seconds since epoch); used to avoid redirecting to dashboard when token is expired */
exp?: number; exp?: number;
locale?: string;
} }


interface User { interface User {
@@ -19,6 +20,7 @@ declare module "next-auth" {
accessToken: string | null; accessToken: string | null;
refreshToken?: string; refreshToken?: string;
abilities: string[]; abilities: string[];
locale?: string;
} }
} }


@@ -28,6 +30,7 @@ declare module "next-auth/jwt" {
accessToken: string | null; accessToken: string | null;
refreshToken?: string; refreshToken?: string;
abilities: string[]; abilities: string[];
locale?: string;
} }
} }


@@ -70,13 +73,24 @@ export const authOptions: AuthOptions = {
}, },
callbacks: { callbacks: {
// Persist custom fields into the JWT token // Persist custom fields into the JWT token
async jwt({ token, user }) {
async jwt({ token, user, trigger, session }) {
// First sign-in: `user` is available // First sign-in: `user` is available
if (user) { if (user) {
token.id = user.id ?? token.sub; // fallback to sub if no id token.id = user.id ?? token.sub; // fallback to sub if no id
token.accessToken = user.accessToken; token.accessToken = user.accessToken;
token.refreshToken = user.refreshToken; token.refreshToken = user.refreshToken;
token.abilities = user.abilities ?? []; token.abilities = user.abilities ?? [];
const loginLocale = (user as { locale?: string }).locale;
if (loginLocale) {
token.locale = loginLocale;
}
}

if (trigger === "update" && session && typeof session === "object" && "locale" in session) {
const next = (session as { locale?: string }).locale;
if (next === "zh" || next === "en") {
token.locale = next;
}
} }


// On subsequent calls (token refresh, session access), user is not present // On subsequent calls (token refresh, session access), user is not present
@@ -91,6 +105,7 @@ export const authOptions: AuthOptions = {
session.refreshToken = token.refreshToken as string | undefined; session.refreshToken = token.refreshToken as string | undefined;
session.abilities = token.abilities as string[]; session.abilities = token.abilities as string[];
session.exp = token.exp as number | undefined; session.exp = token.exp as number | undefined;
session.locale = token.locale as string | undefined;


// Also add abilities to session.user for easier client-side access // Also add abilities to session.user for easier client-side access
if (session.user) { if (session.user) {
@@ -107,5 +122,6 @@ export type SessionWithTokens = Session & {
abilities: string[]; abilities: string[];
/** Backend / JWT subject — often numeric string or number */ /** Backend / JWT subject — often numeric string or number */
id?: string | number; id?: string | number;
locale?: string;
}; };
export default authOptions; export default authOptions;

+ 1
- 0
src/i18n/en/common.json Datei anzeigen

@@ -87,6 +87,7 @@
"Select Date": "選擇日期", "Select Date": "選擇日期",
"Session expired or unauthorized.": "工作階段已過期或未經授權。", "Session expired or unauthorized.": "工作階段已過期或未經授權。",
"Sign out": "Sign out", "Sign out": "Sign out",
"Language": "Language",
"Status": "狀態", "Status": "狀態",
"Stock Qty": "庫存數量", "Stock Qty": "庫存數量",
"Supporting Document": "證明文件", "Supporting Document": "證明文件",


+ 12
- 8
src/i18n/index.tsx Datei anzeigen

@@ -2,12 +2,14 @@ import { cookies, headers } from "next/headers";
import { createInstance, i18n, LanguageDetectorAsyncModule } from "i18next"; import { createInstance, i18n, LanguageDetectorAsyncModule } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend"; import resourcesToBackend from "i18next-resources-to-backend";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { authOptions } from "@/config/authConfig";
import { authOptions, SessionWithTokens } from "@/config/authConfig";
import I18nClientProvider from "./I18nClientProvider"; import I18nClientProvider from "./I18nClientProvider";
import universalLanguageDetect from "@unly/universal-language-detector"; import universalLanguageDetect from "@unly/universal-language-detector";

const FALLBACK_LANG = "zh";
const SUPPORTED_LANGUAGES = ["zh"];
import {
FALLBACK_LANG,
SUPPORTED_LANGUAGES,
normalizeAppLanguage,
} from "./locale";


export const detectLanguage = async (): Promise<string> => { export const detectLanguage = async (): Promise<string> => {
// Logic to get language preference from cookies/headers/session // Logic to get language preference from cookies/headers/session
@@ -21,11 +23,13 @@ export const detectLanguage = async (): Promise<string> => {
const headersList = headers(); const headersList = headers();
//console.time("[i18n] detectLanguage total"); //console.time("[i18n] detectLanguage total");
//console.time("[i18n] getServerSession"); //console.time("[i18n] getServerSession");
const session = await getServerSession(authOptions);
//console.timeEnd("[i18n] getServerSession");
//console.time("[i18n] universalLanguageDetect");
const session = (await getServerSession(authOptions)) as SessionWithTokens | null;
const fromSession = normalizeAppLanguage(session?.locale);
if (fromSession) {
return fromSession;
}
const lang = universalLanguageDetect({ const lang = universalLanguageDetect({
supportedLanguages: SUPPORTED_LANGUAGES,
supportedLanguages: [...SUPPORTED_LANGUAGES],
fallbackLanguage: FALLBACK_LANG, fallbackLanguage: FALLBACK_LANG,
acceptLanguageHeader: headersList.get("accept-language") || undefined, acceptLanguageHeader: headersList.get("accept-language") || undefined,
serverCookies: cookiesObj, serverCookies: cookiesObj,


+ 23
- 0
src/i18n/locale.ts Datei anzeigen

@@ -0,0 +1,23 @@
export const FALLBACK_LANG = "zh";
export const SUPPORTED_LANGUAGES = ["zh", "en"] as const;
export type AppLanguage = (typeof SUPPORTED_LANGUAGES)[number];

/** Cookie name read by `@unly/universal-language-detector`. */
export const I18N_COOKIE_NAME = "i18next";

export function isAppLanguage(value: unknown): value is AppLanguage {
return value === "zh" || value === "en";
}

export function normalizeAppLanguage(value: unknown): AppLanguage | null {
if (typeof value !== "string") return null;
const lower = value.trim().toLowerCase();
if (lower === "zh" || lower.startsWith("zh-")) return "zh";
if (lower === "en" || lower.startsWith("en-")) return "en";
return null;
}

export function setLanguageCookie(lang: AppLanguage) {
if (typeof document === "undefined") return;
document.cookie = `${I18N_COOKIE_NAME}=${lang}; Path=/; SameSite=Lax; Max-Age=31536000`;
}

+ 1
- 0
src/i18n/zh/common.json Datei anzeigen

@@ -90,6 +90,7 @@
"Select Date": "選擇日期", "Select Date": "選擇日期",
"Session expired or unauthorized.": "工作階段已過期或未經授權。", "Session expired or unauthorized.": "工作階段已過期或未經授權。",
"Sign out": "登出", "Sign out": "登出",
"Language": "語言",
"Status": "狀態", "Status": "狀態",
"Stock Qty": "庫存數量", "Stock Qty": "庫存數量",
"Supporting Document": "證明文件", "Supporting Document": "證明文件",


Laden…
Abbrechen
Speichern