mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 22:59:14 +00:00
## Summary Custom nodes can provide localized translations via their locales folder. After the switch to lazy-loading locales (only English pre-loaded), custom node i18n data was being lost because: 1. loadCustomNodesI18n() merges data before locale is loaded 2. loadLocale() uses setLocaleMessage() which overwrites everything Solution: Store custom nodes i18n data and re-merge it after each lazy-loaded locale completes loading. - Add mergeCustomNodesI18n() function to store and merge custom data - Modify loadLocale() to re-merge stored data after loading - Add unit tests for the i18n merging behavior fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/7025 ## Screenshots (if applicable) <img width="706" height="1335" alt="image" src="https://github.com/user-attachments/assets/41e4ba2c-b4c0-4d0d-a104-1ede8939ff92" /> <img width="672" height="1319" alt="image" src="https://github.com/user-attachments/assets/6c3e55e5-20d2-4093-a86a-7496db3dfe94" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7214-fix-preserve-custom-nodes-i18n-data-when-locales-are-lazily-loaded-2c26d73d3650812fab31edd71cf51a19) by [Unito](https://www.unito.io)
201 lines
6.2 KiB
TypeScript
201 lines
6.2 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|