refactor: replace i18n HTTP requests with build-time bundling (#22422)

* refactor: replace i18n HTTP requests with build-time bundling

- Create translationBundler.ts for build-time translation loading
- Replace HTTP fetch in loadTranslations with file system reads
- Add CalComVersion cache invalidation to prevent stale translations
- Fix TypeScript errors in booking page components
- Eliminate 60s timeout issues by removing network dependency

Resolves translation timeout issues by bundling translations at build time
instead of making runtime HTTP requests to /static/locales/ endpoints.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: move i18n files back to packages/lib/server with proper imports

- Move i18n.ts and translationBundler.ts back to packages/lib/server/
- Replace all relative imports with @calcom/lib/server/i18n pattern
- Fix LOCALES_PATH to point to correct directory
- Maintain optimized serverless-friendly translation loading

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* refactor: move locale files to packages/lib/server to eliminate circular deps

- Move all locale files from apps/web/public/static/locales to packages/lib/server/locales
- Create copy-locales-static.js script to copy files during build
- Update all references to use new location for build-time access
- Maintain public folder copying for Next.js runtime access
- Update platform atoms, scripts, and config files
- Fix copy script relative path issue

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: update test imports after locale refactor

- Fix import paths in test files updated by pre-commit hooks
- Ensure all tests use correct locale import paths

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: revert import paths from @calcom/web/lib/i18n to @calcom/lib/server/i18n

- Revert all test file imports back to @calcom/lib/server/i18n as requested
- Addresses GitHub comment feedback to stick with packages/lib/server location
- Fixes import paths in 6 test files that were incorrectly changed

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: update vite config alias to use new locale path

- Update @calcom/web/public/static/locales/en/common.json to @calcom/lib/server/locales/en/common.json
- Addresses GitHub comment about updating platform atoms vite config
- Maintains correct path resolution after locale files moved to packages/lib/server

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: update translationBundler path resolution for production builds

- Use process.cwd() instead of __dirname for locale file path resolution
- Ensures locale files can be found in both development and production environments
- Fixes E2E test failures caused by missing locale files in .next/server/chunks/

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: use relative path resolution for locale files in translationBundler

- Change from process.cwd() to __dirname with relative paths
- Ensures locale files can be found in both development and production environments
- Fixes E2E test failures caused by incorrect path resolution in Next.js builds

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: use process.cwd() for locale path resolution in production builds

- Change from __dirname to process.cwd() with relative paths
- Ensures locale files can be found when bundled into Next.js server chunks
- Fixes E2E test failures caused by incorrect path resolution in production environment
- Follows same pattern used in getStaticProps.tsx for cross-package file access

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: use path.resolve pattern from sendVerificationRequest for locale access

- Change from process.cwd() to path.resolve(process.cwd(), '..', '..', 'packages/lib/server/locales')
- Follows same pattern used in sendVerificationRequest.ts for cross-environment file access
- Should resolve E2E test failures by ensuring locale files can be found when bundled into Next.js server chunks
- Pattern navigates up from current working directory to reach packages directory consistently

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: use dynamic monorepo root detection for locale path resolution

- Replace relative path resolution with dynamic monorepo root finder
- Ensures locale files can be found from any working directory (root, apps/web, apps/api/v2)
- Update API v2 i18n config to use new locale path
- Fixes remaining E2E test failures in API v2 and E2E (1/4) test suites

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix: use require.resolve(__filename) for robust path resolution in all contexts

- Replace __dirname with require.resolve(__filename) in monorepo root detection
- Ensures locale files can be found when running from any working directory
- Fixes E2E API v2 test failures where __dirname resolves to '.' instead of actual file path

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Revert "fix: use require.resolve(__filename) for robust path resolution in all contexts"

This reverts commit b37d8226000da8f7d5fb98b83dd0e95a53d45372.

* fix: update copied locale files after translationBundler path resolution fix

- Copy script updated all locale files in public directory
- Ensures E2E tests have access to latest locale files
- Fixes regression where all E2E tests were failing

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* feat: remove existing locale files to establish copy script as single source of truth

- Delete all common.json files from apps/web/public/static/locales/
- Eliminates developer confusion about which files are authoritative
- copy-locales-static.js script now clearly the only mechanism for populating public folder
- packages/lib/server/locales/ remains the definitive source of truth for translations

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* add comment

* refactor: simplify findMonorepoRoot by removing redundant fallback loop

- Remove unnecessary second while loop using process.cwd()
- The first loop from __dirname will always find the monorepo root
- Add clear error message for fail-fast behavior if repo structure is corrupted
- Improves code clarity and maintainability

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* feat: integrate updated translations from main branch

- Restore all common.json files from main branch to apps/web/public/static/locales/
- Overwrite packages/lib/server/locales/ with up-to-date translation content
- Resolve merge conflicts using Benny's safer 2-step approach
- Ensure translation source of truth remains in packages/lib/server/locales/
- Complete safer conflict resolution to eliminate merge conflicts on PR #22422

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* chore: update package.json and yarn.lock after translation integration

- Update dependencies after ts-node installation for pre-commit hooks
- Ensure yarn.lock reflects current dependency state
- Complete translation integration process

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* feat: remove duplicate locale files from apps/web to establish single source of truth

- Delete all common.json files from apps/web/public/static/locales/
- Maintain packages/lib/server/locales/ as the single source of truth for translations
- copy-locales-static.js script will populate public folder during build process
- Complete Benny's safer 2-step approach: restore from main, then remove duplicates
- Resolve merge conflicts and eliminate developer confusion about translation file locations

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: hbjORbj <sldisek783@gmail.com>
This commit is contained in:
Keith Williams
2025-07-12 07:49:27 +00:00
committed by GitHub
co-authored by keith@cal.com <keithwillcode@gmail.com> Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> hbjORbj
parent bd5e14b488
commit 163c7ff791
64 changed files with 215 additions and 87 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
with:
filters: |
has-files-requiring-all-checks:
- "!(**.md|.github/CODEOWNERS|docs/**|help/**|apps/web/public/static/locales/**/common.json)"
- "!(**.md|.github/CODEOWNERS|docs/**|help/**|packages/lib/server/locales/**/common.json)"
- name: Get Latest Commit SHA
id: get_sha
run: |
+1 -1
View File
@@ -5,7 +5,7 @@ const i18nConfig = require("@calcom/config/next-i18next.config");
/** @type {import("next-i18next").UserConfig} */
const config = {
...i18nConfig,
localePath: path.resolve("../../web/public/static/locales"),
localePath: path.resolve("../../packages/lib/server/locales"),
};
module.exports = config;
@@ -32,7 +32,7 @@ const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
const eventLocale = props.eventData?.interfaceLanguage;
const ns = "common";
let translations;
let translations: Record<string, string> = {};
if (eventLocale) {
const ns = "common";
translations = await loadTranslations(eventLocale, ns);
@@ -68,7 +68,7 @@ const ServerPage = async ({ params, searchParams }: PageProps) => {
const eventLocale = props.eventData?.interfaceLanguage;
const ns = "common";
let translations;
let translations: Record<string, string> = {};
if (eventLocale) {
const ns = "common";
translations = await loadTranslations(eventLocale, ns);
+1 -1
View File
@@ -1,5 +1,5 @@
require("dotenv").config({ path: "../../.env" });
const englishTranslation = require("./public/static/locales/en/common.json");
const englishTranslation = require("../../packages/lib/server/locales/en/common.json");
const { withAxiom } = require("next-axiom");
const { version } = require("./package.json");
const {
+1 -1
View File
@@ -15,7 +15,7 @@
"type-check": "tsc --pretty --noEmit",
"type-check:ci": "tsc-absolute --pretty --noEmit",
"sentry:release": "NODE_OPTIONS='--max-old-space-size=6144' node scripts/create-sentry-release.js",
"copy-static": "node scripts/copy-app-store-static.js",
"copy-static": "node scripts/copy-app-store-static.js && node scripts/copy-locales-static.js",
"build": "yarn copy-static && next build && yarn sentry:release",
"start": "next start",
"lint": "eslint . --ignore-path .gitignore",
+1 -1
View File
@@ -2,7 +2,7 @@ import { loadJSON } from "./loadJSON";
// Provide an standalone localize utility not managed by next-i18n
export async function localize(locale: string) {
const localeModule = `../../public/static/locales/${locale}/common.json`;
const localeModule = `../../../../packages/lib/server/locales/${locale}/common.json`;
const localeMap = loadJSON(localeModule);
return (message: string) => {
if (message in localeMap) return localeMap[message];
@@ -3,7 +3,7 @@ import { join } from "path";
const TEMPLATE_LANGUAGE = "en";
const SPECIFIC_LOCALES = process.argv.slice(2) || [];
const LOCALES_PATH = join(__dirname, "../public/static/locales");
const LOCALES_PATH = join(__dirname, "../../../packages/lib/server/locales");
const ALL_LOCALES = readdirSync(LOCALES_PATH);
+25
View File
@@ -0,0 +1,25 @@
const fs = require("fs");
const path = require("path");
const glob = require("glob");
const copyLocalesStatic = () => {
const localeFiles = glob.sync("../../packages/lib/server/locales/**/*.json", { nodir: true });
localeFiles.forEach((file) => {
const relativePath = file.replace("../../packages/lib/server/locales/", "");
// Create destination directory if it doesn't exist
const destDir = path.join(process.cwd(), "public", "static", "locales", path.dirname(relativePath));
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
// Copy file to destination
const destPath = path.join(process.cwd(), "public", "static", "locales", relativePath);
fs.copyFileSync(file, destPath);
console.log(`Copied ${file} to ${destPath}`);
});
};
// Run the copy function
copyLocalesStatic();
+1 -1
View File
@@ -23,7 +23,7 @@ const translationKeyRegex = /(?<!\w)(?:t\(("[^"]*")(?:,\s*\{[^}]*\})?\)|i18nKey=
/** @type {import("i18n-unused/src/types/index.ts").RunOptions} */
const config = {
// localesPath: localePath, // uncomment to run on all locales (to calculate kb savings)
localesPath: path.join("./apps/website", "/public/static/locales", "/en"),
localesPath: path.join("./packages/lib/server/locales", "/en"),
srcPath: "./apps/website",
srcExtensions: ["ts", "tsx"],
translationContextSeparator: ":",
+1 -1
View File
@@ -43,7 +43,7 @@
},
"buckets": {
"json": {
"include": ["apps/web/public/static/locales/[locale]/common.json"]
"include": ["packages/lib/server/locales/[locale]/common.json"]
}
},
"$schema": "https://lingo.dev/schema/i18n.json"
+1
View File
@@ -108,6 +108,7 @@
"prettier": "^2.8.6",
"prismock": "^1.33.4",
"resize-observer-polyfill": "^1.5.1",
"ts-node": "^10.9.2",
"tsc-absolute": "^1.0.0",
"typescript": "^5.7.2",
"vitest": "^2.1.1",
@@ -2,9 +2,9 @@ import { createInstance } from "i18next";
import { expect, test, describe } from "vitest";
import { getTranslation } from "@calcom/lib/server/i18n";
import en from "@calcom/lib/server/locales/en/common.json";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { WorkflowActions, WorkflowTemplates } from "@calcom/prisma/enums";
import en from "@calcom/web/public/static/locales/en/common.json";
import { getTemplateBodyForAction } from "../actionHelperFunctions";
import compareReminderBodyToTemplate from "../compareReminderBodyToTemplate";
+1 -1
View File
@@ -1,9 +1,9 @@
import type { Prisma } from "@prisma/client";
import dayjs from "@calcom/dayjs";
import { getTranslation } from "@calcom/lib/server/i18n";
import { parseRecurringEvent } from "./isRecurringEvent";
import { getTranslation } from "./server/i18n";
type DestinationCalendar = {
id: number;
+17 -22
View File
@@ -1,6 +1,6 @@
import { createInstance } from "i18next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { CALCOM_VERSION } from "@calcom/lib/constants";
const translationCache = new Map<string, Record<string, string>>();
const i18nInstanceCache = new Map<string, any>();
@@ -10,27 +10,25 @@ const i18nInstanceCache = new Map<string, any>();
* 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";
async function loadFallbackTranslations(): Promise<Record<string, string>> {
const cacheKey = `en-common-${CALCOM_VERSION}`;
if (translationCache.has(cacheKey)) {
return translationCache.get(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",
});
const { getBundledTranslations } = await import("./translationBundler");
const translations = getBundledTranslations("en", "common");
if (!res.ok) {
throw new Error(`Failed to fetch fallback translations: ${res.status}`);
if (Object.keys(translations).length === 0) {
throw new Error("No English fallback translations found");
}
const translations = await res.json();
translationCache.set(cacheKey, translations);
return translations;
} catch (error) {
console.error("Could not fetch fallback translations:", error);
console.error("Could not load fallback translations:", error);
return {};
}
}
@@ -41,25 +39,22 @@ 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) {
export async function loadTranslations(_locale: string, ns: string): Promise<Record<string, string>> {
const locale = _locale === "zh" ? "zh-CN" : _locale;
const cacheKey = `${locale}-${ns}`;
const cacheKey = `${locale}-${ns}-${CALCOM_VERSION}`;
if (translationCache.has(cacheKey)) {
return translationCache.get(cacheKey);
return translationCache.get(cacheKey)!;
}
try {
const url = `${WEBAPP_URL}/static/locales/${locale}/${ns}.json`;
const response = await fetch(url, {
cache: process.env.NODE_ENV === "production" ? "force-cache" : "no-store",
});
const { getBundledTranslations } = await import("./translationBundler");
const translations = getBundledTranslations(locale, ns);
if (!response.ok) {
throw new Error(`Failed to fetch translations: ${response.status}`);
if (Object.keys(translations).length === 0) {
throw new Error(`No translations found for ${locale}/${ns}`);
}
const translations = await response.json();
translationCache.set(cacheKey, translations);
return translations;
} catch (error) {
@@ -76,7 +71,7 @@ export async function loadTranslations(_locale: string, ns: string) {
* @returns {Promise<Function>} A translation function bound to the specified locale and namespace
*/
export const getTranslation = async (locale: string, ns: string) => {
const cacheKey = `${locale}-${ns}`;
const cacheKey = `${locale}-${ns}-${CALCOM_VERSION}`;
if (i18nInstanceCache.has(cacheKey)) {
return i18nInstanceCache.get(cacheKey).getFixedT(locale, ns);
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "تم حفظ مفتاح الترخيص بنجاح",
"timezone_mismatch_tooltip": "أنت تشاهد التقرير بناءً على المنطقة الزمنية لملفك الشخصي ({{userTimezone}})، بينما متصفحك مضبوط على المنطقة الزمنية ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Lisenziya açarı uğurla saxlanıldı",
"timezone_mismatch_tooltip": "Siz hesabatı profil saat qurşağınıza ({{userTimezone}}) əsasən görürsünüz, brauzeriniz isə başqa saat qurşağına ({{browserTimezone}}) təyin edilib",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Лицензионният ключ е успешно записан",
"timezone_mismatch_tooltip": "Преглеждате отчета според часовата зона на вашия профил ({{userTimezone}}), докато браузърът ви е настроен на часова зона ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавете новите си низове над този ред ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "লাইসেন্স কী সফলভাবে সংরক্ষিত হয়েছে",
"timezone_mismatch_tooltip": "আপনি আপনার প্রোফাইল টাইমজোন ({{userTimezone}}) অনুসারে রিপোর্ট দেখছেন, যখন আপনার ব্রাউজার টাইমজোন ({{browserTimezone}}) এ সেট করা আছে",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑ুষান্ত"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Clau de llicència desada correctament",
"timezone_mismatch_tooltip": "Esteu veient l'informe basat en la zona horària del vostre perfil ({{userTimezone}}), mentre que el vostre navegador està configurat a la zona horària ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Afegiu les vostres noves cadenes a dalt ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Licenční klíč byl úspěšně uložen",
"timezone_mismatch_tooltip": "Prohlížíte si report na základě časového pásma vašeho profilu ({{userTimezone}}), zatímco váš prohlížeč je nastaven na časové pásmo ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Licensnøgle blev gemt med succes",
"timezone_mismatch_tooltip": "Du ser rapporten baseret på din profils tidszone ({{userTimezone}}), mens din browser er indstillet til tidszonen ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Το κλειδί άδειας αποθηκεύτηκε με επιτυχία",
"timezone_mismatch_tooltip": "Βλέπετε την αναφορά με βάση τη ζώνη ώρας του προφίλ σας ({{userTimezone}}), ενώ ο περιηγητής σας είναι ρυθμισμένος στη ζώνη ώρας ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Προσθέστε τις νέες συμβολοσειρές σας πάνω από εδώ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -283,7 +283,7 @@
"guests": "Guests",
"guest": "Guest",
"web_conferencing_details_to_follow": "Web conferencing details to follow in the confirmation email.",
"confirmation":"Confirmation",
"confirmation": "Confirmation",
"what_booker_should_provide": "What your booker should provide to receive confirmations",
"404_the_user": "The username",
"username": "Username",
@@ -3360,4 +3360,4 @@
"license_key_saved": "Clave de licencia guardada correctamente",
"timezone_mismatch_tooltip": "Estás viendo el informe basado en la zona horaria de tu perfil ({{userTimezone}}), mientras que tu navegador está configurado en la zona horaria ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agrega tus nuevas cadenas arriba de esta línea ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Clave de licencia guardada correctamente",
"timezone_mismatch_tooltip": "Estás viendo el informe basado en la zona horaria de tu perfil ({{userTimezone}}), mientras que tu navegador está configurado en la zona horaria ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Litsentsivõti salvestati edukalt",
"timezone_mismatch_tooltip": "Te vaatate aruannet oma profiili ajavööndi ({{userTimezone}}) alusel, samal ajal kui teie brauser on seadistatud ajavööndisse ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Lizentzia gakoa ondo gorde da",
"timezone_mismatch_tooltip": "Txostena zure profileko ordu-zonaren arabera ikusten ari zara ({{userTimezone}}), zure nabigatzailea ordu-zona honetan ezarrita dagoen bitartean ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Gehitu zure kate berriak honen gainean ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Lisenssiavain tallennettu onnistuneesti",
"timezone_mismatch_tooltip": "Tarkastelet raporttia profiilisi aikavyöhykkeen ({{userTimezone}}) mukaan, kun taas selaimesi on asetettu aikavyöhykkeelle ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Lisää uudet merkkijonot tämän yläpuolelle ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "מפתח הרישיון נשמר בהצלחה",
"timezone_mismatch_tooltip": "אתה צופה בדוח על בסיס אזור הזמן של הפרופיל שלך ({{userTimezone}}), בעוד שהדפדפן שלך מוגדר לאזור זמן ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -453,4 +453,4 @@
"reschedule_cta_short": "Zakažite ponovno ovdje.",
"you_and_conjunction": "Vi &",
"email_survey_triggered_by_workflow": "Ova anketa je pokrenuta Workflow-om u Calu."
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "A licenckulcs sikeresen mentve",
"timezone_mismatch_tooltip": "A jelentést az Ön profiljában beállított időzóna ({{userTimezone}}) alapján tekinti meg, míg a böngészője a következő időzónára van állítva: {{browserTimezone}}",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adja hozzá az új karakterláncokat fent ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -166,4 +166,4 @@
"reschedule_cta_short": "Jadwal ulang di sini.",
"you_and_conjunction": "Anda &",
"email_survey_triggered_by_workflow": "Survei ini dipicu oleh Workflow di Cal."
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Chiave di licenza salvata con successo",
"timezone_mismatch_tooltip": "Stai visualizzando il report in base al fuso orario del tuo profilo ({{userTimezone}}), mentre il tuo browser è impostato sul fuso orario ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "ライセンスキーが正常に保存されました",
"timezone_mismatch_tooltip": "あなたはプロフィールのタイムゾーン({{userTimezone}})に基づいてレポートを表示していますが、ブラウザのタイムゾーンは({{browserTimezone}})に設定されています",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "សោអាជ្ញាប័ណ្ណត្រូវបានរក្សាទុកដោយជោគជ័យ",
"timezone_mismatch_tooltip": "អ្នកកំពុងមើលរបាយការណ៍ដោយផ្អែកលើតំបន់ពេលវេលាប្រវត្តិរូបរបស់អ្នក ({{userTimezone}}) ខណៈពេលដែលកម្មវិធីរុករករបស់អ្នកត្រូវបានកំណត់ទៅតំបន់ពេលវេលា ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ បន្ថែមខ្សែអក្សរថ្មីរបស់អ្នកនៅខាងលើនេះ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "라이선스 키가 성공적으로 저장되었습니다",
"timezone_mismatch_tooltip": "프로필 시간대({{userTimezone}})를 기준으로 보고서를 보고 계시지만, 브라우저는 다른 시간대({{browserTimezone}})로 설정되어 있습니다",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 여기에 새 문자열을 추가하세요 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -162,4 +162,4 @@
"reschedule_cta_short": "Pārzīmēt šeit.",
"you_and_conjunction": "Jūs &",
"email_survey_triggered_by_workflow": "Šo aptauju aktivizēja darbplūsma Cal."
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Licentiesleutel succesvol opgeslagen",
"timezone_mismatch_tooltip": "Je bekijkt het rapport op basis van je profieltijdzone ({{userTimezone}}), terwijl je browser is ingesteld op tijdzone ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Lisenssnøkkel lagret",
"timezone_mismatch_tooltip": "Du ser rapporten basert på tidssonen i profilen din ({{userTimezone}}), mens nettleseren din er satt til tidssonen ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Legg til dine nye strenger over her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Chave de licença salva com sucesso",
"timezone_mismatch_tooltip": "Você está visualizando o relatório com base no fuso horário do seu perfil ({{userTimezone}}), enquanto seu navegador está configurado para o fuso horário ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Chave de licença salva com sucesso",
"timezone_mismatch_tooltip": "Você está visualizando o relatório com base no fuso horário do seu perfil ({{userTimezone}}), enquanto seu navegador está configurado para o fuso horário ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Cheia de licență a fost salvată cu succes",
"timezone_mismatch_tooltip": "Vizualizați raportul pe baza fusului orar al profilului dvs. ({{userTimezone}}), în timp ce browserul dvs. este setat pe fusul orar ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adăugați stringurile noi deasupra acestui rând ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Лицензионный ключ успешно сохранён",
"timezone_mismatch_tooltip": "Вы просматриваете отчёт в часовом поясе вашего профиля ({{userTimezone}}), в то время как ваш браузер настроен на часовой пояс ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавьте строки выше ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Licenčný kľúč bol úspešne uložený",
"timezone_mismatch_tooltip": "Prezeráte si správu na základe časového pásma vášho profilu ({{userTimezone}}), zatiaľ čo váš prehliadač je nastavený na časové pásmo ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Pridajte svoje nové reťazce nad túto líniu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -67,4 +67,4 @@
"reschedule_cta_short": "Preplánujte tu.",
"you_and_conjunction": "Vy &",
"email_survey_triggered_by_workflow": "Tento prieskum bol spustený pracovným postupom v Cal."
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Licencni ključ je uspešno sačuvan",
"timezone_mismatch_tooltip": "Gledate izveštaj na osnovu vremenske zone vašeg profila ({{userTimezone}}), dok je vaš pretraživač podešen na vremensku zonu ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodajte svoje nove stringove iznad ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Licensnyckeln sparades framgångsrikt",
"timezone_mismatch_tooltip": "Du tittar på rapporten baserat på din profiltidszon ({{userTimezone}}), medan din webbläsare är inställd på tidszonen ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -211,4 +211,4 @@
"reschedule_cta_short": "இங்கே மீண்டும் ஷெடியூல் செய்யவும்.",
"you_and_conjunction": "நீங்கள் &",
"email_survey_triggered_by_workflow": "இந்த ஆய்வு Cal-ல் உள்ள பணிப்பாய்வு மூலம் தூண்டப்பட்டது."
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Lisans anahtarı başarıyla kaydedildi",
"timezone_mismatch_tooltip": "Raporu profil saat diliminize ({{userTimezone}}) göre görüntülüyorsunuz, ancak tarayıcınız ({{browserTimezone}}) saat dilimine ayarlı",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni dizelerinizi yukarıya ekleyin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Ліцензійний ключ успішно збережено",
"timezone_mismatch_tooltip": "Ви переглядаєте звіт на основі часового поясу вашого профілю ({{userTimezone}}), тоді як у вашому браузері встановлено часовий пояс ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "Đã lưu khóa giấy phép thành công",
"timezone_mismatch_tooltip": "Bạn đang xem báo cáo dựa trên múi giờ hồ sơ của bạn ({{userTimezone}}), trong khi trình duyệt của bạn được đặt theo múi giờ ({{browserTimezone}})",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "许可证密钥已成功保存",
"timezone_mismatch_tooltip": "您正在根据您的个人资料时区({{userTimezone}})查看报告,而您的浏览器设置为时区({{browserTimezone}}",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
@@ -3360,4 +3360,4 @@
"license_key_saved": "授權金鑰已成功儲存",
"timezone_mismatch_tooltip": "您正在根據您的個人資料時區({{userTimezone}})查看報告,而您的瀏覽器設置為時區({{browserTimezone}}",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}
+68
View File
@@ -0,0 +1,68 @@
import { readFileSync } from "fs";
import { join } from "path";
import path from "path";
import { CALCOM_VERSION } from "@calcom/lib/constants";
function findMonorepoRoot(): string {
let currentDir = __dirname;
while (currentDir !== path.dirname(currentDir)) {
try {
const packageJsonPath = path.join(currentDir, "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
if (packageJson.workspaces && packageJson.name === "calcom-monorepo") {
return currentDir;
}
} catch (error) {
// package.json doesn't exist in this directory
// Just continue to the next directory
}
currentDir = path.dirname(currentDir);
}
throw new Error("Could not find monorepo root - repository structure may be corrupted");
}
const LOCALES_PATH = path.join(findMonorepoRoot(), "packages/lib/server/locales");
interface LocaleCache {
[cacheKey: string]: Record<string, string>;
}
let localeCache: LocaleCache = {};
let cacheVersion: string | null = null;
function loadTranslationForLocale(locale: string, ns: string): Record<string, string> {
const cacheKey = `${locale}-${ns}-${CALCOM_VERSION}`;
if (cacheVersion === CALCOM_VERSION && localeCache[cacheKey]) {
return localeCache[cacheKey];
}
if (cacheVersion !== CALCOM_VERSION) {
localeCache = {};
cacheVersion = CALCOM_VERSION;
}
try {
const translationPath = join(LOCALES_PATH, locale, `${ns}.json`);
const translations = JSON.parse(readFileSync(translationPath, "utf-8"));
localeCache[cacheKey] = translations;
return translations;
} catch (error) {
console.warn(`Failed to load translations for ${locale}/${ns}:`, error);
return {};
}
}
export function getBundledTranslations(locale: string, ns: string): Record<string, string> {
const normalizedLocale = locale === "zh" ? "zh-CN" : locale;
const translations = loadTranslationForLocale(normalizedLocale, ns);
if (Object.keys(translations).length > 0) {
return translations;
}
const englishTranslations = loadTranslationForLocale("en", ns);
return englishTranslations;
}
@@ -3,14 +3,14 @@ import type { ReactNode } from "react";
import { useState } from "react";
import { useCallback } from "react";
import deTranslations from "@calcom/lib/server/locales/de/common.json";
import enTranslations from "@calcom/lib/server/locales/en/common.json";
import esTranslations from "@calcom/lib/server/locales/es/common.json";
import frTranslations from "@calcom/lib/server/locales/fr/common.json";
import nlTranslations from "@calcom/lib/server/locales/nl/common.json";
import ptBrTranslations from "@calcom/lib/server/locales/pt-BR/common.json";
import type { API_VERSIONS_ENUM } from "@calcom/platform-constants";
import { IconSprites } from "@calcom/ui/components/icon";
import deTranslations from "@calcom/web/public/static/locales/de/common.json";
import enTranslations from "@calcom/web/public/static/locales/en/common.json";
import esTranslations from "@calcom/web/public/static/locales/es/common.json";
import frTranslations from "@calcom/web/public/static/locales/fr/common.json";
import nlTranslations from "@calcom/web/public/static/locales/nl/common.json";
import ptBrTranslations from "@calcom/web/public/static/locales/pt-BR/common.json";
import { AtomsContext } from "../hooks/useAtomsContext";
import { useMe } from "../hooks/useMe";
@@ -1,9 +1,9 @@
import type deTranslations from "@calcom/web/public/static/locales/de/common.json";
import type enTranslations from "@calcom/web/public/static/locales/en/common.json";
import type esTranslations from "@calcom/web/public/static/locales/es/common.json";
import type frTranslations from "@calcom/web/public/static/locales/fr/common.json";
import type nlTranslations from "@calcom/web/public/static/locales/nl/common.json";
import type ptBrTranslations from "@calcom/web/public/static/locales/pt-BR/common.json";
import type deTranslations from "@calcom/lib/server/locales/de/common.json";
import type enTranslations from "@calcom/lib/server/locales/en/common.json";
import type esTranslations from "@calcom/lib/server/locales/es/common.json";
import type frTranslations from "@calcom/lib/server/locales/fr/common.json";
import type nlTranslations from "@calcom/lib/server/locales/nl/common.json";
import type ptBrTranslations from "@calcom/lib/server/locales/pt-BR/common.json";
export type enTranslationKeys = keyof typeof enTranslations;
export type frTranslationKeys = keyof typeof frTranslations;
+2 -2
View File
@@ -62,9 +62,9 @@ export default defineConfig(({ mode }) => {
"@calcom/platform-constants": path.resolve(__dirname, "../constants/index.ts"),
"@calcom/platform-types": path.resolve(__dirname, "../types/index.ts"),
"@calcom/platform-utils": path.resolve(__dirname, "../constants/index.ts"),
"@calcom/web/public/static/locales/en/common.json": path.resolve(
"@calcom/lib/server/locales/en/common.json": path.resolve(
__dirname,
"../../../apps/web/public/static/locales/en/common.json"
"../../lib/server/locales/en/common.json"
),
},
},
+39
View File
@@ -21704,6 +21704,7 @@ __metadata:
prettier: ^2.8.6
prismock: ^1.33.4
resize-observer-polyfill: ^1.5.1
ts-node: ^10.9.2
tsc-absolute: ^1.0.0
turbo: ^1.10.1
typescript: ^5.7.2
@@ -44895,6 +44896,44 @@ __metadata:
languageName: node
linkType: hard
"ts-node@npm:^10.9.2":
version: 10.9.2
resolution: "ts-node@npm:10.9.2"
dependencies:
"@cspotcode/source-map-support": ^0.8.0
"@tsconfig/node10": ^1.0.7
"@tsconfig/node12": ^1.0.7
"@tsconfig/node14": ^1.0.0
"@tsconfig/node16": ^1.0.2
acorn: ^8.4.1
acorn-walk: ^8.1.1
arg: ^4.1.0
create-require: ^1.1.0
diff: ^4.0.1
make-error: ^1.1.1
v8-compile-cache-lib: ^3.0.1
yn: 3.1.1
peerDependencies:
"@swc/core": ">=1.2.50"
"@swc/wasm": ">=1.2.50"
"@types/node": "*"
typescript: ">=2.7"
peerDependenciesMeta:
"@swc/core":
optional: true
"@swc/wasm":
optional: true
bin:
ts-node: dist/bin.js
ts-node-cwd: dist/bin-cwd.js
ts-node-esm: dist/bin-esm.js
ts-node-script: dist/bin-script.js
ts-node-transpile-only: dist/bin-transpile.js
ts-script: dist/bin-script-deprecated.js
checksum: fde256c9073969e234526e2cfead42591b9a2aec5222bac154b0de2fa9e4ceb30efcd717ee8bc785a56f3a119bdd5aa27b333d9dbec94ed254bd26f8944c67ac
languageName: node
linkType: hard
"ts-pattern@npm:4.3.0":
version: 4.3.0
resolution: "ts-pattern@npm:4.3.0"