将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。 - 新增 utils/i18n.js 国际化工具函数 - 更新 store/$t.mixin.js 翻译混入逻辑 - 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
231 lines
7.1 KiB
JavaScript
231 lines
7.1 KiB
JavaScript
import store from '@/store'
|
||
import globalMessages from '@/locales/messages'
|
||
|
||
export const ZH_LOCALE = 'zh-Hans'
|
||
export const EN_LOCALE = 'en'
|
||
export const DEFAULT_LOCALE = ZH_LOCALE
|
||
|
||
function safeReadStorageLocale() {
|
||
try {
|
||
const lifeData = uni.getStorageSync('lifeData') || {}
|
||
return lifeData.vuex_locale || ''
|
||
} catch (error) {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
export function normalizeLocale(locale) {
|
||
const normalized = String(locale || '').trim().toLowerCase()
|
||
if (!normalized) {
|
||
return DEFAULT_LOCALE
|
||
}
|
||
if (normalized === 'en' || normalized === 'en-us' || normalized === 'en_us') {
|
||
return EN_LOCALE
|
||
}
|
||
if (normalized === 'zh' || normalized === 'zh-cn' || normalized === 'zh_cn' || normalized === 'zh-hans') {
|
||
return ZH_LOCALE
|
||
}
|
||
return normalized.startsWith('en') ? EN_LOCALE : ZH_LOCALE
|
||
}
|
||
|
||
export function getLocaleShort(locale) {
|
||
return normalizeLocale(locale) === EN_LOCALE ? 'en' : 'zh'
|
||
}
|
||
|
||
export function getCurrentLocale() {
|
||
const stateLocale = store && store.state ? store.state.vuex_locale : ''
|
||
return normalizeLocale(stateLocale || safeReadStorageLocale())
|
||
}
|
||
|
||
export function getCurrentLanguage() {
|
||
return getLocaleShort(getCurrentLocale())
|
||
}
|
||
|
||
export function getNextLocale(locale) {
|
||
return normalizeLocale(locale) === EN_LOCALE ? ZH_LOCALE : EN_LOCALE
|
||
}
|
||
|
||
export function resolveLanguageHeader(locale) {
|
||
return getLocaleShort(locale) === 'en' ? 'en-US' : 'zh-CN'
|
||
}
|
||
|
||
export function formatI18nText(template, params = {}) {
|
||
return String(template || '').replace(/\{(\w+)\}/g, (match, key) => {
|
||
return Object.prototype.hasOwnProperty.call(params, key) ? params[key] : match
|
||
})
|
||
}
|
||
|
||
function getMessageText(messages = {}, locale, key) {
|
||
const shortLocale = getLocaleShort(locale)
|
||
const fallbackMessages = messages.zh || messages[ZH_LOCALE] || {}
|
||
const scopedMessages = messages[shortLocale] || messages[normalizeLocale(locale)] || fallbackMessages
|
||
if (scopedMessages[key] !== undefined) {
|
||
return scopedMessages[key]
|
||
}
|
||
return fallbackMessages[key]
|
||
}
|
||
|
||
export function translateMessages(messages = {}, locale, key, params = {}) {
|
||
const text = getMessageText(messages, locale, key)
|
||
return formatI18nText(text !== undefined ? text : key, params)
|
||
}
|
||
|
||
export function getMessageScope(messages = {}, namespace = '') {
|
||
if (!namespace) {
|
||
return messages
|
||
}
|
||
return String(namespace)
|
||
.split('.')
|
||
.filter(Boolean)
|
||
.reduce((current, segment) => {
|
||
if (!current || typeof current !== 'object') {
|
||
return undefined
|
||
}
|
||
return current[segment]
|
||
}, messages)
|
||
}
|
||
|
||
export function getGlobalMessages(namespace = '') {
|
||
if (!namespace) {
|
||
return globalMessages
|
||
}
|
||
const zhScope = getMessageScope(globalMessages.zh || globalMessages[ZH_LOCALE] || {}, namespace)
|
||
const enScope = getMessageScope(globalMessages.en || globalMessages[EN_LOCALE] || {}, namespace)
|
||
|
||
if (!zhScope && !enScope) {
|
||
return undefined
|
||
}
|
||
|
||
return {
|
||
zh: zhScope || {},
|
||
en: enScope || {}
|
||
}
|
||
}
|
||
|
||
export function translateGlobalMessages(namespace, locale, key, params = {}) {
|
||
const scopedMessages = getGlobalMessages(namespace)
|
||
if (!scopedMessages || typeof scopedMessages !== 'object') {
|
||
return key
|
||
}
|
||
return translateMessages(scopedMessages, locale, key, params)
|
||
}
|
||
|
||
export function getGlobalMessage(namespace, locale, key, params = {}) {
|
||
const scopedMessages = getGlobalMessages(namespace)
|
||
if (!scopedMessages || typeof scopedMessages !== 'object') {
|
||
return ''
|
||
}
|
||
const text = getMessageText(scopedMessages, locale, key)
|
||
return text === undefined ? '' : formatI18nText(text, params)
|
||
}
|
||
|
||
export function pickLocalizedText(options = {}) {
|
||
const locale = getLocaleShort(options.locale)
|
||
const zhText = options.zh !== undefined ? options.zh : ''
|
||
const enText = options.en !== undefined ? options.en : zhText
|
||
if (locale === 'en') {
|
||
return enText || options.fallback || zhText || ''
|
||
}
|
||
return zhText || options.fallback || enText || ''
|
||
}
|
||
|
||
// === 派生扁平字典(中文 → 英文)===
|
||
// 从 globalMessages 的 zh/en 双套结构反向派生 {'中文':'英文'} map,
|
||
// 供 translateText / $tr 使用。模块加载后懒加载、进程内缓存。
|
||
let __flatDictionary = null
|
||
let __dictConflicts = null
|
||
|
||
// 手动裁决的冲突项:键为中文,值为优先采用的英文。
|
||
// 当同一中文在 messages.js 不同位置有不同英文译法时,在此指定通用译法;
|
||
// 强上下文相关、无法单一裁决的条目,组件内保留 $tm 作为逃生口。
|
||
const DICT_OVERRIDES = {}
|
||
|
||
function buildFlatDictionary() {
|
||
if (__flatDictionary) return __flatDictionary
|
||
|
||
const dict = Object.create(null)
|
||
const conflicts = []
|
||
const seen = Object.create(null)
|
||
|
||
// 1. 优先注入手动裁决项
|
||
Object.keys(DICT_OVERRIDES).forEach((zh) => {
|
||
dict[zh] = DICT_OVERRIDES[zh]
|
||
seen[zh] = { en: DICT_OVERRIDES[zh], path: '<override>' }
|
||
})
|
||
|
||
const zhRoot = globalMessages.zh || globalMessages[ZH_LOCALE] || {}
|
||
const enRoot = globalMessages.en || globalMessages[EN_LOCALE] || {}
|
||
|
||
// 2. 平行 DFS:以 zh 叶子(中文)为 key,同路径 en 叶子为 value
|
||
function walk(zhNode, enNode, path) {
|
||
if (zhNode && typeof zhNode === 'object' && !Array.isArray(zhNode)) {
|
||
const keys = Object.keys(zhNode)
|
||
for (let i = 0; i < keys.length; i += 1) {
|
||
const k = keys[i]
|
||
walk(zhNode[k], enNode ? enNode[k] : undefined, path ? path + '.' + k : k)
|
||
}
|
||
return
|
||
}
|
||
const zhText = zhNode == null ? '' : String(zhNode)
|
||
if (!zhText) return
|
||
const enText = enNode == null ? zhText : String(enNode)
|
||
|
||
if (seen[zhText]) {
|
||
if (seen[zhText].en !== enText) {
|
||
conflicts.push({
|
||
zh: zhText,
|
||
enFirst: seen[zhText].en,
|
||
enCurrent: enText,
|
||
pathFirst: seen[zhText].path,
|
||
pathCurrent: path
|
||
})
|
||
}
|
||
// 首次写入优先:已存在则不覆盖
|
||
return
|
||
}
|
||
dict[zhText] = enText
|
||
seen[zhText] = { en: enText, path }
|
||
}
|
||
walk(zhRoot, enRoot, '')
|
||
|
||
__flatDictionary = dict
|
||
__dictConflicts = conflicts
|
||
|
||
if (process.env.NODE_ENV !== 'production' && conflicts.length) {
|
||
console.warn('[i18n] flat dictionary built with ' + conflicts.length + ' conflicts; call getDictionaryConflicts() for details')
|
||
}
|
||
|
||
return dict
|
||
}
|
||
|
||
// 中文即 key 的翻译函数:中文环境原样返回,英文环境查扁平字典,查不到回退中文。
|
||
// 支持与 $tm/$translate 一致的 {param} 占位符替换。
|
||
export function translateText(text, locale, params = {}) {
|
||
if (text === null || text === undefined) return ''
|
||
const zhText = String(text)
|
||
if (!zhText) return ''
|
||
|
||
if (normalizeLocale(locale) !== EN_LOCALE) {
|
||
return formatI18nText(zhText, params)
|
||
}
|
||
|
||
const dict = buildFlatDictionary()
|
||
const enText = dict[zhText]
|
||
if (enText === undefined) {
|
||
if (process.env.NODE_ENV !== 'production') {
|
||
console.warn('[i18n] missing dictionary entry for: ' + zhText)
|
||
}
|
||
return formatI18nText(zhText, params)
|
||
}
|
||
return formatI18nText(enText, params)
|
||
}
|
||
|
||
export function getFlatDictionary() {
|
||
return buildFlatDictionary()
|
||
}
|
||
|
||
export function getDictionaryConflicts() {
|
||
if (!__dictConflicts) buildFlatDictionary()
|
||
return __dictConflicts || []
|
||
}
|