Skip to content

Commit d6f1b9c

Browse files
committed
fix(i18n): consolidate locale definitions
1 parent 68fa1a4 commit d6f1b9c

5 files changed

Lines changed: 208 additions & 184 deletions

File tree

src/i18n.test.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest'
2-
const { i18n, loadLocale, mergeCustomNodesI18n, resolveSupportedLocale } =
3-
await import('./i18n')
2+
3+
import type * as I18nModule from './i18n'
4+
5+
let i18n: typeof I18nModule.i18n
6+
let loadLocale: typeof I18nModule.loadLocale
7+
let mergeCustomNodesI18n: typeof I18nModule.mergeCustomNodesI18n
8+
let resolveSupportedLocale: typeof I18nModule.resolveSupportedLocale
9+
10+
async function importI18nModule() {
11+
const i18nModule = await import('./i18n')
12+
i18n = i18nModule.i18n
13+
loadLocale = i18nModule.loadLocale
14+
mergeCustomNodesI18n = i18nModule.mergeCustomNodesI18n
15+
resolveSupportedLocale = i18nModule.resolveSupportedLocale
16+
}
417

518
// Mock the JSON imports before importing i18n module
619
vi.mock('./locales/en/main.json', () => ({ default: { welcome: 'Welcome' } }))
@@ -25,6 +38,7 @@ vi.mock('./locales/zh/settings.json', () => ({ default: { theme: '主题' } }))
2538
describe('i18n', () => {
2639
beforeEach(async () => {
2740
vi.resetModules()
41+
await importI18nModule()
2842
})
2943

3044
describe('mergeCustomNodesI18n', () => {
@@ -47,8 +61,6 @@ describe('i18n', () => {
4761
})
4862

4963
it('should store data for not-yet-loaded locales', async () => {
50-
const { i18n, mergeCustomNodesI18n } = await import('./i18n')
51-
5264
// Chinese is not pre-loaded, data should be stored but not merged yet
5365
mergeCustomNodesI18n({
5466
zh: {
@@ -149,7 +161,7 @@ describe('i18n', () => {
149161
it('should handle calling mergeCustomNodesI18n multiple times', async () => {
150162
// Use fresh module instance to ensure clean state
151163
vi.resetModules()
152-
const { i18n, loadLocale, mergeCustomNodesI18n } = await import('./i18n')
164+
await importI18nModule()
153165

154166
mergeCustomNodesI18n({
155167
zh: { plugin1: { name: '插件1' } }
@@ -190,12 +202,19 @@ describe('i18n', () => {
190202
expect(resolved).toBe('zh-TW')
191203
})
192204

193-
it('should fall back from pt-BR base tag pt to pt-BR', async () => {
194-
// pt is not shipped on its own, but pt-BR is.
195-
// resolveSupportedLocale only fires the base-tag fallback for inputs
196-
// whose full tag isn't shipped — verifying the standalone helper here.
197-
expect(resolveSupportedLocale('pt-BR')).toBe('pt-BR')
198-
expect(resolveSupportedLocale('pt')).toBe('en')
205+
it('should preserve shipped pt-BR locale through loadLocale', async () => {
206+
const resolved = await loadLocale('pt-BR')
207+
208+
expect(resolved).toBe('pt-BR')
209+
expect(i18n.global.getLocaleMessage('pt-BR')).toEqual(
210+
expect.objectContaining({
211+
commands: expect.any(Object),
212+
nodeDefs: expect.any(Object),
213+
settings: expect.any(Object)
214+
})
215+
)
216+
217+
expect(await loadLocale('pt')).toBe('en')
199218
})
200219

201220
it('should handle concurrent load requests for same locale', async () => {

src/i18n.ts

Lines changed: 15 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import { createI18n } from 'vue-i18n'
22

3-
// ESLint cannot statically resolve dynamic imports with relative paths in template strings,
4-
// but these are valid ES module imports that Vite processes correctly at build time.
3+
import {
4+
localeDefinitions,
5+
resolveSupportedLocale
6+
} from '@/locales/localeConfig'
7+
import type { SupportedLocale } from '@/locales/localeConfig'
58

69
// Import only English locale eagerly as the default/fallback
710
import enCommands from './locales/en/commands.json' with { type: 'json' }
811
import en from './locales/en/main.json' with { type: 'json' }
912
import enNodes from './locales/en/nodeDefs.json' with { type: 'json' }
1013
import enSettings from './locales/en/settings.json' with { type: 'json' }
1114

15+
export { resolveSupportedLocale }
16+
1217
function buildLocale<
1318
M extends Record<string, unknown>,
1419
N extends Record<string, unknown>,
@@ -23,75 +28,6 @@ function buildLocale<
2328
} as M & { nodeDefs: N; commands: C; settings: S }
2429
}
2530

26-
// Locale loader map - dynamically import locales only when needed
27-
const localeLoaders: Record<
28-
string,
29-
() => Promise<{ default: Record<string, unknown> }>
30-
> = {
31-
ar: () => import('./locales/ar/main.json'),
32-
es: () => import('./locales/es/main.json'),
33-
fa: () => import('./locales/fa/main.json'),
34-
fr: () => import('./locales/fr/main.json'),
35-
ja: () => import('./locales/ja/main.json'),
36-
ko: () => import('./locales/ko/main.json'),
37-
ru: () => import('./locales/ru/main.json'),
38-
tr: () => import('./locales/tr/main.json'),
39-
zh: () => import('./locales/zh/main.json'),
40-
'zh-TW': () => import('./locales/zh-TW/main.json'),
41-
'pt-BR': () => import('./locales/pt-BR/main.json')
42-
}
43-
44-
const nodeDefsLoaders: Record<
45-
string,
46-
() => Promise<{ default: Record<string, unknown> }>
47-
> = {
48-
ar: () => import('./locales/ar/nodeDefs.json'),
49-
es: () => import('./locales/es/nodeDefs.json'),
50-
fa: () => import('./locales/fa/nodeDefs.json'),
51-
fr: () => import('./locales/fr/nodeDefs.json'),
52-
ja: () => import('./locales/ja/nodeDefs.json'),
53-
ko: () => import('./locales/ko/nodeDefs.json'),
54-
ru: () => import('./locales/ru/nodeDefs.json'),
55-
tr: () => import('./locales/tr/nodeDefs.json'),
56-
zh: () => import('./locales/zh/nodeDefs.json'),
57-
'zh-TW': () => import('./locales/zh-TW/nodeDefs.json'),
58-
'pt-BR': () => import('./locales/pt-BR/nodeDefs.json')
59-
}
60-
61-
const commandsLoaders: Record<
62-
string,
63-
() => Promise<{ default: Record<string, unknown> }>
64-
> = {
65-
ar: () => import('./locales/ar/commands.json'),
66-
es: () => import('./locales/es/commands.json'),
67-
fa: () => import('./locales/fa/commands.json'),
68-
fr: () => import('./locales/fr/commands.json'),
69-
ja: () => import('./locales/ja/commands.json'),
70-
ko: () => import('./locales/ko/commands.json'),
71-
ru: () => import('./locales/ru/commands.json'),
72-
tr: () => import('./locales/tr/commands.json'),
73-
zh: () => import('./locales/zh/commands.json'),
74-
'zh-TW': () => import('./locales/zh-TW/commands.json'),
75-
'pt-BR': () => import('./locales/pt-BR/commands.json')
76-
}
77-
78-
const settingsLoaders: Record<
79-
string,
80-
() => Promise<{ default: Record<string, unknown> }>
81-
> = {
82-
ar: () => import('./locales/ar/settings.json'),
83-
es: () => import('./locales/es/settings.json'),
84-
fa: () => import('./locales/fa/settings.json'),
85-
fr: () => import('./locales/fr/settings.json'),
86-
ja: () => import('./locales/ja/settings.json'),
87-
ko: () => import('./locales/ko/settings.json'),
88-
ru: () => import('./locales/ru/settings.json'),
89-
tr: () => import('./locales/tr/settings.json'),
90-
zh: () => import('./locales/zh/settings.json'),
91-
'zh-TW': () => import('./locales/zh-TW/settings.json'),
92-
'pt-BR': () => import('./locales/pt-BR/settings.json')
93-
}
94-
9531
// Track which locales have been loaded
9632
const loadedLocales = new Set<string>(['en'])
9733

@@ -101,48 +37,6 @@ const loadingLocales = new Map<string, Promise<void>>()
10137
// Store custom nodes i18n data for merging when locales are lazily loaded
10238
const customNodesI18nData: Record<string, unknown> = {}
10339

104-
const SUPPORTED_LOCALES = [
105-
'en',
106-
'ar',
107-
'es',
108-
'fa',
109-
'fr',
110-
'ja',
111-
'ko',
112-
'ru',
113-
'tr',
114-
'zh',
115-
'zh-TW',
116-
'pt-BR'
117-
] as const
118-
119-
type SupportedLocale = (typeof SUPPORTED_LOCALES)[number]
120-
121-
// Lowercased lookup map → canonical tag, since BCP-47 matching is
122-
// case-insensitive (e.g. `pt-br` from older browsers must match `pt-BR`).
123-
const supportedLocaleByLower = new Map<string, SupportedLocale>(
124-
SUPPORTED_LOCALES.map((locale) => [locale.toLowerCase(), locale])
125-
)
126-
127-
/**
128-
* Resolve a BCP-47 language tag to a locale we ship messages for, with
129-
* graceful fallback. Tries the full tag (preserves `zh-TW`, `pt-BR`),
130-
* then the base tag (`zh`, `pt`), then `'en'`. Matching is case-insensitive
131-
* but the returned tag is always in canonical casing.
132-
*/
133-
export function resolveSupportedLocale(
134-
input: string | undefined | null
135-
): SupportedLocale {
136-
if (!input) return 'en'
137-
const normalized = input.toLowerCase()
138-
const exact = supportedLocaleByLower.get(normalized)
139-
if (exact) return exact
140-
const base = normalized.split('-')[0]
141-
const baseMatch = supportedLocaleByLower.get(base)
142-
if (baseMatch) return baseMatch
143-
return 'en'
144-
}
145-
14640
/**
14741
* Dynamically load a locale and its associated files (nodeDefs, commands, settings).
14842
* Unsupported locales are clamped to `'en'`.
@@ -160,19 +54,19 @@ export async function loadLocale(locale: string): Promise<SupportedLocale> {
16054
return resolved
16155
}
16256

163-
const loader = localeLoaders[resolved]
164-
const nodeDefsLoader = nodeDefsLoaders[resolved]
165-
const commandsLoader = commandsLoaders[resolved]
166-
const settingsLoader = settingsLoaders[resolved]
57+
const loaders = localeDefinitions[resolved].loaders
58+
if (!loaders) {
59+
return resolved
60+
}
16761

16862
// Create and track the loading promise
16963
const loadPromise = (async () => {
17064
try {
17165
const [main, nodes, commands, settings] = await Promise.all([
172-
loader(),
173-
nodeDefsLoader(),
174-
commandsLoader(),
175-
settingsLoader()
66+
loaders.main(),
67+
loaders.nodeDefs(),
68+
loaders.commands(),
69+
loaders.settings()
17670
])
17771

17872
const messages = buildLocale(

src/locales/CONTRIBUTING.md

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -35,46 +35,20 @@ module.exports = defineConfig({
3535
})
3636
```
3737

38-
#### 1.2 Update `src/platform/settings/constants/coreSettings.ts`
38+
#### 1.2 Update `src/locales/localeConfig.ts`
3939

40-
Add your language to the dropdown options:
40+
Add your language to the shared runtime locale definition. This feeds the
41+
settings dropdown, supported-locale resolution, and lazy locale loading:
4142

4243
```typescript
43-
{
44-
id: 'Comfy.Locale',
45-
name: 'Language',
46-
type: 'combo',
47-
options: [
48-
{ value: 'en', text: 'English' },
49-
{ value: 'zh', text: '中文' },
50-
{ value: 'zh-TW', text: '繁體中文 (台灣)' }, // Add your language here
51-
{ value: 'ru', text: 'Русский' },
52-
{ value: 'ja', text: '日本語' },
53-
{ value: 'ko', text: '한국어' },
54-
{ value: 'fr', text: 'Français' },
55-
{ value: 'es', text: 'Español' }
56-
],
57-
defaultValue: () => navigator.language.split('-')[0] || 'en'
58-
},
59-
```
60-
61-
#### 1.3 Update `src/i18n.ts`
62-
63-
Add imports for your new language files:
64-
65-
```typescript
66-
// Add these imports (replace zh-TW with your language code)
67-
import zhTWCommands from './locales/zh-TW/commands.json'
68-
import zhTW from './locales/zh-TW/main.json'
69-
import zhTWNodes from './locales/zh-TW/nodeDefs.json'
70-
import zhTWSettings from './locales/zh-TW/settings.json'
71-
72-
// Add to the messages object
73-
const messages = {
74-
en: buildLocale(en, enNodes, enCommands, enSettings),
75-
zh: buildLocale(zh, zhNodes, zhCommands, zhSettings),
76-
'zh-TW': buildLocale(zhTW, zhTWNodes, zhTWCommands, zhTWSettings) // Add this line
77-
// ... other languages
44+
'zh-TW': {
45+
text: '繁體中文 (台灣)',
46+
loaders: {
47+
main: () => import('./zh-TW/main.json'),
48+
nodeDefs: () => import('./zh-TW/nodeDefs.json'),
49+
commands: () => import('./zh-TW/commands.json'),
50+
settings: () => import('./zh-TW/settings.json')
51+
}
7852
}
7953
```
8054

0 commit comments

Comments
 (0)