110 lines
3.1 KiB
TypeScript
110 lines
3.1 KiB
TypeScript
|
|
import { useState, useEffect } from 'react';
|
|||
|
|
import { useParams, useRouter } from 'next/navigation';
|
|||
|
|
|
|||
|
|
type Locale = 'zh-CN' | 'zh-TW' | 'en';
|
|||
|
|
|
|||
|
|
interface TranslationData {
|
|||
|
|
[key: string]: any;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const translations: Record<Locale, Record<string, TranslationData>> = {
|
|||
|
|
'zh-CN': {},
|
|||
|
|
'zh-TW': {},
|
|||
|
|
en: {},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Load translation files dynamically
|
|||
|
|
const loadTranslations = async (locale: Locale, namespace: string): Promise<TranslationData> => {
|
|||
|
|
if (translations[locale][namespace]) {
|
|||
|
|
return translations[locale][namespace];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await fetch(`/locales/${locale}/${namespace}.json`);
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error(`HTTP ${response.status}`);
|
|||
|
|
}
|
|||
|
|
const data = await response.json();
|
|||
|
|
translations[locale][namespace] = data;
|
|||
|
|
return data;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`Failed to load translation: ${locale}/${namespace}`, error);
|
|||
|
|
return {};
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export const useTranslation = (namespace: string = 'common') => {
|
|||
|
|
const params = useParams();
|
|||
|
|
const router = useRouter();
|
|||
|
|
const urlLocale = params?.locale as Locale;
|
|||
|
|
|
|||
|
|
const [locale, setLocale] = useState<Locale>(urlLocale || 'zh-CN');
|
|||
|
|
const [translations, setTranslations] = useState<TranslationData>({});
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
// 如果URL中有locale参数,优先使用URL中的语言
|
|||
|
|
if (urlLocale && urlLocale !== locale) {
|
|||
|
|
setLocale(urlLocale);
|
|||
|
|
}
|
|||
|
|
}, [urlLocale, locale]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const loadData = async () => {
|
|||
|
|
setLoading(true);
|
|||
|
|
const data = await loadTranslations(locale, namespace);
|
|||
|
|
setTranslations(data);
|
|||
|
|
setLoading(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
loadData();
|
|||
|
|
}, [locale, namespace]);
|
|||
|
|
|
|||
|
|
const changeLocale = (newLocale: Locale) => {
|
|||
|
|
// 更新localStorage
|
|||
|
|
localStorage.setItem('language', newLocale);
|
|||
|
|
|
|||
|
|
// 构建新的URL路径
|
|||
|
|
const currentPath = window.location.pathname;
|
|||
|
|
const pathWithoutLocale = currentPath.replace(/^\/[^\/]+/, '');
|
|||
|
|
const newPath = `/${newLocale}${pathWithoutLocale}`;
|
|||
|
|
|
|||
|
|
// 导航到新的语言路径
|
|||
|
|
router.push(newPath);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const t = (key: string, defaultValue?: string): string => {
|
|||
|
|
const keys = key.split('.');
|
|||
|
|
let value = translations;
|
|||
|
|
|
|||
|
|
for (const k of keys) {
|
|||
|
|
if (value && typeof value === 'object' && k in value) {
|
|||
|
|
value = value[k];
|
|||
|
|
} else {
|
|||
|
|
return defaultValue || key;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return typeof value === 'string' ? value : defaultValue || key;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
t,
|
|||
|
|
locale,
|
|||
|
|
setLocale: changeLocale,
|
|||
|
|
loading,
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 语言切换工具函数
|
|||
|
|
export const getLocalizedPath = (path: string, locale: Locale): string => {
|
|||
|
|
return `/${locale}${path}`;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 验证locale是否有效
|
|||
|
|
export const isValidLocale = (locale: string): locale is Locale => {
|
|||
|
|
return ['zh-CN', 'zh-TW', 'en'].includes(locale);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export type { Locale };
|