Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,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'
Expand Down Expand Up @@ -391,9 +391,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)
}
Expand Down
196 changes: 196 additions & 0 deletions src/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
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 () => {
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()

Check failure on line 166 in src/i18n.test.ts

View workflow job for this annotation

GitHub Actions / test

src/i18n.test.ts > i18n > mergeCustomNodesI18n > should handle calling mergeCustomNodesI18n multiple times

AssertionError: expected { name: '插件1' } to be undefined - Expected: undefined + Received: { "name": "插件1", } ❯ src/i18n.test.ts:166:34

Check failure on line 166 in src/i18n.test.ts

View workflow job for this annotation

GitHub Actions / test

src/i18n.test.ts > i18n > mergeCustomNodesI18n > should handle calling mergeCustomNodesI18n multiple times

AssertionError: expected { name: '插件1' } to be undefined - Expected: undefined + Received: { "name": "插件1", } ❯ src/i18n.test.ts:166:34

Check failure on line 166 in src/i18n.test.ts

View workflow job for this annotation

GitHub Actions / test

src/i18n.test.ts > i18n > mergeCustomNodesI18n > should handle calling mergeCustomNodesI18n multiple times

AssertionError: expected { name: '插件1' } to be undefined - Expected: undefined + Received: { "name": "插件1", } ❯ src/i18n.test.ts:166:34
})
})

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)
})
})
})
22 changes: 22 additions & 0 deletions src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ 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
// to keep consistency as api.ts, it should use any as type
const customNodesI18nData: Record<string, any> = {}

/**
* Dynamically load a locale and its associated files (nodeDefs, commands, settings)
*/
Expand Down Expand Up @@ -137,6 +141,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
Expand All @@ -150,6 +158,20 @@ 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, any>): void {
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)
Expand Down
Loading