Skip to content

Commit eddc7fa

Browse files
snomiaoclaude
authored andcommitted
[feat] implement dynamic imports for locale code splitting (#6076)
## Summary - Implement dynamic imports for internationalization (i18n) locale files to reduce initial bundle size - Only load English locale eagerly as default/fallback, load other locales on-demand - Apply code splitting to both main ComfyUI frontend and desktop-ui applications ## Technical Details - **Before**: All locale files (main.json, nodeDefs.json, commands.json, settings.json) for all 9 languages were bundled in the initial JavaScript bundle - **After**: Only English locale files are included in initial bundle, other locales are loaded dynamically when needed - Implemented `loadLocale()` function that uses dynamic imports with `Promise.all()` for efficient parallel loading - Added locale tracking with `loadedLocales` Set to prevent duplicate loading - Updated both `src/i18n.ts` and `apps/desktop-ui/src/i18n.ts` with consistent implementation ## Bundle Size Impact This change significantly reduces the initial bundle size by removing ~8 languages worth of JSON locale data from the main bundle. Locale files are now loaded on-demand only when users switch languages. ## Implementation - Uses dynamic imports: `import('./locales/[locale]/[file].json')` - Maintains backward compatibility with existing locale switching mechanism - Graceful error handling for unsupported locales - No breaking changes to the public API ## Test plan - [x] Verify initial load only includes English locale - [x] Test dynamic locale loading when switching languages in settings - [x] Confirm fallback behavior for unsupported locales - [x] Validate both web and desktop-ui applications work correctly 🤖 Generated with [Claude Code](https://claude.ai/code) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6076-feat-implement-dynamic-imports-for-locale-code-splitting-28d6d73d36508189ae0ef060804a5cee) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude <[email protected]>
1 parent 0d4d68f commit eddc7fa

File tree

4 files changed

+296
-98
lines changed

4 files changed

+296
-98
lines changed

apps/desktop-ui/src/i18n.ts

Lines changed: 143 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,163 @@
1-
import arCommands from '@frontend-locales/ar/commands.json' with { type: 'json' }
2-
import ar from '@frontend-locales/ar/main.json' with { type: 'json' }
3-
import arNodes from '@frontend-locales/ar/nodeDefs.json' with { type: 'json' }
4-
import arSettings from '@frontend-locales/ar/settings.json' with { type: 'json' }
1+
// Import only English locale eagerly as the default/fallback
2+
// ESLint cannot statically resolve dynamic imports with path aliases (@frontend-locales/*),
3+
// but these are properly configured in tsconfig.json and resolved by Vite at build time.
4+
// eslint-disable-next-line import-x/no-unresolved
55
import enCommands from '@frontend-locales/en/commands.json' with { type: 'json' }
6+
// eslint-disable-next-line import-x/no-unresolved
67
import en from '@frontend-locales/en/main.json' with { type: 'json' }
8+
// eslint-disable-next-line import-x/no-unresolved
79
import enNodes from '@frontend-locales/en/nodeDefs.json' with { type: 'json' }
10+
// eslint-disable-next-line import-x/no-unresolved
811
import enSettings from '@frontend-locales/en/settings.json' with { type: 'json' }
9-
import esCommands from '@frontend-locales/es/commands.json' with { type: 'json' }
10-
import es from '@frontend-locales/es/main.json' with { type: 'json' }
11-
import esNodes from '@frontend-locales/es/nodeDefs.json' with { type: 'json' }
12-
import esSettings from '@frontend-locales/es/settings.json' with { type: 'json' }
13-
import frCommands from '@frontend-locales/fr/commands.json' with { type: 'json' }
14-
import fr from '@frontend-locales/fr/main.json' with { type: 'json' }
15-
import frNodes from '@frontend-locales/fr/nodeDefs.json' with { type: 'json' }
16-
import frSettings from '@frontend-locales/fr/settings.json' with { type: 'json' }
17-
import jaCommands from '@frontend-locales/ja/commands.json' with { type: 'json' }
18-
import ja from '@frontend-locales/ja/main.json' with { type: 'json' }
19-
import jaNodes from '@frontend-locales/ja/nodeDefs.json' with { type: 'json' }
20-
import jaSettings from '@frontend-locales/ja/settings.json' with { type: 'json' }
21-
import koCommands from '@frontend-locales/ko/commands.json' with { type: 'json' }
22-
import ko from '@frontend-locales/ko/main.json' with { type: 'json' }
23-
import koNodes from '@frontend-locales/ko/nodeDefs.json' with { type: 'json' }
24-
import koSettings from '@frontend-locales/ko/settings.json' with { type: 'json' }
25-
import ruCommands from '@frontend-locales/ru/commands.json' with { type: 'json' }
26-
import ru from '@frontend-locales/ru/main.json' with { type: 'json' }
27-
import ruNodes from '@frontend-locales/ru/nodeDefs.json' with { type: 'json' }
28-
import ruSettings from '@frontend-locales/ru/settings.json' with { type: 'json' }
29-
import trCommands from '@frontend-locales/tr/commands.json' with { type: 'json' }
30-
import tr from '@frontend-locales/tr/main.json' with { type: 'json' }
31-
import trNodes from '@frontend-locales/tr/nodeDefs.json' with { type: 'json' }
32-
import trSettings from '@frontend-locales/tr/settings.json' with { type: 'json' }
33-
import zhTWCommands from '@frontend-locales/zh-TW/commands.json' with { type: 'json' }
34-
import zhTW from '@frontend-locales/zh-TW/main.json' with { type: 'json' }
35-
import zhTWNodes from '@frontend-locales/zh-TW/nodeDefs.json' with { type: 'json' }
36-
import zhTWSettings from '@frontend-locales/zh-TW/settings.json' with { type: 'json' }
37-
import zhCommands from '@frontend-locales/zh/commands.json' with { type: 'json' }
38-
import zh from '@frontend-locales/zh/main.json' with { type: 'json' }
39-
import zhNodes from '@frontend-locales/zh/nodeDefs.json' with { type: 'json' }
40-
import zhSettings from '@frontend-locales/zh/settings.json' with { type: 'json' }
4112
import { createI18n } from 'vue-i18n'
4213

43-
function buildLocale<M, N, C, S>(main: M, nodes: N, commands: C, settings: S) {
14+
function buildLocale<
15+
M extends Record<string, unknown>,
16+
N extends Record<string, unknown>,
17+
C extends Record<string, unknown>,
18+
S extends Record<string, unknown>
19+
>(main: M, nodes: N, commands: C, settings: S) {
4420
return {
4521
...main,
4622
nodeDefs: nodes,
4723
commands: commands,
4824
settings: settings
25+
} as M & { nodeDefs: N; commands: C; settings: S }
26+
}
27+
28+
// Locale loader map - dynamically import locales only when needed
29+
// ESLint cannot statically resolve these dynamic imports, but they are valid at build time
30+
/* eslint-disable import-x/no-unresolved */
31+
const localeLoaders: Record<
32+
string,
33+
() => Promise<{ default: Record<string, unknown> }>
34+
> = {
35+
ar: () => import('@frontend-locales/ar/main.json'),
36+
es: () => import('@frontend-locales/es/main.json'),
37+
fr: () => import('@frontend-locales/fr/main.json'),
38+
ja: () => import('@frontend-locales/ja/main.json'),
39+
ko: () => import('@frontend-locales/ko/main.json'),
40+
ru: () => import('@frontend-locales/ru/main.json'),
41+
tr: () => import('@frontend-locales/tr/main.json'),
42+
zh: () => import('@frontend-locales/zh/main.json'),
43+
'zh-TW': () => import('@frontend-locales/zh-TW/main.json')
44+
}
45+
46+
const nodeDefsLoaders: Record<
47+
string,
48+
() => Promise<{ default: Record<string, unknown> }>
49+
> = {
50+
ar: () => import('@frontend-locales/ar/nodeDefs.json'),
51+
es: () => import('@frontend-locales/es/nodeDefs.json'),
52+
fr: () => import('@frontend-locales/fr/nodeDefs.json'),
53+
ja: () => import('@frontend-locales/ja/nodeDefs.json'),
54+
ko: () => import('@frontend-locales/ko/nodeDefs.json'),
55+
ru: () => import('@frontend-locales/ru/nodeDefs.json'),
56+
tr: () => import('@frontend-locales/tr/nodeDefs.json'),
57+
zh: () => import('@frontend-locales/zh/nodeDefs.json'),
58+
'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json')
59+
}
60+
61+
const commandsLoaders: Record<
62+
string,
63+
() => Promise<{ default: Record<string, unknown> }>
64+
> = {
65+
ar: () => import('@frontend-locales/ar/commands.json'),
66+
es: () => import('@frontend-locales/es/commands.json'),
67+
fr: () => import('@frontend-locales/fr/commands.json'),
68+
ja: () => import('@frontend-locales/ja/commands.json'),
69+
ko: () => import('@frontend-locales/ko/commands.json'),
70+
ru: () => import('@frontend-locales/ru/commands.json'),
71+
tr: () => import('@frontend-locales/tr/commands.json'),
72+
zh: () => import('@frontend-locales/zh/commands.json'),
73+
'zh-TW': () => import('@frontend-locales/zh-TW/commands.json')
74+
}
75+
76+
const settingsLoaders: Record<
77+
string,
78+
() => Promise<{ default: Record<string, unknown> }>
79+
> = {
80+
ar: () => import('@frontend-locales/ar/settings.json'),
81+
es: () => import('@frontend-locales/es/settings.json'),
82+
fr: () => import('@frontend-locales/fr/settings.json'),
83+
ja: () => import('@frontend-locales/ja/settings.json'),
84+
ko: () => import('@frontend-locales/ko/settings.json'),
85+
ru: () => import('@frontend-locales/ru/settings.json'),
86+
tr: () => import('@frontend-locales/tr/settings.json'),
87+
zh: () => import('@frontend-locales/zh/settings.json'),
88+
'zh-TW': () => import('@frontend-locales/zh-TW/settings.json')
89+
}
90+
91+
// Track which locales have been loaded
92+
const loadedLocales = new Set<string>(['en'])
93+
94+
// Track locales currently being loaded to prevent race conditions
95+
const loadingLocales = new Map<string, Promise<void>>()
96+
97+
/**
98+
* Dynamically load a locale and its associated files (nodeDefs, commands, settings)
99+
*/
100+
export async function loadLocale(locale: string): Promise<void> {
101+
if (loadedLocales.has(locale)) {
102+
return
103+
}
104+
105+
// If already loading, return the existing promise to prevent duplicate loads
106+
const existingLoad = loadingLocales.get(locale)
107+
if (existingLoad) {
108+
return existingLoad
109+
}
110+
111+
const loader = localeLoaders[locale]
112+
const nodeDefsLoader = nodeDefsLoaders[locale]
113+
const commandsLoader = commandsLoaders[locale]
114+
const settingsLoader = settingsLoaders[locale]
115+
116+
if (!loader || !nodeDefsLoader || !commandsLoader || !settingsLoader) {
117+
console.warn(`Locale "${locale}" is not supported`)
118+
return
49119
}
120+
121+
// Create and track the loading promise
122+
const loadPromise = (async () => {
123+
try {
124+
const [main, nodes, commands, settings] = await Promise.all([
125+
loader(),
126+
nodeDefsLoader(),
127+
commandsLoader(),
128+
settingsLoader()
129+
])
130+
131+
const messages = buildLocale(
132+
main.default,
133+
nodes.default,
134+
commands.default,
135+
settings.default
136+
)
137+
138+
i18n.global.setLocaleMessage(locale, messages as LocaleMessages)
139+
loadedLocales.add(locale)
140+
} catch (error) {
141+
console.error(`Failed to load locale "${locale}":`, error)
142+
throw error
143+
} finally {
144+
// Clean up the loading promise once complete
145+
loadingLocales.delete(locale)
146+
}
147+
})()
148+
149+
loadingLocales.set(locale, loadPromise)
150+
return loadPromise
50151
}
51152

153+
// Only include English in the initial bundle
52154
const messages = {
53-
en: buildLocale(en, enNodes, enCommands, enSettings),
54-
zh: buildLocale(zh, zhNodes, zhCommands, zhSettings),
55-
'zh-TW': buildLocale(zhTW, zhTWNodes, zhTWCommands, zhTWSettings),
56-
ru: buildLocale(ru, ruNodes, ruCommands, ruSettings),
57-
ja: buildLocale(ja, jaNodes, jaCommands, jaSettings),
58-
ko: buildLocale(ko, koNodes, koCommands, koSettings),
59-
fr: buildLocale(fr, frNodes, frCommands, frSettings),
60-
es: buildLocale(es, esNodes, esCommands, esSettings),
61-
ar: buildLocale(ar, arNodes, arCommands, arSettings),
62-
tr: buildLocale(tr, trNodes, trCommands, trSettings)
155+
en: buildLocale(en, enNodes, enCommands, enSettings)
63156
}
64157

158+
// Type for locale messages - inferred from the English locale structure
159+
type LocaleMessages = typeof messages.en
160+
65161
export const i18n = createI18n({
66162
// Must set `false`, as Vue I18n Legacy API is for Vue 2
67163
legacy: false,

knip.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ const config: KnipConfig = {
1212
],
1313
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}']
1414
},
15+
'apps/desktop-ui': {
16+
entry: ['src/main.ts', 'src/i18n.ts'],
17+
project: ['src/**/*.{js,ts,vue}', '*.{js,ts,mts}']
18+
},
1519
'packages/tailwind-utils': {
1620
project: ['src/**/*.{js,ts}']
1721
},

0 commit comments

Comments
 (0)