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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import 'fake-indexeddb/auto';
import { configure } from '@testing-library/vue'; import { configure } from '@testing-library/vue';
import 'core-js/proposals/set-methods-v2'; import 'core-js/proposals/set-methods-v2';
import englishBaseText from '@n8n/i18n/locales/en.json'; 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'; import { APP_MODALS_ELEMENT_ID } from '@/constants';
// Avoid tests failing because of difference between local and GitHub actions timezone // Avoid tests failing because of difference between local and GitHub actions timezone
@@ -147,4 +147,4 @@ Object.defineProperty(HTMLElement.prototype, 'scrollTo', {
value: vi.fn(), 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/design-system/css/tailwind/index.css';
import './n8n-theme.scss'; 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 App from '@/App.vue';
import router from './router'; import router from './router';
import { i18nInstance, setLanguage } from '@n8n/i18n'; import { i18nInstance } from '@n8n/i18n';
import { TelemetryPlugin } from './plugins/telemetry'; import { TelemetryPlugin } from './plugins/telemetry';
import { GlobalComponentsPlugin } from './plugins/components'; import { GlobalComponentsPlugin } from './plugins/components';
@@ -35,17 +37,6 @@ const app = createApp(App);
app.use(SentryPlugin); 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 // Register module routes
// We do this here so landing straight on a module page works // We do this here so landing straight on a module page works
registerModuleRoutes(router); registerModuleRoutes(router);