mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-07 08:30:06 +00:00
[backport core/1.33] fix: preserve custom nodes i18n data when locales are lazily loaded (#7259)
Backport of #7214 to `core/1.33` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7259-backport-core-1-33-fix-preserve-custom-nodes-i18n-data-when-locales-are-lazily-loaded-2c46d73d3650813ea30bff1b39e78bd2) by [Unito](https://www.unito.io) Co-authored-by: Terry Jia <terryjia88@gmail.com>
This commit is contained in:
@@ -125,7 +125,7 @@ import { useCopy } from '@/composables/useCopy'
|
||||
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
||||
import { usePaste } from '@/composables/usePaste'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import { mergeCustomNodesI18n, t } from '@/i18n'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useLitegraphSettings } from '@/platform/settings/composables/useLitegraphSettings'
|
||||
import { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'
|
||||
@@ -384,9 +384,7 @@ useEventListener(
|
||||
const loadCustomNodesI18n = async () => {
|
||||
try {
|
||||
const i18nData = await api.getCustomNodesI18n()
|
||||
Object.entries(i18nData).forEach(([locale, message]) => {
|
||||
i18n.global.mergeLocaleMessage(locale, message)
|
||||
})
|
||||
mergeCustomNodesI18n(i18nData)
|
||||
} catch (error) {
|
||||
console.error('Failed to load custom nodes i18n', error)
|
||||
}
|
||||
|
||||
200
src/i18n.test.ts
Normal file
200
src/i18n.test.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
const { i18n, loadLocale, mergeCustomNodesI18n } = await import('./i18n')
|
||||
|
||||
// Mock the JSON imports before importing i18n module
|
||||
vi.mock('./locales/en/main.json', () => ({ default: { welcome: 'Welcome' } }))
|
||||
vi.mock('./locales/en/nodeDefs.json', () => ({
|
||||
default: { testNode: 'Test Node' }
|
||||
}))
|
||||
vi.mock('./locales/en/commands.json', () => ({
|
||||
default: { save: 'Save' }
|
||||
}))
|
||||
vi.mock('./locales/en/settings.json', () => ({
|
||||
default: { theme: 'Theme' }
|
||||
}))
|
||||
|
||||
// Mock lazy-loaded locales
|
||||
vi.mock('./locales/zh/main.json', () => ({ default: { welcome: '欢迎' } }))
|
||||
vi.mock('./locales/zh/nodeDefs.json', () => ({
|
||||
default: { testNode: '测试节点' }
|
||||
}))
|
||||
vi.mock('./locales/zh/commands.json', () => ({ default: { save: '保存' } }))
|
||||
vi.mock('./locales/zh/settings.json', () => ({ default: { theme: '主题' } }))
|
||||
|
||||
describe('i18n', () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
describe('mergeCustomNodesI18n', () => {
|
||||
it('should immediately merge data for already loaded locales (en)', async () => {
|
||||
// English is pre-loaded, so merge should work immediately
|
||||
mergeCustomNodesI18n({
|
||||
en: {
|
||||
customNode: {
|
||||
title: 'Custom Node Title'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Verify the custom node data was merged
|
||||
const messages = i18n.global.getLocaleMessage('en') as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
expect(messages.customNode).toEqual({ title: 'Custom Node Title' })
|
||||
})
|
||||
|
||||
it('should store data for not-yet-loaded locales', async () => {
|
||||
const { i18n, mergeCustomNodesI18n } = await import('./i18n')
|
||||
|
||||
// Chinese is not pre-loaded, data should be stored but not merged yet
|
||||
mergeCustomNodesI18n({
|
||||
zh: {
|
||||
customNode: {
|
||||
title: '自定义节点标题'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// zh locale should not exist yet (not loaded)
|
||||
const zhMessages = i18n.global.getLocaleMessage('zh') as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
// Either empty or doesn't have our custom data merged directly
|
||||
// (since zh wasn't loaded yet, mergeLocaleMessage on non-existent locale
|
||||
// may create an empty locale or do nothing useful)
|
||||
expect(zhMessages.customNode).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should merge stored data when locale is lazily loaded', async () => {
|
||||
// First, store custom nodes i18n data (before locale is loaded)
|
||||
mergeCustomNodesI18n({
|
||||
zh: {
|
||||
customNode: {
|
||||
title: '自定义节点标题'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await loadLocale('zh')
|
||||
|
||||
// Verify both the base locale data and custom node data are present
|
||||
const zhMessages = i18n.global.getLocaleMessage('zh') as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
expect(zhMessages.welcome).toBe('欢迎')
|
||||
expect(zhMessages.customNode).toEqual({ title: '自定义节点标题' })
|
||||
})
|
||||
|
||||
it('should preserve custom node data when locale is loaded after merge', async () => {
|
||||
// Simulate the real scenario:
|
||||
// 1. Custom nodes i18n is loaded first
|
||||
mergeCustomNodesI18n({
|
||||
zh: {
|
||||
customNode: {
|
||||
title: '自定义节点标题'
|
||||
},
|
||||
settingsCategories: {
|
||||
Hotkeys: '快捷键'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Then locale is lazily loaded (this would previously overwrite custom data)
|
||||
await loadLocale('zh')
|
||||
|
||||
// 3. Verify custom node data is still present
|
||||
const zhMessages = i18n.global.getLocaleMessage('zh') as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
expect(zhMessages.customNode).toEqual({ title: '自定义节点标题' })
|
||||
expect(zhMessages.settingsCategories).toEqual({ Hotkeys: '快捷键' })
|
||||
|
||||
// 4. Also verify base locale data is present
|
||||
expect(zhMessages.welcome).toBe('欢迎')
|
||||
expect(zhMessages.nodeDefs).toEqual({ testNode: '测试节点' })
|
||||
})
|
||||
|
||||
it('should handle multiple locales in custom nodes i18n data', async () => {
|
||||
// Merge data for multiple locales
|
||||
mergeCustomNodesI18n({
|
||||
en: {
|
||||
customPlugin: { name: 'Easy Use' }
|
||||
},
|
||||
zh: {
|
||||
customPlugin: { name: '简单使用' }
|
||||
}
|
||||
})
|
||||
|
||||
// English should be merged immediately (pre-loaded)
|
||||
const enMessages = i18n.global.getLocaleMessage('en') as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
expect(enMessages.customPlugin).toEqual({ name: 'Easy Use' })
|
||||
|
||||
await loadLocale('zh')
|
||||
const zhMessages = i18n.global.getLocaleMessage('zh') as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
expect(zhMessages.customPlugin).toEqual({ name: '简单使用' })
|
||||
})
|
||||
|
||||
it('should handle calling mergeCustomNodesI18n multiple times', async () => {
|
||||
// Use fresh module instance to ensure clean state
|
||||
vi.resetModules()
|
||||
const { i18n, loadLocale, mergeCustomNodesI18n } = await import('./i18n')
|
||||
|
||||
mergeCustomNodesI18n({
|
||||
zh: { plugin1: { name: '插件1' } }
|
||||
})
|
||||
|
||||
mergeCustomNodesI18n({
|
||||
zh: { plugin2: { name: '插件2' } }
|
||||
})
|
||||
|
||||
await loadLocale('zh')
|
||||
|
||||
const zhMessages = i18n.global.getLocaleMessage('zh') as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
// Only the second call's data should be present
|
||||
expect(zhMessages.plugin2).toEqual({ name: '插件2' })
|
||||
// First call's data is overwritten
|
||||
expect(zhMessages.plugin1).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadLocale', () => {
|
||||
it('should not reload already loaded locale', async () => {
|
||||
await loadLocale('zh')
|
||||
await loadLocale('zh')
|
||||
|
||||
// Should complete without error (second call returns early)
|
||||
})
|
||||
|
||||
it('should warn for unsupported locale', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
await loadLocale('unsupported-locale')
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Locale "unsupported-locale" is not supported'
|
||||
)
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should handle concurrent load requests for same locale', async () => {
|
||||
// Start multiple loads concurrently
|
||||
const promises = [loadLocale('zh'), loadLocale('zh'), loadLocale('zh')]
|
||||
|
||||
await Promise.all(promises)
|
||||
})
|
||||
})
|
||||
})
|
||||
25
src/i18n.ts
25
src/i18n.ts
@@ -90,6 +90,9 @@ const loadedLocales = new Set<string>(['en'])
|
||||
// Track locales currently being loaded to prevent race conditions
|
||||
const loadingLocales = new Map<string, Promise<void>>()
|
||||
|
||||
// Store custom nodes i18n data for merging when locales are lazily loaded
|
||||
const customNodesI18nData: Record<string, unknown> = {}
|
||||
|
||||
/**
|
||||
* Dynamically load a locale and its associated files (nodeDefs, commands, settings)
|
||||
*/
|
||||
@@ -133,6 +136,10 @@ export async function loadLocale(locale: string): Promise<void> {
|
||||
|
||||
i18n.global.setLocaleMessage(locale, messages as LocaleMessages)
|
||||
loadedLocales.add(locale)
|
||||
|
||||
if (customNodesI18nData[locale]) {
|
||||
i18n.global.mergeLocaleMessage(locale, customNodesI18nData[locale])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load locale "${locale}":`, error)
|
||||
throw error
|
||||
@@ -146,6 +153,24 @@ export async function loadLocale(locale: string): Promise<void> {
|
||||
return loadPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the data for later use when locales are lazily loaded,
|
||||
* and immediately merges data for already-loaded locales.
|
||||
*/
|
||||
export function mergeCustomNodesI18n(i18nData: Record<string, unknown>): void {
|
||||
// Clear existing data and replace with new data
|
||||
for (const key of Object.keys(customNodesI18nData)) {
|
||||
delete customNodesI18nData[key]
|
||||
}
|
||||
Object.assign(customNodesI18nData, i18nData)
|
||||
|
||||
for (const [locale, message] of Object.entries(i18nData)) {
|
||||
if (loadedLocales.has(locale)) {
|
||||
i18n.global.mergeLocaleMessage(locale, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only include English in the initial bundle
|
||||
const messages = {
|
||||
en: buildLocale(en, enNodes, enCommands, enSettings)
|
||||
|
||||
Reference in New Issue
Block a user