fix: add mergeWithEnglishFallback for i18n translations (#26016)
* fix * fix * fix * fix * Improve translation cache handling in i18n.ts Refactor translation cache retrieval and fallback logic. --------- Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
This commit is contained in:
co-authored by
Anik Dhabal Babu
parent
71792bdeaa
commit
f291c1c4e7
@@ -32,11 +32,7 @@ const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
|
||||
const eventLocale = props.eventData?.interfaceLanguage;
|
||||
const ns = "common";
|
||||
let translations;
|
||||
if (eventLocale) {
|
||||
const ns = "common";
|
||||
translations = await loadTranslations(eventLocale, ns);
|
||||
}
|
||||
const translations = await loadTranslations(eventLocale ?? "en", ns);
|
||||
|
||||
if ((props as TeamTypePageProps)?.teamId) {
|
||||
return eventLocale ? (
|
||||
|
||||
@@ -68,11 +68,7 @@ const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
|
||||
const eventLocale = props.eventData?.interfaceLanguage;
|
||||
const ns = "common";
|
||||
let translations;
|
||||
if (eventLocale) {
|
||||
const ns = "common";
|
||||
translations = await loadTranslations(eventLocale, ns);
|
||||
}
|
||||
const translations = await loadTranslations(eventLocale ?? "en", ns);
|
||||
|
||||
if ((props as TeamTypePageProps)?.teamId) {
|
||||
return eventLocale ? (
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { mergeWithEnglishFallback } from "./i18n";
|
||||
|
||||
describe("mergeWithEnglishFallback", () => {
|
||||
it("should merge locale translations with English fallback", () => {
|
||||
const localeTranslations = {
|
||||
// Override some existing English keys
|
||||
hello: "Bonjour",
|
||||
goodbye: "Au revoir",
|
||||
// Add new locale-specific keys
|
||||
locale_only: "French only text",
|
||||
};
|
||||
|
||||
const result = mergeWithEnglishFallback(localeTranslations);
|
||||
|
||||
// Check that locale translations override English
|
||||
expect(result.hello).toBe("Bonjour");
|
||||
expect(result.goodbye).toBe("Au revoir");
|
||||
|
||||
// Check that locale-specific keys are preserved
|
||||
expect(result.locale_only).toBe("French only text");
|
||||
|
||||
// Check that English fallback keys are present (using actual keys from the real file)
|
||||
expect(result).toHaveProperty("welcome");
|
||||
expect(result).toHaveProperty("cancel");
|
||||
|
||||
// Verify the structure - should have both English and locale keys
|
||||
expect(Object.keys(result)).toContain("hello"); // overridden
|
||||
expect(Object.keys(result)).toContain("locale_only"); // locale-specific
|
||||
expect(Object.keys(result).length).toBeGreaterThan(3); // Should have many keys from English
|
||||
});
|
||||
|
||||
it("should use English translations as fallback when locale translations are empty", () => {
|
||||
const localeTranslations = {};
|
||||
|
||||
const result = mergeWithEnglishFallback(localeTranslations);
|
||||
|
||||
// Should return all English translations
|
||||
expect(result).toHaveProperty("welcome");
|
||||
expect(result).toHaveProperty("cancel");
|
||||
expect(Object.keys(result).length).toBeGreaterThan(100); // English file has many keys
|
||||
});
|
||||
|
||||
it("should prioritize locale translations over English when keys overlap", () => {
|
||||
const localeTranslations = {
|
||||
welcome: "Bienvenido",
|
||||
cancel: "Cancelar",
|
||||
};
|
||||
|
||||
const result = mergeWithEnglishFallback(localeTranslations);
|
||||
|
||||
// Spanish should override English
|
||||
expect(result.welcome).toBe("Bienvenido");
|
||||
expect(result.cancel).toBe("Cancelar");
|
||||
|
||||
// Should still have other English keys
|
||||
expect(Object.keys(result).length).toBeGreaterThan(100);
|
||||
});
|
||||
|
||||
it("should handle locale translations with additional keys not in English", () => {
|
||||
const localeTranslations = {
|
||||
// Override existing English key
|
||||
welcome: "Ciao",
|
||||
// Add new keys not in English
|
||||
italian_greeting: "Buongiorno",
|
||||
italian_phrase: "Come stai?",
|
||||
};
|
||||
|
||||
const result = mergeWithEnglishFallback(localeTranslations);
|
||||
|
||||
// Check overridden key
|
||||
expect(result.welcome).toBe("Ciao");
|
||||
|
||||
// Check Italian-specific keys
|
||||
expect(result.italian_greeting).toBe("Buongiorno");
|
||||
expect(result.italian_phrase).toBe("Come stai?");
|
||||
|
||||
// Should have English keys plus the new Italian ones
|
||||
expect(Object.keys(result).length).toBeGreaterThan(100);
|
||||
expect(Object.keys(result)).toContain("italian_greeting");
|
||||
expect(Object.keys(result)).toContain("italian_phrase");
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,6 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { fetchWithTimeout } from "../fetchWithTimeout";
|
||||
import logger from "../logger";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { i18n } = require("@calcom/config/next-i18next.config");
|
||||
const log = logger.getSubLogger({ prefix: ["[i18n]"] });
|
||||
|
||||
@@ -20,6 +19,15 @@ const translationCache = new Map<string, Record<string, string>>();
|
||||
const i18nInstanceCache = new Map<string, any>();
|
||||
const SUPPORTED_NAMESPACES = ["common"];
|
||||
|
||||
export function mergeWithEnglishFallback(localeTranslations: Record<string, string>): Record<string, string> {
|
||||
return {
|
||||
// IMPORTANT: Spread English translations first to provide fallback for missing keys
|
||||
...englishTranslations,
|
||||
// Then spread locale translations to override English when keys exist in both
|
||||
...localeTranslations,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads translations for a specific locale and namespace with optimized caching
|
||||
* English translations are bundled as englishTranslations for reliability,
|
||||
@@ -34,8 +42,9 @@ export async function loadTranslations(_locale: string, _ns: string) {
|
||||
const ns = SUPPORTED_NAMESPACES.includes(_ns) ? _ns : "common";
|
||||
const cacheKey = `${locale}-${ns}`;
|
||||
|
||||
if (translationCache.has(cacheKey)) {
|
||||
return translationCache.get(cacheKey);
|
||||
const cached = translationCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (locale === "en") {
|
||||
@@ -48,8 +57,9 @@ export async function loadTranslations(_locale: string, _ns: string) {
|
||||
`../../../apps/web/public/static/locales/${locale}/${ns}.json`
|
||||
);
|
||||
|
||||
translationCache.set(cacheKey, localeTranslations);
|
||||
return localeTranslations;
|
||||
const mergedTranslations = mergeWithEnglishFallback(localeTranslations);
|
||||
translationCache.set(cacheKey, mergedTranslations);
|
||||
return mergedTranslations;
|
||||
} catch (dynamicImportErr) {
|
||||
log.warn(`Dynamic import failed for locale ${locale}:`, dynamicImportErr);
|
||||
|
||||
@@ -64,8 +74,9 @@ export async function loadTranslations(_locale: string, _ns: string) {
|
||||
);
|
||||
if (response.ok) {
|
||||
const httpTranslations = await response.json();
|
||||
translationCache.set(cacheKey, httpTranslations);
|
||||
return httpTranslations;
|
||||
const mergedTranslations = mergeWithEnglishFallback(httpTranslations);
|
||||
translationCache.set(cacheKey, mergedTranslations);
|
||||
return mergedTranslations;
|
||||
}
|
||||
} catch (httpErr) {
|
||||
log.error(`HTTP fallback also failed for locale ${locale}:`, httpErr);
|
||||
|
||||
Reference in New Issue
Block a user