281 lines
7.3 KiB
JavaScript
281 lines
7.3 KiB
JavaScript
/**
|
||
* 国际化 Hook
|
||
* 提供语言切换和文本翻译功能
|
||
*/
|
||
|
||
import { ref, watch } from 'vue'
|
||
import { useI18n as useVueI18n } from 'vue-i18n'
|
||
import { useAppStore } from '../stores'
|
||
|
||
// 支持的语言列表
|
||
export const SUPPORTED_LOCALES = [
|
||
{
|
||
code: 'zh-CN',
|
||
name: '中文',
|
||
nativeName: '中文',
|
||
flag: '🇨🇳'
|
||
},
|
||
{
|
||
code: 'en-US',
|
||
name: 'English',
|
||
nativeName: 'English',
|
||
flag: '🇺🇸'
|
||
}
|
||
]
|
||
|
||
// 检测浏览器语言
|
||
function detectBrowserLocale() {
|
||
const browserLang = navigator.language || navigator.languages?.[0]
|
||
|
||
if (browserLang?.startsWith('zh')) {
|
||
return 'zh-CN'
|
||
} else if (browserLang?.startsWith('en')) {
|
||
return 'en-US'
|
||
}
|
||
|
||
return 'en-US' // 默认语言
|
||
}
|
||
|
||
// 格式化日期
|
||
export function formatDate(date, options = {}) {
|
||
const defaultOptions = {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric'
|
||
}
|
||
|
||
const { locale } = useI18n()
|
||
return new Intl.DateTimeFormat(locale.value, { ...defaultOptions, ...options }).format(new Date(date))
|
||
}
|
||
|
||
// 格式化数字
|
||
export function formatNumber(number, options = {}) {
|
||
const defaultOptions = {
|
||
minimumFractionDigits: 0,
|
||
maximumFractionDigits: 2
|
||
}
|
||
|
||
const { locale } = useI18n()
|
||
return new Intl.NumberFormat(locale.value, { ...defaultOptions, ...options }).format(number)
|
||
}
|
||
|
||
// 格式化货币
|
||
export function formatCurrency(amount, currency = 'USD') {
|
||
const { locale } = useI18n()
|
||
return new Intl.NumberFormat(locale.value, {
|
||
style: 'currency',
|
||
currency
|
||
}).format(amount)
|
||
}
|
||
|
||
// 格式化相对时间
|
||
export function formatRelativeTime(date) {
|
||
const now = new Date()
|
||
const targetDate = new Date(date)
|
||
const diffInSeconds = Math.floor((now - targetDate) / 1000)
|
||
|
||
const rtf = new Intl.RelativeTimeFormat(useI18n().locale.value, { numeric: 'auto' })
|
||
|
||
const units = [
|
||
{ unit: 'year', seconds: 31536000 },
|
||
{ unit: 'month', seconds: 2592000 },
|
||
{ unit: 'week', seconds: 604800 },
|
||
{ unit: 'day', seconds: 86400 },
|
||
{ unit: 'hour', seconds: 3600 },
|
||
{ unit: 'minute', seconds: 60 },
|
||
{ unit: 'second', seconds: 1 }
|
||
]
|
||
|
||
for (const { unit, seconds } of units) {
|
||
const interval = Math.floor(diffInSeconds / seconds)
|
||
if (interval >= 1) {
|
||
return rtf.format(-interval, unit)
|
||
}
|
||
}
|
||
|
||
return rtf.format(0, 'second')
|
||
}
|
||
|
||
export function useI18nExt() {
|
||
const { locale: i18nLocale, messages, t, d } = useVueI18n()
|
||
const appStore = useAppStore()
|
||
|
||
// 当前语言
|
||
const currentLocale = ref(i18nLocale.value)
|
||
|
||
// 设置语言
|
||
const setLocale = (localeCode, saveToStorage = true) => {
|
||
// 验证语言有效性
|
||
const isValidLocale = SUPPORTED_LOCALES.some(lang => lang.code === localeCode)
|
||
if (!isValidLocale) {
|
||
console.warn(`Invalid locale: ${localeCode}, falling back to 'en-US'`)
|
||
localeCode = 'en-US'
|
||
}
|
||
|
||
// 更新 i18n 语言
|
||
i18nLocale.value = localeCode
|
||
|
||
// 更新当前语言状态
|
||
currentLocale.value = localeCode
|
||
|
||
// 只保存到 localStorage,store 由 App.vue 统一管理
|
||
if (saveToStorage) {
|
||
localStorage.setItem('app-locale', localeCode)
|
||
}
|
||
|
||
// 更新 HTML lang 属性
|
||
document.documentElement.lang = localeCode
|
||
|
||
// 不直接调用store,避免循环
|
||
|
||
// 更新日期格式
|
||
document.documentElement.dir = getTextDirection(localeCode)
|
||
}
|
||
|
||
// 获取文本方向
|
||
const getTextDirection = (localeCode) => {
|
||
// 大多数语言从左到右,少数语言从右到左
|
||
const rtlLocales = ['ar', 'he', 'fa', 'ur']
|
||
return rtlLocales.some(lang => localeCode.startsWith(lang)) ? 'rtl' : 'ltr'
|
||
}
|
||
|
||
// 获取语言显示名称
|
||
const getLocaleName = (localeCode) => {
|
||
const locale = SUPPORTED_LOCALES.find(lang => lang.code === localeCode)
|
||
return locale ? `${locale.flag} ${locale.nativeName}` : localeCode
|
||
}
|
||
|
||
// 获取所有支持的语言
|
||
const getSupportedLocales = () => {
|
||
return SUPPORTED_LOCALES
|
||
}
|
||
|
||
// 检查是否为 RTL 语言
|
||
const isRTL = () => {
|
||
return getTextDirection(currentLocale.value) === 'rtl'
|
||
}
|
||
|
||
// 切换到下一个语言
|
||
const toggleLocale = () => {
|
||
const currentIndex = SUPPORTED_LOCALES.findIndex(lang => lang.code === currentLocale.value)
|
||
const nextIndex = (currentIndex + 1) % SUPPORTED_LOCALES.length
|
||
const nextLocale = SUPPORTED_LOCALES[nextIndex]
|
||
|
||
setLocale(nextLocale.code)
|
||
}
|
||
|
||
// 动态加载语言包
|
||
const loadLocaleMessages = async (localeCode) => {
|
||
try {
|
||
// 这里可以动态加载语言包文件
|
||
// const messages = await import(`../locales/lang/${localeCode}.js`)
|
||
// i18n.global.setLocaleMessage(localeCode, messages.default)
|
||
|
||
console.log(`Loading messages for locale: ${localeCode}`)
|
||
} catch (error) {
|
||
console.error(`Failed to load messages for locale: ${localeCode}`, error)
|
||
}
|
||
}
|
||
|
||
// 翻译函数扩展
|
||
const translate = (key, params = {}) => {
|
||
let translation = t(key)
|
||
|
||
// 处理参数替换
|
||
if (typeof params === 'object' && Object.keys(params).length > 0) {
|
||
Object.keys(params).forEach(param => {
|
||
translation = translation.replace(`{${param}}`, params[param])
|
||
})
|
||
}
|
||
|
||
return translation
|
||
}
|
||
|
||
// 复数化处理
|
||
const pluralize = (key, count) => {
|
||
if (count === 0) {
|
||
return t(`${key}.zero`)
|
||
} else if (count === 1) {
|
||
return t(`${key}.one`)
|
||
} else if (count > 1 && count <= 10) {
|
||
return t(`${key}.few`)
|
||
} else {
|
||
return t(`${key}.other`)
|
||
}
|
||
}
|
||
|
||
// 初始化语言设置
|
||
const initLocale = () => {
|
||
// 从 localStorage 读取保存的语言设置
|
||
const savedLocale = localStorage.getItem('app-locale')
|
||
|
||
// 从 store 获取语言设置
|
||
const storeLocale = appStore.locale
|
||
|
||
// 优先级:store > localStorage > 浏览器检测
|
||
const initialLocale = storeLocale || savedLocale || detectBrowserLocale()
|
||
|
||
// 设置初始语言
|
||
setLocale(initialLocale, false)
|
||
|
||
// 移除所有监听器,避免循环调用
|
||
// store 和 i18n 独立工作,避免相互干扰
|
||
}
|
||
|
||
return {
|
||
locale: currentLocale,
|
||
setLocale,
|
||
getLocaleName,
|
||
getSupportedLocales,
|
||
isRTL,
|
||
toggleLocale,
|
||
loadLocaleMessages,
|
||
translate,
|
||
pluralize,
|
||
formatDate,
|
||
formatNumber,
|
||
formatCurrency,
|
||
formatRelativeTime,
|
||
initLocale
|
||
}
|
||
}
|
||
|
||
// 简化的导出供页面组件使用
|
||
export function useI18n() {
|
||
const { locale: i18nLocale, t } = useVueI18n()
|
||
const appStore = useAppStore()
|
||
|
||
// 当前语言
|
||
const currentLocale = ref(i18nLocale.value)
|
||
|
||
// 设置语言
|
||
const setLocale = (localeCode, saveToStorage = true) => {
|
||
// 验证语言有效性
|
||
const isValidLocale = SUPPORTED_LOCALES.some(lang => lang.code === localeCode)
|
||
if (!isValidLocale) {
|
||
console.warn(`Invalid locale: ${localeCode}, falling back to 'en-US'`)
|
||
localeCode = 'en-US'
|
||
}
|
||
|
||
// 更新 i18n 语言
|
||
i18nLocale.value = localeCode
|
||
|
||
// 更新当前语言状态
|
||
currentLocale.value = localeCode
|
||
|
||
// 只保存到 localStorage,store 由 App.vue 统一管理
|
||
if (saveToStorage) {
|
||
localStorage.setItem('app-locale', localeCode)
|
||
}
|
||
|
||
// 更新 HTML lang 属性
|
||
document.documentElement.lang = localeCode
|
||
}
|
||
|
||
return {
|
||
currentLocale,
|
||
locales: SUPPORTED_LOCALES,
|
||
t,
|
||
setLocale
|
||
}
|
||
} |