mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 11:01:15 +00:00
fix(editor): Fix i18n package locale files hot reloading (no-changelog) (#19385)
Co-authored-by: Csaba Tuncsik <csaba.tuncsik@gmail.com>
This commit is contained in:
@@ -1,34 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import '@/polyfills';
|
||||
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import LoadingView from '@/views/LoadingView.vue';
|
||||
import AssistantsHub from '@/components/AskAssistant/AssistantsHub.vue';
|
||||
import AskAssistantFloatingButton from '@/components/AskAssistant/Chat/AskAssistantFloatingButton.vue';
|
||||
import BannerStack from '@/components/banners/BannerStack.vue';
|
||||
import Modals from '@/components/Modals.vue';
|
||||
import Telemetry from '@/components/Telemetry.vue';
|
||||
import AskAssistantFloatingButton from '@/components/AskAssistant/Chat/AskAssistantFloatingButton.vue';
|
||||
import AssistantsHub from '@/components/AskAssistant/AssistantsHub.vue';
|
||||
import { loadLanguage } from '@n8n/i18n';
|
||||
import { useHistoryHelper } from '@/composables/useHistoryHelper';
|
||||
import { useTelemetryContext } from '@/composables/useTelemetryContext';
|
||||
import { useWorkflowDiffRouting } from '@/composables/useWorkflowDiffRouting';
|
||||
import {
|
||||
APP_MODALS_ELEMENT_ID,
|
||||
CODEMIRROR_TOOLTIP_CONTAINER_ELEMENT_ID,
|
||||
HIRING_BANNER,
|
||||
VIEWS,
|
||||
} from '@/constants';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { useAssistantStore } from '@/stores/assistant.store';
|
||||
import { useBuilderStore } from '@/stores/builder.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useHistoryHelper } from '@/composables/useHistoryHelper';
|
||||
import { useWorkflowDiffRouting } from '@/composables/useWorkflowDiffRouting';
|
||||
import { useStyles } from './composables/useStyles';
|
||||
import LoadingView from '@/views/LoadingView.vue';
|
||||
import { locale } from '@n8n/design-system';
|
||||
import { setLanguage } from '@n8n/i18n';
|
||||
// Note: no need to import en.json here; default 'en' is handled via setLanguage
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import axios from 'axios';
|
||||
import { useTelemetryContext } from '@/composables/useTelemetryContext';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStyles } from './composables/useStyles';
|
||||
|
||||
const route = useRoute();
|
||||
const rootStore = useRootStore();
|
||||
@@ -100,13 +101,16 @@ watch(route, (r) => {
|
||||
|
||||
watch(
|
||||
defaultLocale,
|
||||
(newLocale) => {
|
||||
void loadLanguage(newLocale);
|
||||
void locale.use(newLocale);
|
||||
async (newLocale) => {
|
||||
setLanguage(newLocale);
|
||||
|
||||
axios.defaults.headers.common['Accept-Language'] = newLocale;
|
||||
void locale.use(newLocale);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// Dev HMR for i18n is imported in main.ts before app mount
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -2,6 +2,8 @@ import '@testing-library/jest-dom';
|
||||
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 { APP_MODALS_ELEMENT_ID } from '@/constants';
|
||||
|
||||
// Avoid tests failing because of difference between local and GitHub actions timezone
|
||||
@@ -144,3 +146,5 @@ Object.defineProperty(HTMLElement.prototype, 'scrollTo', {
|
||||
writable: true,
|
||||
value: vi.fn(),
|
||||
});
|
||||
|
||||
loadLanguage('en', englishBaseText);
|
||||
|
||||
69
packages/frontend/editor-ui/src/dev/i18nHmr.ts
Normal file
69
packages/frontend/editor-ui/src/dev/i18nHmr.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { i18n, i18nInstance, setLanguage, updateLocaleMessages } from '@n8n/i18n';
|
||||
import type { LocaleMessages } from '@n8n/i18n/types';
|
||||
import { locale as designLocale } from '@n8n/design-system';
|
||||
|
||||
const hot = import.meta.hot;
|
||||
const DEFAULT_LOCALE = 'en';
|
||||
|
||||
if (hot) {
|
||||
// Eagerly import locale JSONs so this module becomes their HMR owner
|
||||
const localeModules = import.meta.glob('@n8n/i18n/locales/*.json', { eager: true }) as Record<
|
||||
string,
|
||||
{ default?: LocaleMessages }
|
||||
>;
|
||||
const localePaths = Object.keys(localeModules);
|
||||
|
||||
const lcOf = (p: string) => p.match(/\/locales\/([^/]+)\.json$/)?.[1] ?? DEFAULT_LOCALE;
|
||||
const apply = (lc: string, msgs: LocaleMessages) => updateLocaleMessages(lc, msgs);
|
||||
|
||||
// Seed all locales on initial load in dev so switching locales
|
||||
// does not require component-level dynamic imports (avoids hard reload chains)
|
||||
for (const p of localePaths) {
|
||||
const lc = lcOf(p);
|
||||
const msgs = (localeModules[p] as { default?: LocaleMessages })?.default;
|
||||
if (msgs && lc) apply(lc, msgs);
|
||||
}
|
||||
const refresh = () => {
|
||||
const current = (i18nInstance.global.locale.value as string) || DEFAULT_LOCALE;
|
||||
i18n.clearCache();
|
||||
setLanguage(current);
|
||||
void designLocale.use(current);
|
||||
};
|
||||
// Fetch with cache-buster to avoid stale content (one-update-behind);
|
||||
// falls back to the eager map if network fetch is unavailable.
|
||||
const fetchAndApply = async (file: string) => {
|
||||
try {
|
||||
const res = await fetch(`/@fs${file}?t=${Date.now()}`);
|
||||
apply(lcOf(file), (await res.json()) as LocaleMessages);
|
||||
} catch {
|
||||
const msgs = localeModules[file]?.default;
|
||||
if (msgs) apply(lcOf(file), msgs);
|
||||
}
|
||||
};
|
||||
|
||||
// 1) Apply fresh modules provided by Vite HMR
|
||||
hot.accept(localePaths, (mods) => {
|
||||
mods.forEach((mod, i) => apply(lcOf(localePaths[i] ?? DEFAULT_LOCALE), mod?.default ?? {}));
|
||||
refresh();
|
||||
});
|
||||
|
||||
// 2) Handle explicit locale update events (fetch ensures latest content)
|
||||
hot.on('n8n:locale-update', async (payload: { locales?: string[]; file?: string }) => {
|
||||
if (payload.file) await fetchAndApply(payload.file);
|
||||
refresh();
|
||||
});
|
||||
|
||||
// 3) Last resort for cases where accept doesn’t trigger
|
||||
hot.on(
|
||||
'vite:afterUpdate',
|
||||
async (payload: { updates?: Array<{ path?: string; acceptedPath?: string }> }) => {
|
||||
const updates = payload?.updates ?? [];
|
||||
const files = updates
|
||||
.map((u) => (u.path ?? u.acceptedPath ?? '') as string)
|
||||
.filter((p) => p.includes('/locales/') && p.endsWith('.json'));
|
||||
if (files.length === 0) return;
|
||||
for (const file of files) await fetchAndApply(file);
|
||||
refresh();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,8 @@ import { useInsightsStore } from '@/features/insights/insights.store';
|
||||
import type { InsightsDateRange } from '@n8n/api-types';
|
||||
import { N8nOption, N8nSelect } from '@n8n/design-system';
|
||||
import { ref } from 'vue';
|
||||
import { TIME_RANGE_LABELS, UNLICENSED_TIME_RANGE } from '../insights.constants';
|
||||
import { UNLICENSED_TIME_RANGE } from '../insights.constants';
|
||||
import { getTimeRangeLabels } from '../insights.utils';
|
||||
|
||||
const model = defineModel<InsightsDateRange['key'] | typeof UNLICENSED_TIME_RANGE>({
|
||||
required: true,
|
||||
@@ -11,11 +12,13 @@ const model = defineModel<InsightsDateRange['key'] | typeof UNLICENSED_TIME_RANG
|
||||
|
||||
const insightsStore = useInsightsStore();
|
||||
|
||||
const timeRangeLabels = getTimeRangeLabels();
|
||||
|
||||
const timeOptions = ref(
|
||||
insightsStore.dateRanges.map((option) => {
|
||||
return {
|
||||
key: option.key,
|
||||
label: TIME_RANGE_LABELS[option.key],
|
||||
label: timeRangeLabels[option.key],
|
||||
value: option.licensed ? option.key : UNLICENSED_TIME_RANGE,
|
||||
licensed: option.licensed,
|
||||
};
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { VIEWS } from '@/constants';
|
||||
import {
|
||||
INSIGHT_IMPACT_TYPES,
|
||||
INSIGHTS_UNIT_IMPACT_MAPPING,
|
||||
TIME_RANGE_LABELS,
|
||||
} from '@/features/insights/insights.constants';
|
||||
import type { InsightsSummaryDisplay } from '@/features/insights/insights.types';
|
||||
import type { InsightsDateRange, InsightsSummary } from '@n8n/api-types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
||||
import { computed, useCssModule } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { I18nT } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getTimeRangeLabels } from '../insights.utils';
|
||||
|
||||
const props = defineProps<{
|
||||
summary: InsightsSummaryDisplay;
|
||||
@@ -25,6 +25,8 @@ const route = useRoute();
|
||||
const $style = useCssModule();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const timeRangeLabels = getTimeRangeLabels();
|
||||
|
||||
const summaryTitles = computed<Record<keyof InsightsSummary, string>>(() => ({
|
||||
total: i18n.baseText('insights.banner.title.total'),
|
||||
failed: i18n.baseText('insights.banner.title.failed'),
|
||||
@@ -94,7 +96,7 @@ const trackTabClick = (insightType: keyof InsightsSummary) => {
|
||||
</N8nTooltip>
|
||||
</strong>
|
||||
<small :class="$style.days">
|
||||
{{ TIME_RANGE_LABELS[timeRange] }}
|
||||
{{ timeRangeLabels[timeRange] }}
|
||||
</small>
|
||||
<span v-if="value === 0 && id === 'timeSaved'" :class="$style.empty">
|
||||
<em>--</em>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { InsightsSummaryType } from '@n8n/api-types';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import dateformat from 'dateformat';
|
||||
|
||||
export const INSIGHT_TYPES = {
|
||||
@@ -79,14 +78,4 @@ export const TELEMETRY_TIME_RANGE = {
|
||||
year: 365,
|
||||
};
|
||||
|
||||
export const TIME_RANGE_LABELS = {
|
||||
day: useI18n().baseText('insights.lastNHours', { interpolate: { count: 24 } }),
|
||||
week: useI18n().baseText('insights.lastNDays', { interpolate: { count: 7 } }),
|
||||
'2weeks': useI18n().baseText('insights.lastNDays', { interpolate: { count: 14 } }),
|
||||
month: useI18n().baseText('insights.lastNDays', { interpolate: { count: 30 } }),
|
||||
quarter: useI18n().baseText('insights.lastNDays', { interpolate: { count: 90 } }),
|
||||
'6months': useI18n().baseText('insights.months', { interpolate: { count: 6 } }),
|
||||
year: useI18n().baseText('insights.oneYear'),
|
||||
};
|
||||
|
||||
export const UNLICENSED_TIME_RANGE = 'UNLICENSED_TIME_RANGE' as const;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import type { InsightsSummary, InsightsSummaryType } from '@n8n/api-types';
|
||||
import type { InsightsSummaryDisplay } from '@/features/insights/insights.types';
|
||||
import {
|
||||
@@ -57,3 +58,17 @@ export const transformInsightsSummary = (data: InsightsSummary | null): Insights
|
||||
unit: INSIGHTS_UNIT_MAPPING[key](data[key].value),
|
||||
}))
|
||||
: [];
|
||||
|
||||
export const getTimeRangeLabels = () => {
|
||||
const i18n = useI18n();
|
||||
|
||||
return {
|
||||
day: i18n.baseText('insights.lastNHours', { interpolate: { count: 24 } }),
|
||||
week: i18n.baseText('insights.lastNDays', { interpolate: { count: 7 } }),
|
||||
'2weeks': i18n.baseText('insights.lastNDays', { interpolate: { count: 14 } }),
|
||||
month: i18n.baseText('insights.lastNDays', { interpolate: { count: 30 } }),
|
||||
quarter: i18n.baseText('insights.lastNDays', { interpolate: { count: 90 } }),
|
||||
'6months': i18n.baseText('insights.months', { interpolate: { count: 6 } }),
|
||||
year: i18n.baseText('insights.oneYear'),
|
||||
};
|
||||
};
|
||||
|
||||
7
packages/frontend/editor-ui/src/i18n/loadDefaultEn.ts
Normal file
7
packages/frontend/editor-ui/src/i18n/loadDefaultEn.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
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);
|
||||
}
|
||||
@@ -15,7 +15,8 @@ import './n8n-theme.scss';
|
||||
import App from '@/App.vue';
|
||||
import router from './router';
|
||||
|
||||
import { i18nInstance } from '@n8n/i18n';
|
||||
import { i18nInstance, setLanguage } from '@n8n/i18n';
|
||||
|
||||
import { TelemetryPlugin } from './plugins/telemetry';
|
||||
import { GlobalComponentsPlugin } from './plugins/components';
|
||||
import { GlobalDirectivesPlugin } from './plugins/directives';
|
||||
@@ -34,6 +35,17 @@ 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);
|
||||
|
||||
1
packages/frontend/editor-ui/src/shims.d.ts
vendored
1
packages/frontend/editor-ui/src/shims.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-comlink/client" />
|
||||
|
||||
import type { VNode, ComponentPublicInstance } from 'vue';
|
||||
|
||||
Reference in New Issue
Block a user