mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-19 00:23:35 -05:00
Compare commits
1 Commits
cursor/for
...
chore/lazl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac208e8722 |
@@ -5,7 +5,7 @@ import { type TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { LanguageIcon } from "@/components/icons/language-icon";
|
||||
import { mixColor } from "@/lib/color";
|
||||
import { getI18nLanguage } from "@/lib/i18n-utils";
|
||||
import i18n from "@/lib/i18n.config";
|
||||
import i18n, { loadLanguage } from "@/lib/i18n.config";
|
||||
import { getLanguageDisplayName } from "@/lib/language-display-name";
|
||||
import { useClickOutside } from "@/lib/use-click-outside-hook";
|
||||
import { cn, isRTLLanguage } from "@/lib/utils";
|
||||
@@ -47,9 +47,11 @@ export function LanguageSwitch({
|
||||
|
||||
const handleI18nLanguage = (languageCode: string) => {
|
||||
const calculatedLanguage = getI18nLanguage(languageCode, surveyLanguages);
|
||||
if (i18n.language !== calculatedLanguage) {
|
||||
i18n.changeLanguage(calculatedLanguage);
|
||||
}
|
||||
loadLanguage(calculatedLanguage).then(() => {
|
||||
if (i18n.language !== calculatedLanguage) {
|
||||
i18n.changeLanguage(calculatedLanguage);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const changeLanguage = (languageCode: string) => {
|
||||
|
||||
@@ -1,22 +1,42 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import i18n from "../../lib/i18n.config";
|
||||
import i18n, { loadLanguage } from "../../lib/i18n.config";
|
||||
|
||||
export const I18nProvider = ({ language, children }: { language: string; children?: ComponentChildren }) => {
|
||||
// Set language synchronously on initial render so children get the correct translations immediately.
|
||||
// This is safe because all translations are pre-loaded (bundled) in i18n.config.ts.
|
||||
if (i18n.language !== language) {
|
||||
// If translations are already available (English or previously loaded), set language synchronously
|
||||
const alreadyLoaded = language === "en" || i18n.hasResourceBundle(language, "translation");
|
||||
if (alreadyLoaded && i18n.language !== language) {
|
||||
i18n.changeLanguage(language);
|
||||
}
|
||||
|
||||
// Handle language prop changes after initial render
|
||||
const [isReady, setIsReady] = useState(alreadyLoaded);
|
||||
|
||||
useEffect(() => {
|
||||
if (i18n.language !== language) {
|
||||
i18n.changeLanguage(language);
|
||||
if (i18n.hasResourceBundle(language, "translation")) {
|
||||
if (i18n.language !== language) {
|
||||
i18n.changeLanguage(language);
|
||||
}
|
||||
setIsReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setIsReady(false);
|
||||
loadLanguage(language).then(() => {
|
||||
if (!cancelled) {
|
||||
i18n.changeLanguage(language);
|
||||
setIsReady(true);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [language]);
|
||||
|
||||
if (!isReady) return null;
|
||||
|
||||
// work around for react-i18next not supporting preact
|
||||
return <I18nextProvider i18n={i18n}>{children as unknown as React.ReactNode}</I18nextProvider>;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RenderSurvey } from "@/components/general/render-survey";
|
||||
import { I18nProvider } from "@/components/i18n/provider";
|
||||
import { FILE_PICK_EVENT } from "@/lib/constants";
|
||||
import { getI18nLanguage } from "@/lib/i18n-utils";
|
||||
import { setLocaleBaseUrl } from "@/lib/i18n.config";
|
||||
import { addCustomThemeToDom, addStylesToDom, setStyleNonce } from "@/lib/styles";
|
||||
|
||||
export const renderSurveyInline = (props: SurveyContainerProps) => {
|
||||
@@ -19,8 +20,9 @@ export const renderSurvey = (props: SurveyContainerProps) => {
|
||||
// render SurveyNew
|
||||
// if survey type is link, we don't pass the placement, overlay, clickOutside, onClose
|
||||
|
||||
const { mode, containerId, languageCode } = props;
|
||||
const { mode, containerId, languageCode, appUrl } = props;
|
||||
|
||||
setLocaleBaseUrl(`${appUrl || ""}/js/locales`);
|
||||
addStylesToDom();
|
||||
addCustomThemeToDom({ styling: props.styling });
|
||||
|
||||
|
||||
@@ -1,70 +1,77 @@
|
||||
import i18n from "i18next";
|
||||
import ICU from "i18next-icu";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import arTranslations from "../../locales/ar.json";
|
||||
import daTranslations from "../../locales/da.json";
|
||||
import deTranslations from "../../locales/de.json";
|
||||
import enTranslations from "../../locales/en.json";
|
||||
import esTranslations from "../../locales/es.json";
|
||||
import frTranslations from "../../locales/fr.json";
|
||||
import hiTranslations from "../../locales/hi.json";
|
||||
import huTranslations from "../../locales/hu.json";
|
||||
import itTranslations from "../../locales/it.json";
|
||||
import jaTranslations from "../../locales/ja.json";
|
||||
import nlTranslations from "../../locales/nl.json";
|
||||
import ptTranslations from "../../locales/pt.json";
|
||||
import roTranslations from "../../locales/ro.json";
|
||||
import ruTranslations from "../../locales/ru.json";
|
||||
import svTranslations from "../../locales/sv.json";
|
||||
import uzTranslations from "../../locales/uz.json";
|
||||
import zhHansTranslations from "../../locales/zh-Hans.json";
|
||||
|
||||
const SUPPORTED_LOCALES = [
|
||||
"ar",
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"hi",
|
||||
"hu",
|
||||
"it",
|
||||
"ja",
|
||||
"nl",
|
||||
"pt",
|
||||
"ro",
|
||||
"ru",
|
||||
"sv",
|
||||
"uz",
|
||||
"zh-Hans",
|
||||
] as const;
|
||||
|
||||
i18n
|
||||
.use(ICU)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
supportedLngs: [
|
||||
"ar",
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fr",
|
||||
"hi",
|
||||
"hu",
|
||||
"it",
|
||||
"ja",
|
||||
"nl",
|
||||
"pt",
|
||||
"ro",
|
||||
"ru",
|
||||
"sv",
|
||||
"uz",
|
||||
"zh-Hans",
|
||||
],
|
||||
supportedLngs: [...SUPPORTED_LOCALES],
|
||||
|
||||
resources: {
|
||||
ar: { translation: arTranslations },
|
||||
da: { translation: daTranslations },
|
||||
de: { translation: deTranslations },
|
||||
en: { translation: enTranslations },
|
||||
es: { translation: esTranslations },
|
||||
fr: { translation: frTranslations },
|
||||
hi: { translation: hiTranslations },
|
||||
hu: { translation: huTranslations },
|
||||
it: { translation: itTranslations },
|
||||
ja: { translation: jaTranslations },
|
||||
nl: { translation: nlTranslations },
|
||||
pt: { translation: ptTranslations },
|
||||
ro: { translation: roTranslations },
|
||||
ru: { translation: ruTranslations },
|
||||
sv: { translation: svTranslations },
|
||||
uz: { translation: uzTranslations },
|
||||
"zh-Hans": { translation: zhHansTranslations },
|
||||
},
|
||||
|
||||
interpolation: { escapeValue: false },
|
||||
});
|
||||
|
||||
let localeBaseUrl = "";
|
||||
|
||||
export const setLocaleBaseUrl = (url: string) => {
|
||||
localeBaseUrl = url;
|
||||
};
|
||||
|
||||
const pendingLoads: Record<string, Promise<void>> = {};
|
||||
|
||||
export const loadLanguage = async (lng: string): Promise<void> => {
|
||||
if (lng === "en" || i18n.hasResourceBundle(lng, "translation")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lng in pendingLoads) {
|
||||
return pendingLoads[lng];
|
||||
}
|
||||
|
||||
if (!localeBaseUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingLoads[lng] = (async () => {
|
||||
try {
|
||||
const response = await fetch(`${localeBaseUrl}/${lng}.json`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const translations = await response.json();
|
||||
i18n.addResourceBundle(lng, "translation", translations);
|
||||
} catch {
|
||||
console.warn(`[formbricks] Failed to load translations for "${lng}". Using English fallback.`);
|
||||
} finally {
|
||||
delete pendingLoads[lng];
|
||||
}
|
||||
})();
|
||||
|
||||
return pendingLoads[lng];
|
||||
};
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import preact from "@preact/preset-vite";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { loadEnv } from "vite";
|
||||
import { type PluginOption, loadEnv } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { defineConfig } from "vitest/config";
|
||||
@@ -27,7 +27,7 @@ const config = ({ mode }) => {
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(mode),
|
||||
},
|
||||
plugins: [preact(), tsconfigPaths()],
|
||||
plugins: [preact(), tsconfigPaths() as PluginOption],
|
||||
};
|
||||
|
||||
// Check if we're building the UMD bundle (separate build step)
|
||||
@@ -53,7 +53,11 @@ const config = ({ mode }) => {
|
||||
},
|
||||
plugins: [
|
||||
...sharedConfig.plugins,
|
||||
copyCompiledAssetsPlugin({ filename: "surveys", distDir: resolve(__dirname, "dist") }),
|
||||
copyCompiledAssetsPlugin({
|
||||
filename: "surveys",
|
||||
distDir: resolve(__dirname, "dist"),
|
||||
localesDir: resolve(__dirname, "locales"),
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -63,11 +67,7 @@ const config = ({ mode }) => {
|
||||
...sharedConfig,
|
||||
test: {
|
||||
name: "surveys",
|
||||
environment: "node",
|
||||
environmentMatchGlobs: [
|
||||
["**/*.test.tsx", "jsdom"],
|
||||
["**/lib/**/*.test.ts", "jsdom"],
|
||||
],
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./vitestSetup.ts"],
|
||||
include: ["**/*.test.ts", "**/*.test.tsx"],
|
||||
exclude: ["dist/**", "node_modules/**"],
|
||||
@@ -109,7 +109,7 @@ const config = ({ mode }) => {
|
||||
entryRoot: "src",
|
||||
}),
|
||||
copyCompiledAssetsPlugin({ filename: "surveys", distDir: resolve(__dirname, "dist") }),
|
||||
process.env.ANALYZE === "true" && visualizer({ filename: resolve(__dirname, "stats.html"), open: false, gzipSize: true, brotliSize: true }),
|
||||
process.env.ANALYZE === "true" && (visualizer({ filename: resolve(__dirname, "stats.html"), open: false, gzipSize: true, brotliSize: true }) as PluginOption),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ interface CopyCompiledAssetsPluginOptions {
|
||||
filename: string;
|
||||
distDir: string;
|
||||
skipDirectoryCheck?: boolean; // New option to skip checking non-existent directories
|
||||
localesDir?: string; // Optional directory containing locale JSON files to copy
|
||||
}
|
||||
|
||||
const ensureDirectoryExists = async (dirPath: string): Promise<void> => {
|
||||
@@ -85,6 +86,22 @@ export function copyCompiledAssetsPlugin(options: CopyCompiledAssetsPluginOption
|
||||
}
|
||||
|
||||
console.log(`Copied ${String(copiedFiles)} files to ${outputDir} (${options.filename})`);
|
||||
|
||||
// Copy locale JSON files if localesDir is specified
|
||||
if (options.localesDir) {
|
||||
const localesOutputDir = path.resolve(outputDir, "locales");
|
||||
await ensureDirectoryExists(localesOutputDir);
|
||||
|
||||
const localeFiles = (await readdir(options.localesDir)).filter(
|
||||
(f) => f.endsWith(".json") && f !== "i18n.json"
|
||||
);
|
||||
|
||||
for (const file of localeFiles) {
|
||||
await copyFile(path.resolve(options.localesDir, file), path.resolve(localesOutputDir, file));
|
||||
}
|
||||
|
||||
console.log(`Copied ${String(localeFiles.length)} locale files to ${localesOutputDir}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (options.skipDirectoryCheck) {
|
||||
console.error(
|
||||
|
||||
Reference in New Issue
Block a user