mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 04:40:18 +08:00
i18n: - Add 9 new locale files (ja/ko/de/es/fr/pt/ru/vi/zh-TW) - Add multilingual README files for all 11 languages - Add locale helper, index, and modular translation system - Add translation generation scripts Website (docs/index.html): - Replace 公益AI接口 branding with 晴辰云AI接口 - Remove OpenClaw 独立安装包 promotion block - Update SEO meta tags (description, keywords, OG, Twitter, JSON-LD) - Add 11-language README links to footer - Update 元宝派 link to new URL Bug fixes: - fix(cron): delivery format mode:'push' → mode:'announce', remove invalid 'to' field (fixes #141) - fix(cron): allow single-channel users to select delivery channel - fix(cron): preserve delivery field in job state for editing - fix(models): add 'ollama' as recognized API type, prevent overwriting native ollama config (fixes #140) - fix(models): skip /v1 append for ollama native API baseUrl - fix(assistant): normalize 'google-generative-ai' consistently, add ollama hints - fix(version): use CLI path classification for source detection on Windows (fixes #139) - fix(version): default to 'official' instead of 'chinese' when source unknown - fix(version): reorder npm global package check based on active CLI
116 lines
3.0 KiB
JavaScript
116 lines
3.0 KiB
JavaScript
/**
|
||
* i18n 国际化核心模块
|
||
* 模块化多语言架构,支持 zh-CN / en / zh-TW / ja / ko
|
||
*/
|
||
import { buildLocales } from '../locales/index.js'
|
||
|
||
const LANGS = buildLocales()
|
||
const LANG_KEY = 'clawpanel_lang'
|
||
const FALLBACK = 'zh-CN'
|
||
|
||
let _lang = FALLBACK
|
||
let _dict = LANGS[FALLBACK]
|
||
let _listeners = []
|
||
|
||
/**
|
||
* 翻译函数
|
||
* @param {string} key - 点分隔路径,如 'sidebar.dashboard'
|
||
* @param {object} [params] - 插值参数,如 { count: 3 } 替换 {count}
|
||
* @returns {string}
|
||
*/
|
||
export function t(key, params) {
|
||
let val = _resolve(_dict, key)
|
||
if (val === undefined) {
|
||
// fallback 到中文
|
||
val = _resolve(LANGS[FALLBACK], key)
|
||
}
|
||
if (val === undefined) return key
|
||
if (params) {
|
||
for (const [k, v] of Object.entries(params)) {
|
||
val = val.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v))
|
||
}
|
||
}
|
||
return val
|
||
}
|
||
|
||
function _resolve(obj, path) {
|
||
const parts = path.split('.')
|
||
let cur = obj
|
||
for (const p of parts) {
|
||
if (cur == null || typeof cur !== 'object') return undefined
|
||
cur = cur[p]
|
||
}
|
||
return typeof cur === 'string' ? cur : undefined
|
||
}
|
||
|
||
/** 获取当前语言 */
|
||
export function getLang() { return _lang }
|
||
|
||
/** 获取所有可用语言 */
|
||
export function getAvailableLangs() {
|
||
return [
|
||
{ code: 'zh-CN', label: '简体中文' },
|
||
{ code: 'zh-TW', label: '繁體中文' },
|
||
{ code: 'en', label: 'English' },
|
||
{ code: 'ja', label: '日本語' },
|
||
{ code: 'ko', label: '한국어' },
|
||
{ code: 'vi', label: 'Tiếng Việt' },
|
||
{ code: 'es', label: 'Español' },
|
||
{ code: 'pt', label: 'Português' },
|
||
{ code: 'ru', label: 'Русский' },
|
||
{ code: 'fr', label: 'Français' },
|
||
{ code: 'de', label: 'Deutsch' },
|
||
]
|
||
}
|
||
|
||
/** 切换语言 */
|
||
export function setLang(lang) {
|
||
if (!LANGS[lang]) return
|
||
_lang = lang
|
||
_dict = LANGS[lang]
|
||
localStorage.setItem(LANG_KEY, lang)
|
||
_listeners.forEach(fn => { try { fn(lang) } catch {} })
|
||
}
|
||
|
||
/** 监听语言变化 */
|
||
export function onLangChange(fn) {
|
||
_listeners.push(fn)
|
||
return () => { _listeners = _listeners.filter(cb => cb !== fn) }
|
||
}
|
||
|
||
/** 初始化:localStorage > navigator.language > fallback */
|
||
export function initI18n() {
|
||
const saved = localStorage.getItem(LANG_KEY)
|
||
if (saved && LANGS[saved]) {
|
||
_lang = saved
|
||
_dict = LANGS[saved]
|
||
return
|
||
}
|
||
// 自动检测浏览器语言
|
||
const nav = navigator.language || navigator.languages?.[0] || ''
|
||
if (nav === 'zh-TW' || nav === 'zh-HK') {
|
||
_lang = 'zh-TW'
|
||
} else if (nav.startsWith('zh')) {
|
||
_lang = 'zh-CN'
|
||
} else if (nav.startsWith('ja')) {
|
||
_lang = 'ja'
|
||
} else if (nav.startsWith('ko')) {
|
||
_lang = 'ko'
|
||
} else if (nav.startsWith('vi')) {
|
||
_lang = 'vi'
|
||
} else if (nav.startsWith('es')) {
|
||
_lang = 'es'
|
||
} else if (nav.startsWith('pt')) {
|
||
_lang = 'pt'
|
||
} else if (nav.startsWith('ru')) {
|
||
_lang = 'ru'
|
||
} else if (nav.startsWith('fr')) {
|
||
_lang = 'fr'
|
||
} else if (nav.startsWith('de')) {
|
||
_lang = 'de'
|
||
} else if (nav.startsWith('en')) {
|
||
_lang = 'en'
|
||
}
|
||
_dict = LANGS[_lang] || LANGS[FALLBACK]
|
||
}
|