feat: i18n 11 languages + website update + fix #139 #140 #141

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
This commit is contained in:
晴天
2026-03-24 22:31:11 +08:00
parent f8af3bea4a
commit 985d263dc6
261 changed files with 26760 additions and 175 deletions

165
scripts/split-locales.cjs Normal file
View File

@@ -0,0 +1,165 @@
/**
* 迁移脚本:将单体 JSON 语言包拆分为模块化 JS 文件
* 读取 11 种语言 JSON → 生成 src/locales/modules/*.js
*/
const fs = require('fs')
const path = require('path')
const LOCALES_DIR = path.resolve(__dirname, '../src/locales')
const MODULES_DIR = path.resolve(LOCALES_DIR, 'modules')
function readLang(file) {
const p = path.join(LOCALES_DIR, file)
return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : {}
}
const zhCN = readLang('zh-CN.json')
const en = readLang('en.json')
const zhTW = readLang('zh-TW.json')
const ja = readLang('ja.json')
const ko = readLang('ko.json')
const vi = readLang('vi.json')
const es = readLang('es.json')
const pt = readLang('pt.json')
const ru = readLang('ru.json')
const fr = readLang('fr.json')
const de = readLang('de.json')
// 模块名映射JSON key → 文件名)
const MODULE_FILE_MAP = {
common: 'common',
sidebar: 'sidebar',
instance: 'instance',
dashboard: 'dashboard',
services: 'services',
settings: 'settings',
models: 'models',
agents: 'agents',
gateway: 'gateway',
security: 'security',
communication: 'communication',
channels: 'channels',
memory: 'memory',
cron: 'cron',
usage: 'usage',
skills: 'skills',
chat: 'chat',
chatDebug: 'chat-debug',
setup: 'setup',
about: 'about',
ext: 'ext',
logs: 'logs',
assistant: 'assistant',
toast: 'toast',
modal: 'modal',
}
// 确保输出目录存在
if (!fs.existsSync(MODULES_DIR)) {
fs.mkdirSync(MODULES_DIR, { recursive: true })
}
// 转义 JS 字符串中的特殊字符
function escapeStr(s) {
if (typeof s !== 'string') return ''
return s
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\r/g, '')
}
// 参数顺序: zhCN, en, zhTW, ja, ko, vi, es, pt, ru, fr, de
const LANG_ORDER = [
{ key: 'zhCN', fallback: null },
{ key: 'en', fallback: null },
{ key: 'zhTW', fallback: 'zhCN' },
{ key: 'ja', fallback: 'en' },
{ key: 'ko', fallback: 'en' },
{ key: 'vi', fallback: 'en' },
{ key: 'es', fallback: 'en' },
{ key: 'pt', fallback: 'en' },
{ key: 'ru', fallback: 'en' },
{ key: 'fr', fallback: 'en' },
{ key: 'de', fallback: 'en' },
]
// 生成一个模块的 JS 源码11 语言)
function generateModule(sectionKey, sections) {
const keys = Object.keys(sections.zhCN)
const lines = []
lines.push("import { _ } from '../helper.js'")
lines.push('')
lines.push('export default {')
for (const key of keys) {
const vals = {}
for (const { key: lk } of LANG_ORDER) {
vals[lk] = (sections[lk] && sections[lk][key]) || ''
}
// 计算可省略的参数(和 fallback 相同时省略)
const params = LANG_ORDER.map(({ key: lk, fallback }) => {
if (!fallback) return vals[lk] // zhCN, en 必填
return vals[lk] === vals[fallback] ? '' : vals[lk]
})
// 从尾部截断空参数
let lastNonEmpty = 1 // 至少保留 zhCN + en
for (let i = params.length - 1; i >= 2; i--) {
if (params[i]) { lastNonEmpty = i; break }
}
const parts = params.slice(0, lastNonEmpty + 1).map(v => `'${escapeStr(v)}'`)
lines.push(` ${safeKey(key)}: _(${parts.join(', ')}),`)
}
lines.push('}')
lines.push('')
return lines.join('\n')
}
// 确保 key 是合法的 JS 标识符,否则加引号
function safeKey(key) {
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) return key
return `'${key}'`
}
let totalKeys = 0
let fileCount = 0
for (const [sectionKey, fileName] of Object.entries(MODULE_FILE_MAP)) {
const zhCNSection = zhCN[sectionKey]
if (!zhCNSection) {
console.warn(`⚠ Section "${sectionKey}" not found in zh-CN.json, skipping`)
continue
}
const sections = {
zhCN: zhCNSection,
en: en[sectionKey] || {},
zhTW: zhTW[sectionKey] || {},
ja: ja[sectionKey] || {},
ko: ko[sectionKey] || {},
vi: vi[sectionKey] || {},
es: es[sectionKey] || {},
pt: pt[sectionKey] || {},
ru: ru[sectionKey] || {},
fr: fr[sectionKey] || {},
de: de[sectionKey] || {},
}
const source = generateModule(sectionKey, sections)
const outPath = path.join(MODULES_DIR, `${fileName}.js`)
fs.writeFileSync(outPath, source, 'utf8')
const keyCount = Object.keys(zhCNSection).length
totalKeys += keyCount
fileCount++
console.log(`${fileName}.js (${keyCount} keys)`)
}
console.log(`\n✓ Done: ${fileCount} module files, ${totalKeys} total keys`)
console.log(` Output: ${MODULES_DIR}`)