update
Some checks failed
deploy / deploy (push) Has been cancelled

This commit is contained in:
toom1996
2026-08-16 00:56:59 +08:00
parent 31cb7a125a
commit 781242ede0
26 changed files with 1814 additions and 1214 deletions

36
src/i18n/config.ts Normal file
View File

@ -0,0 +1,36 @@
// i18n 核心配置:语言列表、默认语言、URL 前缀规则
// 改这里即可增减语言;新增语言记得同步 src/i18n/dictionary.ts 里的每个 key(单文件聚合多语言)。
/** 默认语言(无前缀时使用的语言,如 /shows)。 */
export const DEFAULT_LOCALE = 'en' as const;
/** 支持的语言列表。URL 前缀用的就是这些值,例如 /cn/shows 的 `cn`。 */
export const LOCALES = ['en', 'cn'] as const;
export type Locale = (typeof LOCALES)[number];
/**
* 默认语言是否带 URL 前缀:
* - false → 英文在根路径(/shows),中文在 /cn/shows(本项目采用)
* - true → 所有语言都带前缀(/en/shows、/cn/shows)
*/
export const PREFIX_DEFAULT_LOCALE = false;
/** 语言显示名(用于语言切换器等 UI)。 */
export const LOCALE_NAMES: Record<Locale, string> = {
en: 'English',
cn: '中文',
};
/** 浏览器 Accept-Language / navigator.language → 本项目支持的语言。 */
export const BROWSER_LOCALE_MAP: Record<string, Locale> = {
'zh': 'cn',
'zh-cn': 'cn',
'zh-tw': 'cn',
'zh-hk': 'cn',
'en': 'en',
};
/** 判断一个路径段是不是已支持的语言前缀。 */
export function isLocaleSegment(seg: string | undefined): seg is Locale {
return !!seg && (LOCALES as readonly string[]).includes(seg);
}

56
src/i18n/dictionary.ts Normal file
View File

@ -0,0 +1,56 @@
import { DEFAULT_LOCALE, type Locale } from './config';
/**
* 单文件翻译字典。
*
* 设计(按你的要求):
* - key 就是「英文原文」,直接当文案用,无需额外维护英文副本;
* - value 只存「非默认语言」的翻译(本项目默认语言是 en,所以只存 cn 等);
* - 英文环境调用 t(key) 直接返回 key 本身;
* - 其它语言从 value 里取对应翻译,取不到就回退英文(开发环境会告警,方便发现漏翻)。
*
* 新增一条:直接加一行 `'英文原文': { cn: '中文' }` 即可;
* 当前是英文时根本不需要在字典里登记,写了 t('Nice day!') 就能渲染 "Nice day!"。
*/
// 除默认语言外的所有语言(如 'cn')。加新语言(ja 等)时这里会自动包含。
type OtherLocale = Exclude<Locale, typeof DEFAULT_LOCALE>;
type TranslationEntry = Partial<Record<OtherLocale, string>>;
export const dict: Record<string, TranslationEntry> = {
Portfolio: { cn: '作品集' },
'All Shows': { cn: '全部走秀' },
'Latest Projects': { cn: '最新项目' },
About: { cn: '关于' },
'Contact Us': { cn: '联系我们' },
Login: { cn: '登录' },
Signup: { cn: '注册' },
Contact: { cn: '联系' },
'Back to top': { cn: '回到顶部' },
'© 2026 PORTFOLIO STUDIO. ALL RIGHTS RESERVED.': { cn: '© 2026 PORTFOLIO STUDIO. 保留所有权利。' },
Previous: { cn: '上一组' },
Next: { cn: '下一组' },
};
/** 把 {name} 形式的占位符替换成参数值。 */
function interpolate(text: string, params?: Record<string, string | number>): string {
if (!params) return text;
return text.replace(/\{(\w+)\}/g, (_, k) => (k in params ? String(params[k]) : `{${k}}`));
}
/**
* 取翻译:
* - 当前是默认语言(en)→ 直接返回 key(英文原文);
* - 其它语言 → 优先取字典里的翻译,缺失则回退英文并返回 key(不会白屏)。
*/
export function translate(key: string, locale: Locale, params?: Record<string, string | number>): string {
if (locale === DEFAULT_LOCALE) return interpolate(key, params);
const localized = dict[key]?.[locale as OtherLocale];
if (localized) return interpolate(localized, params);
if (import.meta.env.DEV) {
console.warn(`[i18n] 缺少 "${locale}" 翻译:"${key}",已回退英文`);
}
return interpolate(key, params);
}

49
src/i18n/utils.ts Normal file
View File

@ -0,0 +1,49 @@
import {
DEFAULT_LOCALE,
LOCALES,
PREFIX_DEFAULT_LOCALE,
isLocaleSegment,
type Locale,
} from './config';
import { translate } from './dictionary';
/**
* 从当前 URL 解析语言:
* - 路径首段若是支持的语言前缀(如 /cn/shows → cn)则用之;
* - 否则回退到默认语言(en)。
*/
export function getLocaleFromUrl(url: URL | string): Locale {
const pathname = typeof url === 'string' ? url : url.pathname;
const seg = pathname.split('/').filter(Boolean)[0];
return isLocaleSegment(seg) ? seg : DEFAULT_LOCALE;
}
/**
* 生成带语言前缀的链接。
* - en(默认且 PREFIX_DEFAULT_LOCALE=false)→ /shows
* - cn → /cn/shows
* 第二个参数可指定目标语言,方便做语言切换器。
*/
export function useTranslatedPath(locale: Locale) {
return (path: string, targetLocale: Locale = locale): string => {
const clean = path.startsWith('/') ? path : `/${path}`;
if (targetLocale === DEFAULT_LOCALE && !PREFIX_DEFAULT_LOCALE) return clean;
return `/${targetLocale}${clean}`;
};
}
/**
* 取翻译函数 t():key 即英文原文,按当前语言自动匹配翻译。
* - 英文环境:t('All Shows') 直接返回 'All Shows'(key 本身)
* - 中文环境:t('All Shows') 返回字典里的 '全部走秀'(取不到则回退英文)
* - 支持 {name} 形式的插值参数。
*/
export function useTranslations(locale: Locale) {
function t(key: string, params?: Record<string, string | number>): string {
return translate(key, locale, params);
}
return { t, locale };
}
export { LOCALES, DEFAULT_LOCALE };
export type { Locale };