多语言调整

This commit is contained in:
tianmo committed 2026-10-22 18:51:26 +08:00
1 parent 04de5e5a8e
commit 8bd9154414
23 files changed
+395 -155

No files matched your search

-7
View File
@@ -38,7 +38,6 @@ const overrides = {
export: { title: '匯出設定', spineVersion: 'Spine 版本', fps: '匯出 FPS', exportJson: '匯出 Spine JSON', exportImages: '匯出圖片 ZIP', connectorReady: '連接器可用 · Spine 已就緒', connectorOffline: '本機 Spine 連接器未啟動', spineNotFound: '連接器已啟動,但找不到 Spine', notAuthorized: '連接器已啟動,但目前網站尚未授權', checking: '偵測中', offline: '未啟動', notFound: '找不到 Spine', unauthorized: '未授權', connectorBusy: '正在重新計算並產生 Spine 專案', connectorGenerate: '使用本機 Spine 自動產生 {version} 專案' },
spine: { title: 'Spine 屬性', resources: 'Spine 資源', animations: 'Spine 動畫', hierarchy: '階層樹', xPosition: 'X 位置', yPosition: 'Y 位置', rotation: '旋轉', startTime: '開始時間', scaleX: 'X 縮放', scaleY: 'Y 縮放' },
help: { title: '使用說明', commonOps: '常用操作', shortcuts: '快速鍵', modules: '功能模組詳解' },
presets: { particleSweep: '1.粒子掃過', barBlocks: '2.長條方塊', forceTrail: '3.力場拖尾', lightningSequence: '4.雷電序列幀', texturePivot: '5.貼圖軸點', subEmitter: '6.子發射器' },
},
ja: {
common: {
@@ -77,7 +76,6 @@ const overrides = {
export: { title: 'エクスポート設定', spineVersion: 'Spine バージョン', fps: 'エクスポート FPS', exportJson: 'Spine JSON を書き出す', exportImages: '画像 ZIP を書き出す', connectorReady: 'コネクタ使用可能 · Spine 準備完了', connectorOffline: 'ローカル Spine コネクタが起動していません', spineNotFound: 'コネクタは起動していますが Spine が見つかりません', notAuthorized: 'コネクタは起動していますが、このサイトは未認証です', checking: '確認中', offline: '未起動', notFound: 'Spine が見つかりません', unauthorized: '未認証', connectorBusy: 'Spine プロジェクトを再計算して生成中', connectorGenerate: 'ローカル Spine で Spine {version} プロジェクトを生成' },
spine: { title: 'Spine プロパティ', resources: 'Spine リソース', animations: 'Spine アニメーション', hierarchy: '階層ツリー', xPosition: 'X 位置', yPosition: 'Y 位置', rotation: '回転', startTime: '開始時間', scaleX: 'X スケール', scaleY: 'Y スケール' },
help: { title: 'ヘルプ', commonOps: 'よく使う操作', shortcuts: 'ショートカット', modules: '機能モジュール' },
presets: { particleSweep: '1.パーティクルスイープ', barBlocks: '2.バーブロック', forceTrail: '3.フォースフィールドトレイル', lightningSequence: '4.雷シーケンス', texturePivot: '5.テクスチャピボット', subEmitter: '6.サブエミッター' },
},
ko: {
common: {
@@ -116,7 +114,6 @@ const overrides = {
export: { title: '내보내기 설정', spineVersion: 'Spine 버전', fps: '내보내기 FPS', exportJson: 'Spine JSON 내보내기', exportImages: '이미지 ZIP 내보내기', connectorReady: '커넥터 사용 가능 · Spine 준비됨', connectorOffline: '로컬 Spine 커넥터가 실행 중이 아닙니다', spineNotFound: '커넥터는 실행 중이지만 Spine을 찾을 수 없습니다', notAuthorized: '커넥터는 실행 중이지만 이 사이트는 승인되지 않았습니다', checking: '확인 중', offline: '오프라인', notFound: 'Spine 없음', unauthorized: '승인되지 않음', connectorBusy: 'Spine 프로젝트를 다시 계산하고 생성 중', connectorGenerate: '로컬 Spine으로 Spine {version} 프로젝트 생성' },
spine: { title: 'Spine 속성', resources: 'Spine 리소스', animations: 'Spine 애니메이션', hierarchy: '계층 트리', xPosition: 'X 위치', yPosition: 'Y 위치', rotation: '회전', startTime: '시작 시간', scaleX: 'X 크기', scaleY: 'Y 크기' },
help: { title: '도움말', commonOps: '자주 쓰는 작업', shortcuts: '단축키', modules: '기능 모듈' },
presets: { particleSweep: '1.파티클 스윕', barBlocks: '2.바 블록', forceTrail: '3.힘장 트레일', lightningSequence: '4.번개 시퀀스', texturePivot: '5.텍스처 피벗', subEmitter: '6.하위 이미터' },
},
fr: {
common: {
@@ -155,7 +152,6 @@ const overrides = {
export: { title: 'Paramètres d’export', spineVersion: 'Version de Spine', fps: 'FPS d’export', exportJson: 'Exporter Spine JSON', exportImages: 'Exporter les images ZIP', connectorReady: 'Connecteur disponible · Spine prêt', connectorOffline: 'Le connecteur Spine local n’est pas démarré', spineNotFound: 'Le connecteur est démarré, mais Spine est introuvable', notAuthorized: 'Le connecteur est démarré, mais ce site n’est pas autorisé', checking: 'Vérification', offline: 'Hors ligne', notFound: 'Spine introuvable', unauthorized: 'Non autorisé', connectorBusy: 'Recalcul et génération du projet Spine', connectorGenerate: 'Générer un projet Spine {version} avec Spine local' },
spine: { title: 'Propriétés Spine', resources: 'Ressources Spine', animations: 'Animations Spine', hierarchy: 'Hiérarchie', xPosition: 'Position X', yPosition: 'Position Y', rotation: 'Rotation', startTime: 'Heure de début', scaleX: 'Échelle X', scaleY: 'Échelle Y' },
help: { title: 'Aide', commonOps: 'Actions courantes', shortcuts: 'Raccourcis', modules: 'Guide des modules' },
presets: { particleSweep: '1.Balayage de particules', barBlocks: '2.Blocs rectangulaires', forceTrail: '3.Traînée de champ de force', lightningSequence: '4.Séquence d’éclairs', texturePivot: '5.Pivot de texture', subEmitter: '6.Sous-émetteur' },
},
de: {
common: {
@@ -194,7 +190,6 @@ const overrides = {
export: { title: 'Exporteinstellungen', spineVersion: 'Spine-Version', fps: 'Export-FPS', exportJson: 'Spine JSON exportieren', exportImages: 'Bilder-ZIP exportieren', connectorReady: 'Connector verfügbar · Spine bereit', connectorOffline: 'Der lokale Spine-Connector läuft nicht', spineNotFound: 'Der Connector läuft, aber Spine wurde nicht gefunden', notAuthorized: 'Der Connector läuft, aber diese Website ist nicht autorisiert', checking: 'Prüfung', offline: 'Offline', notFound: 'Spine nicht gefunden', unauthorized: 'Nicht autorisiert', connectorBusy: 'Spine-Projekt wird neu berechnet und erstellt', connectorGenerate: 'Spine-{version}-Projekt mit lokalem Spine erstellen' },
spine: { title: 'Spine-Eigenschaften', resources: 'Spine-Ressourcen', animations: 'Spine-Animationen', hierarchy: 'Hierarchie', xPosition: 'X-Position', yPosition: 'Y-Position', rotation: 'Drehung', startTime: 'Startzeit', scaleX: 'X-Skalierung', scaleY: 'Y-Skalierung' },
help: { title: 'Hilfe', commonOps: 'Häufige Aktionen', shortcuts: 'Tastenkürzel', modules: 'Modulhandbuch' },
presets: { particleSweep: '1.Partikel-Sweep', barBlocks: '2.Balkenblöcke', forceTrail: '3.Kraftfeldschweif', lightningSequence: '4.Blitzsequenz', texturePivot: '5.Textur-Pivot', subEmitter: '6.Sub-Emitter' },
},
'pt-BR': {
common: {
@@ -233,7 +228,6 @@ const overrides = {
export: { title: 'Configurações de exportação', spineVersion: 'Versão do Spine', fps: 'FPS de exportação', exportJson: 'Exportar Spine JSON', exportImages: 'Exportar imagens ZIP', connectorReady: 'Conector disponível · Spine pronto', connectorOffline: 'O conector local do Spine não está em execução', spineNotFound: 'O conector está ativo, mas o Spine não foi encontrado', notAuthorized: 'O conector está ativo, mas este site não está autorizado', checking: 'Verificando', offline: 'Offline', notFound: 'Spine não encontrado', unauthorized: 'Não autorizado', connectorBusy: 'Recalculando e gerando o projeto Spine', connectorGenerate: 'Gerar projeto Spine {version} com o Spine local' },
spine: { title: 'Propriedades do Spine', resources: 'Recursos Spine', animations: 'Animações Spine', hierarchy: 'Hierarquia', xPosition: 'Posição X', yPosition: 'Posição Y', rotation: 'Rotação', startTime: 'Tempo inicial', scaleX: 'Escala X', scaleY: 'Escala Y' },
help: { title: 'Ajuda', commonOps: 'Ações comuns', shortcuts: 'Atalhos', modules: 'Guia dos módulos' },
presets: { particleSweep: '1.Varredura de partículas', barBlocks: '2.Blocos retangulares', forceTrail: '3.Rastro de campo de força', lightningSequence: '4.Sequência de raios', texturePivot: '5.Pivô da textura', subEmitter: '6.Subemissor' },
},
es: {
common: {
@@ -272,7 +266,6 @@ const overrides = {
export: { title: 'Ajustes de exportación', spineVersion: 'Versión de Spine', fps: 'FPS de exportación', exportJson: 'Exportar Spine JSON', exportImages: 'Exportar imágenes ZIP', connectorReady: 'Conector disponible · Spine listo', connectorOffline: 'El conector local de Spine no está iniciado', spineNotFound: 'El conector está iniciado, pero no se encontró Spine', notAuthorized: 'El conector está iniciado, pero este sitio no está autorizado', checking: 'Comprobando', offline: 'Sin conexión', notFound: 'Spine no encontrado', unauthorized: 'No autorizado', connectorBusy: 'Recalculando y generando el proyecto de Spine', connectorGenerate: 'Generar proyecto Spine {version} con Spine local' },
spine: { title: 'Propiedades de Spine', resources: 'Recursos de Spine', animations: 'Animaciones de Spine', hierarchy: 'Jerarquía', xPosition: 'Posición X', yPosition: 'Posición Y', rotation: 'Rotación', startTime: 'Tiempo inicial', scaleX: 'Escala X', scaleY: 'Escala Y' },
help: { title: 'Ayuda', commonOps: 'Acciones comunes', shortcuts: 'Atajos', modules: 'Guía de módulos' },
presets: { particleSweep: '1.Barrido de partículas', barBlocks: '2.Bloques rectangulares', forceTrail: '3.Estela de campo de fuerza', lightningSequence: '4.Secuencia de rayos', texturePivot: '5.Pivote de textura', subEmitter: '6.Subemisor' },
},
}
+51 -2
View File
@@ -1,4 +1,4 @@
import { readFile } from 'node:fs/promises'
import { readFile, stat } from 'node:fs/promises'
import { build } from 'esbuild'
const localeCodes = ['zh-CN', 'zh-TW', 'en', 'ja', 'ko', 'fr', 'de', 'pt-BR', 'es']
@@ -7,6 +7,7 @@ const localeMessages = Object.fromEntries(await Promise.all(localeCodes.map(asyn
JSON.parse(await readFile(`src/i18n/locales/${code}.json`, 'utf8')),
])))
const zh = localeMessages['zh-CN']
const presetIndex = JSON.parse(await readFile('public/Preset/index.json', 'utf8'))
function flattenKeys(value, prefix = '') {
const keys = []
@@ -59,6 +60,22 @@ for (const code of localeCodes.slice(1)) {
}
}
for (const preset of presetIndex) {
const file = typeof preset === 'string' ? preset : preset?.file
if (typeof file !== 'string' || !file.trim()) throw new Error('Preset index entry is missing a file')
await stat(`public/Preset/${file}`)
if (typeof preset === 'string' || preset.name == null) continue
if (typeof preset.name === 'string') continue
if (typeof preset.name !== 'object' || Array.isArray(preset.name)) {
throw new Error(`Preset ${file} has an invalid name`)
}
for (const [code, name] of Object.entries(preset.name)) {
if (!localeCodes.includes(code) || typeof name !== 'string' || !name.trim()) {
throw new Error(`Preset ${file} has an invalid ${code} display name`)
}
}
}
const bundled = await build({
entryPoints: ['src/i18n/index.ts'],
bundle: true,
@@ -71,6 +88,38 @@ const i18nModule = await import(
`data:text/javascript;base64,${Buffer.from(bundled.outputFiles[0].contents).toString('base64')}`
)
const presetNamesBundle = await build({
entryPoints: ['src/views/particle-panel/presetNames.ts'],
bundle: true,
format: 'esm',
platform: 'node',
write: false,
logLevel: 'silent',
})
const presetNames = await import(
`data:text/javascript;base64,${Buffer.from(presetNamesBundle.outputFiles[0].contents).toString('base64')}`
)
const parsedPresets = presetNames.parsePresetIndex([
{ file: '8.test.json' },
'9.raw.json',
{ file: 'old.json', name: 'Old Preset' },
{ file: 'partial.json', name: { ja: '日本語プリセット' } },
])
if (parsedPresets.length !== 4) throw new Error('Preset compatibility parser rejected legacy entries')
const presetCases = [
[parsedPresets[0], 'zh-CN', '8.test'],
[parsedPresets[1], 'en', '9.raw'],
[parsedPresets[2], 'fr', 'Old Preset'],
[parsedPresets[3], 'ja', '日本語プリセット'],
[parsedPresets[3], 'fr', 'partial'],
]
for (const [preset, locale, expected] of presetCases) {
const actual = presetNames.resolvePresetDisplayName(preset, locale)
if (actual !== expected) {
throw new Error(`Preset fallback failed: expected ${expected}, got ${actual}`)
}
}
const cases = [
[null, ['zh-CN'], 'zh-CN'],
[null, ['zh-Hant-TW'], 'zh-TW'],
@@ -95,4 +144,4 @@ for (const [saved, browserLanguages, expected] of cases) {
}
}
console.log(`i18n 校验通过:${localeCodes.length} 种语言、${zhKeys.length} 个消息键,键与占位符一致,自动语言与缓存优先级正确`)
console.log(`i18n 校验通过:${localeCodes.length} 种语言、${zhKeys.length} 个消息键,预设索引兼容,自动语言与缓存优先级正确`)