diff --git a/apps/desktop-ui/src/i18n.ts b/apps/desktop-ui/src/i18n.ts index f9d6eab37b..5f0739db75 100644 --- a/apps/desktop-ui/src/i18n.ts +++ b/apps/desktop-ui/src/i18n.ts @@ -1,67 +1,163 @@ -import arCommands from '@frontend-locales/ar/commands.json' with { type: 'json' } -import ar from '@frontend-locales/ar/main.json' with { type: 'json' } -import arNodes from '@frontend-locales/ar/nodeDefs.json' with { type: 'json' } -import arSettings from '@frontend-locales/ar/settings.json' with { type: 'json' } +// Import only English locale eagerly as the default/fallback +// ESLint cannot statically resolve dynamic imports with path aliases (@frontend-locales/*), +// but these are properly configured in tsconfig.json and resolved by Vite at build time. +// eslint-disable-next-line import-x/no-unresolved import enCommands from '@frontend-locales/en/commands.json' with { type: 'json' } +// eslint-disable-next-line import-x/no-unresolved import en from '@frontend-locales/en/main.json' with { type: 'json' } +// eslint-disable-next-line import-x/no-unresolved import enNodes from '@frontend-locales/en/nodeDefs.json' with { type: 'json' } +// eslint-disable-next-line import-x/no-unresolved import enSettings from '@frontend-locales/en/settings.json' with { type: 'json' } -import esCommands from '@frontend-locales/es/commands.json' with { type: 'json' } -import es from '@frontend-locales/es/main.json' with { type: 'json' } -import esNodes from '@frontend-locales/es/nodeDefs.json' with { type: 'json' } -import esSettings from '@frontend-locales/es/settings.json' with { type: 'json' } -import frCommands from '@frontend-locales/fr/commands.json' with { type: 'json' } -import fr from '@frontend-locales/fr/main.json' with { type: 'json' } -import frNodes from '@frontend-locales/fr/nodeDefs.json' with { type: 'json' } -import frSettings from '@frontend-locales/fr/settings.json' with { type: 'json' } -import jaCommands from '@frontend-locales/ja/commands.json' with { type: 'json' } -import ja from '@frontend-locales/ja/main.json' with { type: 'json' } -import jaNodes from '@frontend-locales/ja/nodeDefs.json' with { type: 'json' } -import jaSettings from '@frontend-locales/ja/settings.json' with { type: 'json' } -import koCommands from '@frontend-locales/ko/commands.json' with { type: 'json' } -import ko from '@frontend-locales/ko/main.json' with { type: 'json' } -import koNodes from '@frontend-locales/ko/nodeDefs.json' with { type: 'json' } -import koSettings from '@frontend-locales/ko/settings.json' with { type: 'json' } -import ruCommands from '@frontend-locales/ru/commands.json' with { type: 'json' } -import ru from '@frontend-locales/ru/main.json' with { type: 'json' } -import ruNodes from '@frontend-locales/ru/nodeDefs.json' with { type: 'json' } -import ruSettings from '@frontend-locales/ru/settings.json' with { type: 'json' } -import trCommands from '@frontend-locales/tr/commands.json' with { type: 'json' } -import tr from '@frontend-locales/tr/main.json' with { type: 'json' } -import trNodes from '@frontend-locales/tr/nodeDefs.json' with { type: 'json' } -import trSettings from '@frontend-locales/tr/settings.json' with { type: 'json' } -import zhTWCommands from '@frontend-locales/zh-TW/commands.json' with { type: 'json' } -import zhTW from '@frontend-locales/zh-TW/main.json' with { type: 'json' } -import zhTWNodes from '@frontend-locales/zh-TW/nodeDefs.json' with { type: 'json' } -import zhTWSettings from '@frontend-locales/zh-TW/settings.json' with { type: 'json' } -import zhCommands from '@frontend-locales/zh/commands.json' with { type: 'json' } -import zh from '@frontend-locales/zh/main.json' with { type: 'json' } -import zhNodes from '@frontend-locales/zh/nodeDefs.json' with { type: 'json' } -import zhSettings from '@frontend-locales/zh/settings.json' with { type: 'json' } import { createI18n } from 'vue-i18n' -function buildLocale(main: M, nodes: N, commands: C, settings: S) { +function buildLocale< + M extends Record, + N extends Record, + C extends Record, + S extends Record +>(main: M, nodes: N, commands: C, settings: S) { return { ...main, nodeDefs: nodes, commands: commands, settings: settings + } as M & { nodeDefs: N; commands: C; settings: S } +} + +// Locale loader map - dynamically import locales only when needed +// ESLint cannot statically resolve these dynamic imports, but they are valid at build time +/* eslint-disable import-x/no-unresolved */ +const localeLoaders: Record< + string, + () => Promise<{ default: Record }> +> = { + ar: () => import('@frontend-locales/ar/main.json'), + es: () => import('@frontend-locales/es/main.json'), + fr: () => import('@frontend-locales/fr/main.json'), + ja: () => import('@frontend-locales/ja/main.json'), + ko: () => import('@frontend-locales/ko/main.json'), + ru: () => import('@frontend-locales/ru/main.json'), + tr: () => import('@frontend-locales/tr/main.json'), + zh: () => import('@frontend-locales/zh/main.json'), + 'zh-TW': () => import('@frontend-locales/zh-TW/main.json') +} + +const nodeDefsLoaders: Record< + string, + () => Promise<{ default: Record }> +> = { + ar: () => import('@frontend-locales/ar/nodeDefs.json'), + es: () => import('@frontend-locales/es/nodeDefs.json'), + fr: () => import('@frontend-locales/fr/nodeDefs.json'), + ja: () => import('@frontend-locales/ja/nodeDefs.json'), + ko: () => import('@frontend-locales/ko/nodeDefs.json'), + ru: () => import('@frontend-locales/ru/nodeDefs.json'), + tr: () => import('@frontend-locales/tr/nodeDefs.json'), + zh: () => import('@frontend-locales/zh/nodeDefs.json'), + 'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json') +} + +const commandsLoaders: Record< + string, + () => Promise<{ default: Record }> +> = { + ar: () => import('@frontend-locales/ar/commands.json'), + es: () => import('@frontend-locales/es/commands.json'), + fr: () => import('@frontend-locales/fr/commands.json'), + ja: () => import('@frontend-locales/ja/commands.json'), + ko: () => import('@frontend-locales/ko/commands.json'), + ru: () => import('@frontend-locales/ru/commands.json'), + tr: () => import('@frontend-locales/tr/commands.json'), + zh: () => import('@frontend-locales/zh/commands.json'), + 'zh-TW': () => import('@frontend-locales/zh-TW/commands.json') +} + +const settingsLoaders: Record< + string, + () => Promise<{ default: Record }> +> = { + ar: () => import('@frontend-locales/ar/settings.json'), + es: () => import('@frontend-locales/es/settings.json'), + fr: () => import('@frontend-locales/fr/settings.json'), + ja: () => import('@frontend-locales/ja/settings.json'), + ko: () => import('@frontend-locales/ko/settings.json'), + ru: () => import('@frontend-locales/ru/settings.json'), + tr: () => import('@frontend-locales/tr/settings.json'), + zh: () => import('@frontend-locales/zh/settings.json'), + 'zh-TW': () => import('@frontend-locales/zh-TW/settings.json') +} + +// Track which locales have been loaded +const loadedLocales = new Set(['en']) + +// Track locales currently being loaded to prevent race conditions +const loadingLocales = new Map>() + +/** + * Dynamically load a locale and its associated files (nodeDefs, commands, settings) + */ +export async function loadLocale(locale: string): Promise { + if (loadedLocales.has(locale)) { + return + } + + // If already loading, return the existing promise to prevent duplicate loads + const existingLoad = loadingLocales.get(locale) + if (existingLoad) { + return existingLoad + } + + const loader = localeLoaders[locale] + const nodeDefsLoader = nodeDefsLoaders[locale] + const commandsLoader = commandsLoaders[locale] + const settingsLoader = settingsLoaders[locale] + + if (!loader || !nodeDefsLoader || !commandsLoader || !settingsLoader) { + console.warn(`Locale "${locale}" is not supported`) + return } + + // Create and track the loading promise + const loadPromise = (async () => { + try { + const [main, nodes, commands, settings] = await Promise.all([ + loader(), + nodeDefsLoader(), + commandsLoader(), + settingsLoader() + ]) + + const messages = buildLocale( + main.default, + nodes.default, + commands.default, + settings.default + ) + + i18n.global.setLocaleMessage(locale, messages as LocaleMessages) + loadedLocales.add(locale) + } catch (error) { + console.error(`Failed to load locale "${locale}":`, error) + throw error + } finally { + // Clean up the loading promise once complete + loadingLocales.delete(locale) + } + })() + + loadingLocales.set(locale, loadPromise) + return loadPromise } +// Only include English in the initial bundle const messages = { - en: buildLocale(en, enNodes, enCommands, enSettings), - zh: buildLocale(zh, zhNodes, zhCommands, zhSettings), - 'zh-TW': buildLocale(zhTW, zhTWNodes, zhTWCommands, zhTWSettings), - ru: buildLocale(ru, ruNodes, ruCommands, ruSettings), - ja: buildLocale(ja, jaNodes, jaCommands, jaSettings), - ko: buildLocale(ko, koNodes, koCommands, koSettings), - fr: buildLocale(fr, frNodes, frCommands, frSettings), - es: buildLocale(es, esNodes, esCommands, esSettings), - ar: buildLocale(ar, arNodes, arCommands, arSettings), - tr: buildLocale(tr, trNodes, trCommands, trSettings) + en: buildLocale(en, enNodes, enCommands, enSettings) } +// Type for locale messages - inferred from the English locale structure +type LocaleMessages = typeof messages.en + export const i18n = createI18n({ // Must set `false`, as Vue I18n Legacy API is for Vue 2 legacy: false, diff --git a/knip.config.ts b/knip.config.ts index 0ed7361e2a..ef562e9024 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -12,6 +12,10 @@ const config: KnipConfig = { ], project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}'] }, + 'apps/desktop-ui': { + entry: ['src/main.ts', 'src/i18n.ts'], + project: ['src/**/*.{js,ts,vue}', '*.{js,ts,mts}'] + }, 'packages/tailwind-utils': { project: ['src/**/*.{js,ts}'] }, diff --git a/src/i18n.ts b/src/i18n.ts index 38a8dfe951..d3e34245ca 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,68 +1,159 @@ import { createI18n } from 'vue-i18n' -import arCommands from './locales/ar/commands.json' with { type: 'json' } -import ar from './locales/ar/main.json' with { type: 'json' } -import arNodes from './locales/ar/nodeDefs.json' with { type: 'json' } -import arSettings from './locales/ar/settings.json' with { type: 'json' } +// ESLint cannot statically resolve dynamic imports with relative paths in template strings, +// but these are valid ES module imports that Vite processes correctly at build time. + +// Import only English locale eagerly as the default/fallback import enCommands from './locales/en/commands.json' with { type: 'json' } import en from './locales/en/main.json' with { type: 'json' } import enNodes from './locales/en/nodeDefs.json' with { type: 'json' } import enSettings from './locales/en/settings.json' with { type: 'json' } -import esCommands from './locales/es/commands.json' with { type: 'json' } -import es from './locales/es/main.json' with { type: 'json' } -import esNodes from './locales/es/nodeDefs.json' with { type: 'json' } -import esSettings from './locales/es/settings.json' with { type: 'json' } -import frCommands from './locales/fr/commands.json' with { type: 'json' } -import fr from './locales/fr/main.json' with { type: 'json' } -import frNodes from './locales/fr/nodeDefs.json' with { type: 'json' } -import frSettings from './locales/fr/settings.json' with { type: 'json' } -import jaCommands from './locales/ja/commands.json' with { type: 'json' } -import ja from './locales/ja/main.json' with { type: 'json' } -import jaNodes from './locales/ja/nodeDefs.json' with { type: 'json' } -import jaSettings from './locales/ja/settings.json' with { type: 'json' } -import koCommands from './locales/ko/commands.json' with { type: 'json' } -import ko from './locales/ko/main.json' with { type: 'json' } -import koNodes from './locales/ko/nodeDefs.json' with { type: 'json' } -import koSettings from './locales/ko/settings.json' with { type: 'json' } -import ruCommands from './locales/ru/commands.json' with { type: 'json' } -import ru from './locales/ru/main.json' with { type: 'json' } -import ruNodes from './locales/ru/nodeDefs.json' with { type: 'json' } -import ruSettings from './locales/ru/settings.json' with { type: 'json' } -import trCommands from './locales/tr/commands.json' with { type: 'json' } -import tr from './locales/tr/main.json' with { type: 'json' } -import trNodes from './locales/tr/nodeDefs.json' with { type: 'json' } -import trSettings from './locales/tr/settings.json' with { type: 'json' } -import zhTWCommands from './locales/zh-TW/commands.json' with { type: 'json' } -import zhTW from './locales/zh-TW/main.json' with { type: 'json' } -import zhTWNodes from './locales/zh-TW/nodeDefs.json' with { type: 'json' } -import zhTWSettings from './locales/zh-TW/settings.json' with { type: 'json' } -import zhCommands from './locales/zh/commands.json' with { type: 'json' } -import zh from './locales/zh/main.json' with { type: 'json' } -import zhNodes from './locales/zh/nodeDefs.json' with { type: 'json' } -import zhSettings from './locales/zh/settings.json' with { type: 'json' } - -function buildLocale(main: M, nodes: N, commands: C, settings: S) { + +function buildLocale< + M extends Record, + N extends Record, + C extends Record, + S extends Record +>(main: M, nodes: N, commands: C, settings: S) { return { ...main, nodeDefs: nodes, commands: commands, settings: settings + } as M & { nodeDefs: N; commands: C; settings: S } +} + +// Locale loader map - dynamically import locales only when needed +const localeLoaders: Record< + string, + () => Promise<{ default: Record }> +> = { + ar: () => import('./locales/ar/main.json'), + es: () => import('./locales/es/main.json'), + fr: () => import('./locales/fr/main.json'), + ja: () => import('./locales/ja/main.json'), + ko: () => import('./locales/ko/main.json'), + ru: () => import('./locales/ru/main.json'), + tr: () => import('./locales/tr/main.json'), + zh: () => import('./locales/zh/main.json'), + 'zh-TW': () => import('./locales/zh-TW/main.json') +} + +const nodeDefsLoaders: Record< + string, + () => Promise<{ default: Record }> +> = { + ar: () => import('./locales/ar/nodeDefs.json'), + es: () => import('./locales/es/nodeDefs.json'), + fr: () => import('./locales/fr/nodeDefs.json'), + ja: () => import('./locales/ja/nodeDefs.json'), + ko: () => import('./locales/ko/nodeDefs.json'), + ru: () => import('./locales/ru/nodeDefs.json'), + tr: () => import('./locales/tr/nodeDefs.json'), + zh: () => import('./locales/zh/nodeDefs.json'), + 'zh-TW': () => import('./locales/zh-TW/nodeDefs.json') +} + +const commandsLoaders: Record< + string, + () => Promise<{ default: Record }> +> = { + ar: () => import('./locales/ar/commands.json'), + es: () => import('./locales/es/commands.json'), + fr: () => import('./locales/fr/commands.json'), + ja: () => import('./locales/ja/commands.json'), + ko: () => import('./locales/ko/commands.json'), + ru: () => import('./locales/ru/commands.json'), + tr: () => import('./locales/tr/commands.json'), + zh: () => import('./locales/zh/commands.json'), + 'zh-TW': () => import('./locales/zh-TW/commands.json') +} + +const settingsLoaders: Record< + string, + () => Promise<{ default: Record }> +> = { + ar: () => import('./locales/ar/settings.json'), + es: () => import('./locales/es/settings.json'), + fr: () => import('./locales/fr/settings.json'), + ja: () => import('./locales/ja/settings.json'), + ko: () => import('./locales/ko/settings.json'), + ru: () => import('./locales/ru/settings.json'), + tr: () => import('./locales/tr/settings.json'), + zh: () => import('./locales/zh/settings.json'), + 'zh-TW': () => import('./locales/zh-TW/settings.json') +} + +// Track which locales have been loaded +const loadedLocales = new Set(['en']) + +// Track locales currently being loaded to prevent race conditions +const loadingLocales = new Map>() + +/** + * Dynamically load a locale and its associated files (nodeDefs, commands, settings) + */ +export async function loadLocale(locale: string): Promise { + if (loadedLocales.has(locale)) { + return } + + // If already loading, return the existing promise to prevent duplicate loads + const existingLoad = loadingLocales.get(locale) + if (existingLoad) { + return existingLoad + } + + const loader = localeLoaders[locale] + const nodeDefsLoader = nodeDefsLoaders[locale] + const commandsLoader = commandsLoaders[locale] + const settingsLoader = settingsLoaders[locale] + + if (!loader || !nodeDefsLoader || !commandsLoader || !settingsLoader) { + console.warn(`Locale "${locale}" is not supported`) + return + } + + // Create and track the loading promise + const loadPromise = (async () => { + try { + const [main, nodes, commands, settings] = await Promise.all([ + loader(), + nodeDefsLoader(), + commandsLoader(), + settingsLoader() + ]) + + const messages = buildLocale( + main.default, + nodes.default, + commands.default, + settings.default + ) + + i18n.global.setLocaleMessage(locale, messages as LocaleMessages) + loadedLocales.add(locale) + } catch (error) { + console.error(`Failed to load locale "${locale}":`, error) + throw error + } finally { + // Clean up the loading promise once complete + loadingLocales.delete(locale) + } + })() + + loadingLocales.set(locale, loadPromise) + return loadPromise } +// Only include English in the initial bundle const messages = { - en: buildLocale(en, enNodes, enCommands, enSettings), - zh: buildLocale(zh, zhNodes, zhCommands, zhSettings), - 'zh-TW': buildLocale(zhTW, zhTWNodes, zhTWCommands, zhTWSettings), - ru: buildLocale(ru, ruNodes, ruCommands, ruSettings), - ja: buildLocale(ja, jaNodes, jaCommands, jaSettings), - ko: buildLocale(ko, koNodes, koCommands, koSettings), - fr: buildLocale(fr, frNodes, frCommands, frSettings), - es: buildLocale(es, esNodes, esCommands, esSettings), - ar: buildLocale(ar, arNodes, arCommands, arSettings), - tr: buildLocale(tr, trNodes, trCommands, trSettings) + en: buildLocale(en, enNodes, enCommands, enSettings) } +// Type for locale messages - inferred from the English locale structure +type LocaleMessages = typeof messages.en + export const i18n = createI18n({ // Must set `false`, as Vue I18n Legacy API is for Vue 2 legacy: false, diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index fb889305c2..68aaa7fed5 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -45,7 +45,7 @@ import { useCoreCommands } from '@/composables/useCoreCommands' import { useErrorHandling } from '@/composables/useErrorHandling' import { useProgressFavicon } from '@/composables/useProgressFavicon' import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig' -import { i18n } from '@/i18n' +import { i18n, loadLocale } from '@/i18n' import { useSettingStore } from '@/platform/settings/settingStore' import { useFrontendVersionMismatchWarning } from '@/platform/updates/common/useFrontendVersionMismatchWarning' import { useVersionCompatibilityStore } from '@/platform/updates/common/versionCompatibilityStore' @@ -145,10 +145,17 @@ watchEffect(() => { ) }) -watchEffect(() => { +watchEffect(async () => { const locale = settingStore.get('Comfy.Locale') if (locale) { - i18n.global.locale.value = locale as 'en' | 'zh' | 'ru' | 'ja' + // Load the locale dynamically if not already loaded + try { + await loadLocale(locale) + // Type assertion is safe here as loadLocale validates the locale exists + i18n.global.locale.value = locale as typeof i18n.global.locale.value + } catch (error) { + console.error(`Failed to switch to locale "${locale}":`, error) + } } })