fix: refactor i18n loadTranslations and set timeout to 3s (#22633)
* refactor * use abort controller * address comment * fix tests * fix time * fix tests
This commit is contained in:
@@ -26,7 +26,12 @@ vi.mock("@calcom/emails/email-manager", () => {
|
||||
|
||||
vi.mock("@calcom/lib/server/i18n", () => {
|
||||
return {
|
||||
getTranslation: (key: string) => key,
|
||||
getTranslation: async (locale: string, namespace: string) => {
|
||||
const t = (key: string) => key;
|
||||
t.locale = locale;
|
||||
t.namespace = namespace;
|
||||
return t;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -25,6 +25,17 @@ vi.mock("@calcom/prisma", () => {
|
||||
|
||||
vi.mock("stripe");
|
||||
|
||||
vi.mock("@calcom/lib/server/i18n", () => {
|
||||
return {
|
||||
getTranslation: async (locale: string, namespace: string) => {
|
||||
const t = (key: string) => key;
|
||||
t.locale = locale;
|
||||
t.namespace = namespace;
|
||||
return t;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/lib/constants", async () => {
|
||||
const actual = (await vi.importActual("@calcom/lib/constants")) as typeof import("@calcom/lib/constants");
|
||||
return {
|
||||
|
||||
+11
@@ -43,6 +43,17 @@ vi.mock("@calcom/lib/domainManager/organization", () => ({
|
||||
createDomain: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/server/i18n", () => {
|
||||
return {
|
||||
getTranslation: async (locale: string, namespace: string) => {
|
||||
const t = (key: string) => key;
|
||||
t.locale = locale;
|
||||
t.namespace = namespace;
|
||||
return t;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const mockOrganizationOnboarding = {
|
||||
name: "Test Org",
|
||||
slug: "test-org",
|
||||
|
||||
@@ -17,6 +17,16 @@ vi.mock("@calcom/features/ee/workflows/lib/reminders/providers/emailProvider", (
|
||||
sendOrScheduleWorkflowEmails: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/server/i18n", () => {
|
||||
return {
|
||||
getTranslation: async (locale: string, namespace: string) => {
|
||||
const t = (key: string) => key;
|
||||
t.locale = locale;
|
||||
t.namespace = namespace;
|
||||
return t;
|
||||
},
|
||||
};
|
||||
});
|
||||
describe("reminderScheduler", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createInstance } from "i18next";
|
||||
import { expect, test, describe } from "vitest";
|
||||
import { vi, expect, test, describe } from "vitest";
|
||||
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
@@ -10,6 +10,17 @@ import { getTemplateBodyForAction } from "../actionHelperFunctions";
|
||||
import compareReminderBodyToTemplate from "../compareReminderBodyToTemplate";
|
||||
import plainTextReminderTemplates from "../reminders/templates/plainTextTemplates";
|
||||
|
||||
vi.mock("@calcom/lib/server/i18n", () => {
|
||||
return {
|
||||
getTranslation: async (locale: string, namespace: string) => {
|
||||
const t = (key: string) => key;
|
||||
t.locale = locale;
|
||||
t.namespace = namespace;
|
||||
return t;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const translation = async () => {
|
||||
const _i18n = createInstance();
|
||||
await _i18n.init({
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export async function fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs: number) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
});
|
||||
return response;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
+23
-47
@@ -1,39 +1,14 @@
|
||||
import { createInstance } from "i18next";
|
||||
|
||||
import { i18n } from "@calcom/config/next-i18next.config";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { fetchWithTimeout } from "../fetchWithTimeout";
|
||||
import logger from "../logger";
|
||||
|
||||
const translationCache = new Map<string, Record<string, string>>();
|
||||
const i18nInstanceCache = new Map<string, any>();
|
||||
|
||||
/**
|
||||
* Loads English fallback translations for when requested locale translations fail
|
||||
* Implements caching to avoid redundant network requests
|
||||
* @returns {Promise<Record<string, string>>} English translations object or empty object on failure
|
||||
*/
|
||||
async function loadFallbackTranslations() {
|
||||
const cacheKey = "en-common";
|
||||
|
||||
if (translationCache.has(cacheKey)) {
|
||||
return translationCache.get(cacheKey);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${WEBAPP_URL}/static/locales/en/common.json`, {
|
||||
cache: process.env.NODE_ENV === "production" ? "force-cache" : "no-store",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch fallback translations: ${res.status}`);
|
||||
}
|
||||
|
||||
const translations = await res.json();
|
||||
translationCache.set(cacheKey, translations);
|
||||
return translations;
|
||||
} catch (error) {
|
||||
console.error("Could not fetch fallback translations:", error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
const SUPPORTED_NAMESPACES = ["common"];
|
||||
|
||||
/**
|
||||
* Loads translations for a specific locale and namespace with optimized caching
|
||||
@@ -41,32 +16,33 @@ async function loadFallbackTranslations() {
|
||||
* @param {string} ns - The namespace for the translations
|
||||
* @returns {Promise<Record<string, string>>} Translations object or fallback translations on failure
|
||||
*/
|
||||
export async function loadTranslations(_locale: string, ns: string) {
|
||||
const locale = _locale === "zh" ? "zh-CN" : _locale;
|
||||
export async function loadTranslations(_locale: string, _ns: string) {
|
||||
let locale = _locale === "zh" ? "zh-CN" : _locale;
|
||||
locale = i18n.locales.includes(locale) ? locale : "en";
|
||||
const ns = SUPPORTED_NAMESPACES.includes(_ns) ? _ns : "common";
|
||||
const cacheKey = `${locale}-${ns}`;
|
||||
|
||||
if (translationCache.has(cacheKey)) {
|
||||
return translationCache.get(cacheKey);
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `${WEBAPP_URL}/static/locales/${locale}/${ns}.json`;
|
||||
const response = await fetch(url, {
|
||||
const url = `${WEBAPP_URL}/static/locales/${locale}/${ns}.json`;
|
||||
const response = await fetchWithTimeout(
|
||||
url,
|
||||
{
|
||||
cache: process.env.NODE_ENV === "production" ? "force-cache" : "no-store",
|
||||
});
|
||||
},
|
||||
3000
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch translations: ${response.status}`);
|
||||
}
|
||||
|
||||
const translations = await response.json();
|
||||
translationCache.set(cacheKey, translations);
|
||||
return translations;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load translations for ${locale}/${ns}, falling back to English:`, error);
|
||||
const fallbackTranslations = await loadFallbackTranslations();
|
||||
return fallbackTranslations;
|
||||
if (!response.ok) {
|
||||
logger.error(`Failed to fetch translations: ${response.status}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
const translations = await response.json();
|
||||
translationCache.set(cacheKey, translations);
|
||||
return translations;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,8 +9,11 @@ import { UserRepository } from "./user";
|
||||
|
||||
vi.mock("@calcom/lib/server/i18n", () => {
|
||||
return {
|
||||
getTranslation: (key: string) => {
|
||||
return () => key;
|
||||
getTranslation: async (locale: string, namespace: string) => {
|
||||
const t = (key: string) => key;
|
||||
t.locale = locale;
|
||||
t.namespace = namespace;
|
||||
return t;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -11,8 +11,11 @@ import { UserCreationService } from "./userCreationService";
|
||||
|
||||
vi.mock("@calcom/lib/server/i18n", () => {
|
||||
return {
|
||||
getTranslation: (key: string) => {
|
||||
return () => key;
|
||||
getTranslation: async (locale: string, namespace: string) => {
|
||||
const t = (key: string) => key;
|
||||
t.locale = locale;
|
||||
t.namespace = namespace;
|
||||
return t;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user