fix(editor): Convert autocompleteUIValues to a getter and streamline i18n HMR import (no-changelog) (#19560)

This commit is contained in:
Csaba Tuncsik
2025-09-16 10:55:46 +02:00
committed by GitHub
parent a400716d37
commit 7d988dbf84
7 changed files with 36 additions and 52 deletions

View File

@@ -1,5 +1,6 @@
/* eslint-disable id-denylist */
import { I18nClass, loadLanguage, i18nInstance } from './index';
import type { LocaleMessages } from './types';
// Store original state for cleanup
let originalLocale: string;
@@ -16,7 +17,7 @@ describe(I18nClass, () => {
minShort: 'm',
hrsShort: 'h',
},
});
} as unknown as LocaleMessages);
originalLocale = i18nInstance.global.locale.value;
originalHtmlLang = document.querySelector('html')?.getAttribute('lang') ?? 'en';
});
@@ -78,7 +79,7 @@ describe('loadLanguage', () => {
const messages = {
hello: 'Hallo',
world: 'Welt',
};
} as unknown as LocaleMessages;
const result = loadLanguage(locale, messages);
@@ -91,7 +92,7 @@ describe('loadLanguage', () => {
it('should set the HTML lang attribute when loading a language', () => {
const locale = 'fr';
const messages = { greeting: 'Bonjour' };
const messages = { greeting: 'Bonjour' } as unknown as LocaleMessages;
loadLanguage(locale, messages);
@@ -109,7 +110,7 @@ describe('loadLanguage', () => {
currency: 'EUR',
},
},
};
} as unknown as LocaleMessages;
loadLanguage(locale, messages);
@@ -122,8 +123,8 @@ describe('loadLanguage', () => {
it('should not reload a language if it has already been loaded', () => {
const locale = 'es';
const originalMessages = { hello: 'Hola' };
const newMessages = { hello: 'Buenos días' };
const originalMessages = { hello: 'Hola' } as unknown as LocaleMessages;
const newMessages = { hello: 'Buenos días' } as unknown as LocaleMessages;
// Load the language for the first time
loadLanguage(locale, originalMessages);
@@ -144,7 +145,7 @@ describe('loadLanguage', () => {
it('should handle empty messages object', () => {
const locale = 'it';
const messages = {};
const messages = {} as unknown as LocaleMessages;
const result = loadLanguage(locale, messages);
@@ -162,7 +163,7 @@ describe('loadLanguage', () => {
},
},
simple: 'Simples',
};
} as unknown as LocaleMessages;
loadLanguage(locale, messages);
@@ -187,7 +188,7 @@ describe('loadLanguage', () => {
minimumFractionDigits: 2,
},
},
};
} as unknown as LocaleMessages;
loadLanguage(locale, messages);
@@ -205,17 +206,17 @@ describe('loadLanguage', () => {
const locale2 = 'de-switch';
// Load first language
loadLanguage(locale1, { hello: 'Bonjour' });
loadLanguage(locale1, { hello: 'Bonjour' } as unknown as LocaleMessages);
expect(i18nInstance.global.locale.value).toBe(locale1);
expect(i18nInstance.global.t('hello')).toBe('Bonjour');
// Load second language
loadLanguage(locale2, { hello: 'Hallo' });
loadLanguage(locale2, { hello: 'Hallo' } as unknown as LocaleMessages);
expect(i18nInstance.global.locale.value).toBe(locale2);
expect(i18nInstance.global.t('hello')).toBe('Hallo');
// Switch back to first language (should not reload messages)
loadLanguage(locale1, { hello: 'Salut' }); // Different message
loadLanguage(locale1, { hello: 'Salut' } as unknown as LocaleMessages); // Different message
expect(i18nInstance.global.locale.value).toBe(locale1);
expect(i18nInstance.global.t('hello')).toBe('Bonjour'); // Should be original message
testLocales.add(locale1);
@@ -224,7 +225,7 @@ describe('loadLanguage', () => {
it('should return the locale that was set', () => {
const locale = 'nl';
const messages = { test: 'test' };
const messages = { test: 'test' } as unknown as LocaleMessages;
const result = loadLanguage(locale, messages);
@@ -239,7 +240,7 @@ describe('loadLanguage', () => {
special: '特殊字符测试',
emoji: '🚀 测试 🎉',
mixed: 'Mixed 混合 content',
};
} as unknown as LocaleMessages;
loadLanguage(locale, messages);
@@ -256,7 +257,7 @@ describe('loadLanguage', () => {
defined: 'Valid message',
undefined,
null: null,
};
} as unknown as LocaleMessages;
loadLanguage(locale, messages);
@@ -275,7 +276,7 @@ describe('loadLanguage', () => {
nested: {
list: ['a', 'b', 'c'],
},
};
} as unknown as LocaleMessages;
loadLanguage(locale, messages);
@@ -290,7 +291,7 @@ describe('loadLanguage', () => {
const currentLocale = 'en';
const messages = {
newMessage: 'This is a new message',
};
} as unknown as LocaleMessages;
// Ensure we're starting with English
i18nInstance.global.locale.value = currentLocale;
@@ -322,7 +323,7 @@ describe('loadLanguage', () => {
maximumFractionDigits: 3,
},
},
};
} as unknown as LocaleMessages;
loadLanguage(locale, messages);
@@ -347,7 +348,7 @@ describe('loadLanguage', () => {
number: 42,
zero: 0,
string: 'actual string',
};
} as unknown as LocaleMessages;
loadLanguage(locale, messages);
@@ -369,10 +370,10 @@ describe('loadLanguage', () => {
const locale1 = 'preserve-1';
const locale2 = 'preserve-2';
loadLanguage(locale1, { test: 'test1' });
loadLanguage(locale1, { test: 'test1' } as unknown as LocaleMessages);
expect(html?.getAttribute('lang')).toBe(locale1);
loadLanguage(locale2, { test: 'test2' });
loadLanguage(locale2, { test: 'test2' } as unknown as LocaleMessages);
expect(html?.getAttribute('lang')).toBe(locale2);
// Restore original

View File

@@ -3,7 +3,8 @@ import type { INodeProperties, INodePropertyCollection, INodePropertyOptions } f
import { ref } from 'vue';
import { createI18n } from 'vue-i18n';
import type { BaseTextKey, INodeTranslationHeaders } from './types';
import englishBaseText from './locales/en.json';
import type { BaseTextKey, LocaleMessages, INodeTranslationHeaders } from './types';
import {
deriveMiddleKey,
isNestedInCollectionLike,
@@ -17,7 +18,7 @@ export const i18nInstance = createI18n({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
messages: { en: {} },
messages: { en: englishBaseText },
warnHtmlMessage: false,
});
@@ -402,7 +403,7 @@ export function setLanguage(locale: string) {
return locale;
}
export function loadLanguage(locale: string, messages: Record<string, unknown>) {
export function loadLanguage(locale: string, messages: LocaleMessages) {
if (loadedLanguages.includes(locale)) {
return setLanguage(locale);
}
@@ -440,10 +441,8 @@ export function addNodeTranslation(
* Dev/runtime helper to replace messages for a locale without import side-effects.
* Used by editor UI HMR to apply updated translation JSON.
*/
export function updateLocaleMessages(locale: string, messages: Record<string, unknown>) {
const { numberFormats, ...rest } = messages as Record<string, unknown> & {
numberFormats?: Record<string, unknown>;
};
export function updateLocaleMessages(locale: string, messages: LocaleMessages) {
const { numberFormats, ...rest } = messages;
i18nInstance.global.setLocaleMessage(locale, rest);
if (numberFormats) i18nInstance.global.setNumberFormat(locale, numberFormats);

View File

@@ -3,7 +3,9 @@ import type englishBaseText from './locales/en.json';
export type GetBaseTextKey<T> = T extends `_${string}` ? never : T;
export type BaseTextKey = GetBaseTextKey<keyof typeof englishBaseText>;
export type LocaleMessages = typeof englishBaseText;
export type LocaleMessages = typeof englishBaseText & {
numberFormats: { [key: string]: Intl.NumberFormatOptions };
};
export interface INodeTranslationHeaders {
data: {

View File

@@ -109,8 +109,6 @@ watch(
},
{ immediate: true },
);
// Dev HMR for i18n is imported in main.ts before app mount
</script>
<template>

View File

@@ -3,7 +3,7 @@ import 'fake-indexeddb/auto';
import { configure } from '@testing-library/vue';
import 'core-js/proposals/set-methods-v2';
import englishBaseText from '@n8n/i18n/locales/en.json';
import { loadLanguage } from '@n8n/i18n';
import { loadLanguage, type LocaleMessages } from '@n8n/i18n';
import { APP_MODALS_ELEMENT_ID } from '@/constants';
// Avoid tests failing because of difference between local and GitHub actions timezone
@@ -147,4 +147,4 @@ Object.defineProperty(HTMLElement.prototype, 'scrollTo', {
value: vi.fn(),
});
loadLanguage('en', englishBaseText);
loadLanguage('en', englishBaseText as LocaleMessages);

View File

@@ -1,7 +0,0 @@
import { loadLanguage } from '@n8n/i18n';
import type { LocaleMessages } from '@n8n/i18n/types';
export async function loadDefaultEn() {
const mod = (await import('@n8n/i18n/locales/en.json')) as { default: LocaleMessages };
loadLanguage('en', mod.default);
}

View File

@@ -11,11 +11,13 @@ import '@n8n/design-system/css/index.scss';
// import '@n8n/design-system/css/tailwind/index.css';
import './n8n-theme.scss';
// Ensure i18n HMR owner is evaluated as early as possible in dev
import '@/dev/i18nHmr';
import App from '@/App.vue';
import router from './router';
import { i18nInstance, setLanguage } from '@n8n/i18n';
import { i18nInstance } from '@n8n/i18n';
import { TelemetryPlugin } from './plugins/telemetry';
import { GlobalComponentsPlugin } from './plugins/components';
@@ -35,17 +37,6 @@ const app = createApp(App);
app.use(SentryPlugin);
// Initialize i18n
if (import.meta.env.DEV) {
// Import HMR owner early so messages are seeded before app mount
await import('@/dev/i18nHmr');
setLanguage('en');
} else {
// Production: load English messages explicitly via isolated module
const { loadDefaultEn } = await import('@/i18n/loadDefaultEn');
await loadDefaultEn();
}
// Register module routes
// We do this here so landing straight on a module page works
registerModuleRoutes(router);