Skip to content

Commit 4061bfb

Browse files
authored
fix: refactor i18n loadTranslations and set timeout to 3s (calcom#22878)
* refactor * use abort controller * address comment * fix tests * fix time * fix tests * fix platform library build
1 parent 18059e5 commit 4061bfb

9 files changed

Lines changed: 97 additions & 53 deletions

File tree

apps/web/test/lib/handleChildrenEventTypes.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ vi.mock("@calcom/emails/email-manager", () => {
2626

2727
vi.mock("@calcom/lib/server/i18n", () => {
2828
return {
29-
getTranslation: (key: string) => key,
29+
getTranslation: async (locale: string, namespace: string) => {
30+
const t = (key: string) => key;
31+
t.locale = locale;
32+
t.namespace = namespace;
33+
return t;
34+
},
3035
};
3136
});
3237

packages/features/ee/billing/credit-service.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ vi.mock("@calcom/prisma", () => {
2525

2626
vi.mock("stripe");
2727

28+
vi.mock("@calcom/lib/server/i18n", () => {
29+
return {
30+
getTranslation: async (locale: string, namespace: string) => {
31+
const t = (key: string) => key;
32+
t.locale = locale;
33+
t.namespace = namespace;
34+
return t;
35+
},
36+
};
37+
});
38+
2839
vi.mock("@calcom/lib/constants", async () => {
2940
const actual = (await vi.importActual("@calcom/lib/constants")) as typeof import("@calcom/lib/constants");
3041
return {

packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ vi.mock("@calcom/lib/domainManager/organization", () => ({
4343
createDomain: vi.fn(),
4444
}));
4545

46+
vi.mock("@calcom/lib/server/i18n", () => {
47+
return {
48+
getTranslation: async (locale: string, namespace: string) => {
49+
const t = (key: string) => key;
50+
t.locale = locale;
51+
t.namespace = namespace;
52+
return t;
53+
},
54+
};
55+
});
56+
4657
const mockOrganizationOnboarding = {
4758
name: "Test Org",
4859
slug: "test-org",

packages/features/ee/workflows/lib/reminders/reminderScheduler.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ vi.mock("@calcom/features/ee/workflows/lib/reminders/providers/emailProvider", (
1717
sendOrScheduleWorkflowEmails: vi.fn(),
1818
}));
1919

20+
vi.mock("@calcom/lib/server/i18n", () => {
21+
return {
22+
getTranslation: async (locale: string, namespace: string) => {
23+
const t = (key: string) => key;
24+
t.locale = locale;
25+
t.namespace = namespace;
26+
return t;
27+
},
28+
};
29+
});
2030
describe("reminderScheduler", () => {
2131
beforeEach(() => {
2232
vi.clearAllMocks();

packages/features/ee/workflows/lib/test/compareReminderBodyToTemplate.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createInstance } from "i18next";
2-
import { expect, test, describe } from "vitest";
2+
import { vi, expect, test, describe } from "vitest";
33

44
import { getTranslation } from "@calcom/lib/server/i18n";
55
import { TimeFormat } from "@calcom/lib/timeFormat";
@@ -10,6 +10,17 @@ import { getTemplateBodyForAction } from "../actionHelperFunctions";
1010
import compareReminderBodyToTemplate from "../compareReminderBodyToTemplate";
1111
import plainTextReminderTemplates from "../reminders/templates/plainTextTemplates";
1212

13+
vi.mock("@calcom/lib/server/i18n", () => {
14+
return {
15+
getTranslation: async (locale: string, namespace: string) => {
16+
const t = (key: string) => key;
17+
t.locale = locale;
18+
t.namespace = namespace;
19+
return t;
20+
},
21+
};
22+
});
23+
1324
const translation = async () => {
1425
const _i18n = createInstance();
1526
await _i18n.init({

packages/lib/fetchWithTimeout.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export async function fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs: number) {
2+
const controller = new AbortController();
3+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
4+
try {
5+
const response = await fetch(url, {
6+
...options,
7+
signal: controller.signal,
8+
});
9+
return response;
10+
} finally {
11+
clearTimeout(timeout);
12+
}
13+
}

packages/lib/server/i18n.ts

Lines changed: 24 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,49 @@
11
import { createInstance } from "i18next";
22

3+
/* eslint-disable @typescript-eslint/no-var-requires */
4+
const { i18n } = require("@calcom/config/next-i18next.config");
35
import { WEBAPP_URL } from "@calcom/lib/constants";
46

7+
import { fetchWithTimeout } from "../fetchWithTimeout";
8+
import logger from "../logger";
9+
510
const translationCache = new Map<string, Record<string, string>>();
611
const i18nInstanceCache = new Map<string, any>();
7-
8-
/**
9-
* Loads English fallback translations for when requested locale translations fail
10-
* Implements caching to avoid redundant network requests
11-
* @returns {Promise<Record<string, string>>} English translations object or empty object on failure
12-
*/
13-
async function loadFallbackTranslations() {
14-
const cacheKey = "en-common";
15-
16-
if (translationCache.has(cacheKey)) {
17-
return translationCache.get(cacheKey);
18-
}
19-
20-
try {
21-
const res = await fetch(`${WEBAPP_URL}/static/locales/en/common.json`, {
22-
cache: process.env.NODE_ENV === "production" ? "force-cache" : "no-store",
23-
});
24-
25-
if (!res.ok) {
26-
throw new Error(`Failed to fetch fallback translations: ${res.status}`);
27-
}
28-
29-
const translations = await res.json();
30-
translationCache.set(cacheKey, translations);
31-
return translations;
32-
} catch (error) {
33-
console.error("Could not fetch fallback translations:", error);
34-
return {};
35-
}
36-
}
12+
const SUPPORTED_NAMESPACES = ["common"];
3713

3814
/**
3915
* Loads translations for a specific locale and namespace with optimized caching
4016
* @param {string} _locale - The locale code (e.g., 'en', 'fr', 'zh')
4117
* @param {string} ns - The namespace for the translations
4218
* @returns {Promise<Record<string, string>>} Translations object or fallback translations on failure
4319
*/
44-
export async function loadTranslations(_locale: string, ns: string) {
45-
const locale = _locale === "zh" ? "zh-CN" : _locale;
20+
export async function loadTranslations(_locale: string, _ns: string) {
21+
let locale = _locale === "zh" ? "zh-CN" : _locale;
22+
locale = i18n.locales.includes(locale) ? locale : "en";
23+
const ns = SUPPORTED_NAMESPACES.includes(_ns) ? _ns : "common";
4624
const cacheKey = `${locale}-${ns}`;
4725

4826
if (translationCache.has(cacheKey)) {
4927
return translationCache.get(cacheKey);
5028
}
5129

52-
try {
53-
const url = `${WEBAPP_URL}/static/locales/${locale}/${ns}.json`;
54-
const response = await fetch(url, {
30+
const url = `${WEBAPP_URL}/static/locales/${locale}/${ns}.json`;
31+
const response = await fetchWithTimeout(
32+
url,
33+
{
5534
cache: process.env.NODE_ENV === "production" ? "force-cache" : "no-store",
56-
});
57-
58-
if (!response.ok) {
59-
throw new Error(`Failed to fetch translations: ${response.status}`);
60-
}
35+
},
36+
3000
37+
);
6138

62-
const translations = await response.json();
63-
translationCache.set(cacheKey, translations);
64-
return translations;
65-
} catch (error) {
66-
console.warn(`Failed to load translations for ${locale}/${ns}, falling back to English:`, error);
67-
const fallbackTranslations = await loadFallbackTranslations();
68-
return fallbackTranslations;
39+
if (!response.ok) {
40+
logger.error(`Failed to fetch translations: ${response.status}`);
41+
return {};
6942
}
43+
44+
const translations = await response.json();
45+
translationCache.set(cacheKey, translations);
46+
return translations;
7047
}
7148

7249
/**

packages/lib/server/repository/user.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import { UserRepository } from "./user";
99

1010
vi.mock("@calcom/lib/server/i18n", () => {
1111
return {
12-
getTranslation: (key: string) => {
13-
return () => key;
12+
getTranslation: async (locale: string, namespace: string) => {
13+
const t = (key: string) => key;
14+
t.locale = locale;
15+
t.namespace = namespace;
16+
return t;
1417
},
1518
};
1619
});

packages/lib/server/service/userCreationService.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ import { UserCreationService } from "./userCreationService";
1111

1212
vi.mock("@calcom/lib/server/i18n", () => {
1313
return {
14-
getTranslation: (key: string) => {
15-
return () => key;
14+
getTranslation: async (locale: string, namespace: string) => {
15+
const t = (key: string) => key;
16+
t.locale = locale;
17+
t.namespace = namespace;
18+
return t;
1619
},
1720
};
1821
});

0 commit comments

Comments
 (0)