{showLogo && (
@@ -647,6 +656,10 @@ const BaseSignIn: FC = ({showLogo = true, ...rest}: BaseSignInP
);
+
+ if (!preferences) return content;
+
+ return
{content};
};
export default BaseSignIn;
diff --git a/packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx b/packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx
index 9ce123f96..f52f37d12 100644
--- a/packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx
+++ b/packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx
@@ -25,6 +25,7 @@ import {
EmbeddedSignInFlowStatusV2,
EmbeddedSignInFlowTypeV2,
FlowMetadataResponse,
+ Preferences,
} from '@asgardeo/browser';
import {FC, ReactElement, useState, useEffect, useRef, ReactNode} from 'react';
// eslint-disable-next-line import/no-named-as-default
@@ -103,6 +104,13 @@ export type SignInProps = {
*/
onSuccess?: (authData: Record
) => void;
+ /**
+ * Component-level preferences to override global i18n and theme settings.
+ * Preferences are deep-merged with global ones, with component preferences
+ * taking precedence. Affects this component and all its descendants.
+ */
+ preferences?: Preferences;
+
/**
* Size variant for the component.
*/
@@ -189,6 +197,7 @@ interface PasskeyState {
*/
const SignIn: FC = ({
className,
+ preferences,
size = 'medium',
onSuccess,
onError,
@@ -196,7 +205,7 @@ const SignIn: FC = ({
children,
}: SignInProps): ReactElement => {
const {applicationId, afterSignInUrl, signIn, isInitialized, isLoading, meta} = useAsgardeo();
- const {t} = useTranslation();
+ const {t} = useTranslation(preferences?.i18n);
// State management for the flow
const [components, setComponents] = useState([]);
@@ -687,6 +696,7 @@ const SignIn: FC = ({
className={className}
size={size}
variant={variant}
+ preferences={preferences}
/>
);
};
diff --git a/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx b/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx
index d9e08fc06..d18ca0629 100644
--- a/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx
+++ b/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx
@@ -24,12 +24,14 @@ import {
withVendorCSSClassPrefix,
EmbeddedFlowComponentTypeV2 as EmbeddedFlowComponentType,
createPackageComponentLogger,
+ Preferences,
} from '@asgardeo/browser';
import {cx} from '@emotion/css';
import {FC, ReactElement, ReactNode, useEffect, useState, useCallback, useRef} from 'react';
import useAsgardeo from '../../../../../contexts/Asgardeo/useAsgardeo';
import FlowProvider from '../../../../../contexts/Flow/FlowProvider';
import useFlow from '../../../../../contexts/Flow/useFlow';
+import ComponentPreferencesContext from '../../../../../contexts/I18n/ComponentPreferencesContext';
import useTheme from '../../../../../contexts/Theme/useTheme';
import {useForm, FormField} from '../../../../../hooks/useForm';
import useTranslation from '../../../../../hooks/useTranslation';
@@ -208,6 +210,13 @@ export interface BaseSignUpProps {
* @returns Promise resolving to the sign-up response.
*/
onSubmit?: (payload: EmbeddedFlowExecuteRequestPayload) => Promise;
+ /**
+ * Component-level preferences to override global i18n and theme settings.
+ * Preferences are deep-merged with global ones, with component preferences
+ * taking precedence. Affects this component and all its descendants.
+ */
+ preferences?: Preferences;
+
/**
* Whether to redirect after sign-up.
*/
@@ -1016,11 +1025,11 @@ const BaseSignUpContent: FC = ({
* This component handles both the presentation layer and sign-up flow logic.
* It accepts API functions as props to maintain framework independence.
*/
-const BaseSignUp: FC = ({showLogo = true, ...rest}: BaseSignUpProps): ReactElement => {
+const BaseSignUp: FC = ({preferences, showLogo = true, ...rest}: BaseSignUpProps): ReactElement => {
const {theme, colorScheme} = useTheme();
const styles: any = useStyles(theme, colorScheme);
- return (
+ const content: ReactElement = (
{showLogo && (
@@ -1032,6 +1041,10 @@ const BaseSignUp: FC = ({showLogo = true, ...rest}: BaseSignUpP
);
+
+ if (!preferences) return content;
+
+ return
{content};
};
export default BaseSignUp;
diff --git a/packages/react/src/contexts/FlowMeta/FlowMetaContext.ts b/packages/react/src/contexts/FlowMeta/FlowMetaContext.ts
index ab9742535..c8de880a3 100644
--- a/packages/react/src/contexts/FlowMeta/FlowMetaContext.ts
+++ b/packages/react/src/contexts/FlowMeta/FlowMetaContext.ts
@@ -36,6 +36,11 @@ export interface FlowMetaContextValue {
* The fetched flow metadata response, or null while loading / on error
*/
meta: FlowMetadataResponse | null;
+ /**
+ * Fetches flow metadata for the given language and activates it in the i18n system.
+ * Use this to switch the UI language at runtime.
+ */
+ switchLanguage: (language: string) => Promise
;
}
const FlowMetaContext: Context = createContext(null);
diff --git a/packages/react/src/contexts/FlowMeta/FlowMetaProvider.tsx b/packages/react/src/contexts/FlowMeta/FlowMetaProvider.tsx
index 08627b643..3c7fbd092 100644
--- a/packages/react/src/contexts/FlowMeta/FlowMetaProvider.tsx
+++ b/packages/react/src/contexts/FlowMeta/FlowMetaProvider.tsx
@@ -17,7 +17,7 @@
*/
import {FlowMetadataResponse, FlowMetaType, getFlowMetaV2} from '@asgardeo/browser';
-import {I18nBundle} from '@asgardeo/i18n';
+import {I18nBundle, TranslationBundleConstants} from '@asgardeo/i18n';
import {
FC,
PropsWithChildren,
@@ -74,6 +74,7 @@ const FlowMetaProvider: FC> = ({
const [meta, setMeta] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
+ const [pendingLanguage, setPendingLanguage] = useState(null);
// Track whether an initial fetch has been triggered so we don't double-fetch
// when the component first mounts with a stable config reference.
@@ -98,6 +99,56 @@ const FlowMetaProvider: FC> = ({
}
}, [enabled, baseUrl, applicationId]);
+ const switchLanguage: (language: string) => Promise = useCallback(
+ async (language: string): Promise => {
+ if (!enabled) return;
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const result: FlowMetadataResponse = await getFlowMetaV2({
+ baseUrl,
+ id: applicationId,
+ language,
+ type: FlowMetaType.App,
+ });
+
+ // Inject translations for the new language before switching
+ if (result.i18n?.translations && i18nContext?.injectBundles) {
+ const flatTranslations: Record = {};
+ Object.entries(result.i18n.translations).forEach(([namespace, keys]: [string, Record]) => {
+ Object.entries(keys).forEach(([key, value]: [string, string]) => {
+ flatTranslations[`${namespace}.${key}`] = value;
+ });
+ });
+ const bundle: I18nBundle = {translations: flatTranslations} as unknown as I18nBundle;
+ i18nContext.injectBundles({[language]: bundle});
+ }
+
+ // Defer setLanguage to the next effect cycle so injectBundles state
+ // is committed before I18nProvider's setLanguage checks mergedBundles.
+ setPendingLanguage(language);
+ setMeta(result);
+ } catch (err: unknown) {
+ setError(err instanceof Error ? err : new Error(String(err)));
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [enabled, baseUrl, applicationId, i18nContext],
+ );
+
+ // After injectBundles + setPendingLanguage are batched and committed, this
+ // effect fires with the updated i18nContext (mergedBundles now includes the
+ // new language), so setLanguage succeeds on the first switch.
+ useEffect(() => {
+ if (pendingLanguage && i18nContext?.setLanguage) {
+ i18nContext.setLanguage(pendingLanguage);
+ setPendingLanguage(null);
+ }
+ }, [pendingLanguage, i18nContext?.setLanguage]);
+
useEffect(() => {
if (!hasFetchedRef.current) {
hasFetchedRef.current = true;
@@ -115,7 +166,7 @@ const FlowMetaProvider: FC> = ({
return;
}
- const metaLanguage: string = meta.i18n.language || 'en';
+ const metaLanguage: string = meta.i18n.language || TranslationBundleConstants.FALLBACK_LOCALE;
// Flatten namespace-keyed translations to dot-path keys:
// { "signin": { "heading": "Sign In" } } → { "signin.heading": "Sign In" }
@@ -146,6 +197,7 @@ const FlowMetaProvider: FC> = ({
fetchFlowMeta,
isLoading,
meta,
+ switchLanguage,
};
return {children};
diff --git a/packages/react/src/contexts/I18n/ComponentPreferencesContext.ts b/packages/react/src/contexts/I18n/ComponentPreferencesContext.ts
new file mode 100644
index 000000000..1ede3089b
--- /dev/null
+++ b/packages/react/src/contexts/I18n/ComponentPreferencesContext.ts
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Preferences} from '@asgardeo/browser';
+import {Context, createContext} from 'react';
+
+/**
+ * Context for component-level preferences overrides.
+ * Presentational components can provide this context to override the global i18n
+ * and theme settings for their entire subtree, including all nested components.
+ */
+const ComponentPreferencesContext: Context = createContext(undefined);
+
+export default ComponentPreferencesContext;
diff --git a/packages/react/src/contexts/I18n/I18nProvider.tsx b/packages/react/src/contexts/I18n/I18nProvider.tsx
index 37d3222ef..14147afa4 100644
--- a/packages/react/src/contexts/I18n/I18nProvider.tsx
+++ b/packages/react/src/contexts/I18n/I18nProvider.tsx
@@ -16,8 +16,14 @@
* under the License.
*/
-import {deepMerge, I18nPreferences, createPackageComponentLogger} from '@asgardeo/browser';
-import {I18nBundle, getDefaultI18nBundles} from '@asgardeo/i18n';
+import {deepMerge, I18nPreferences, I18nStorageStrategy, createPackageComponentLogger} from '@asgardeo/browser';
+import {
+ I18nBundle,
+ I18nTranslations,
+ TranslationBundleConstants,
+ getDefaultI18nBundles,
+ normalizeTranslations,
+} from '@asgardeo/i18n';
import {FC, PropsWithChildren, ReactElement, useCallback, useEffect, useMemo, useState} from 'react';
import I18nContext, {I18nContextValue} from './I18nContext';
@@ -26,7 +32,8 @@ const logger: ReturnType = createPackageCom
'I18nProvider',
);
-const I18N_LANGUAGE_STORAGE_KEY: string = 'asgardeo-i18n-language';
+const DEFAULT_STORAGE_KEY: string = 'asgardeo-i18n-language';
+const DEFAULT_URL_PARAM: string = 'lang';
export interface I18nProviderProps {
/**
@@ -35,42 +42,86 @@ export interface I18nProviderProps {
preferences?: I18nPreferences;
}
-/**
- * Detects the browser's default language or returns a fallback
- */
const detectBrowserLanguage = (): string => {
if (typeof window !== 'undefined' && window.navigator) {
- return window.navigator.language || 'en-US';
+ return window.navigator.language || TranslationBundleConstants.FALLBACK_LOCALE;
}
- return 'en-US';
+
+ return TranslationBundleConstants.FALLBACK_LOCALE;
};
-/**
- * Gets the stored language from localStorage or returns null
- */
-const getStoredLanguage = (): string | null => {
- if (typeof window !== 'undefined' && window.localStorage) {
- try {
- return window.localStorage.getItem(I18N_LANGUAGE_STORAGE_KEY);
- } catch (error) {
- // localStorage might not be available or accessible
- return null;
- }
+const deriveRootDomain = (hostname: string): string => {
+ const parts: string[] = hostname.split('.');
+ return parts.length > 1 ? parts.slice(-2).join('.') : hostname;
+};
+
+const getCookie = (name: string): string | null => {
+ if (typeof document === 'undefined') return null;
+ const match: RegExpMatchArray | null = document.cookie.match(
+ new RegExp(`(?:^|; )${name.replace(/([.*+?^${}()|[\]\\])/g, '\\$1')}=([^;]*)`),
+ );
+ return match ? decodeURIComponent(match[1]) : null;
+};
+
+const setCookie = (name: string, value: string, domain: string): void => {
+ if (typeof document === 'undefined') return;
+ const maxAge: number = 365 * 24 * 60 * 60;
+ const secure: string = typeof window !== 'undefined' && window.location.protocol === 'https:' ? '; Secure' : '';
+ document.cookie =
+ `${encodeURIComponent(name)}=${encodeURIComponent(value)}` +
+ `; Max-Age=${maxAge}` +
+ `; Path=/` +
+ `; Domain=${domain}` +
+ `; SameSite=Lax${secure}`;
+};
+
+interface StorageAdapter {
+ read: () => string | null;
+ write: (language: string) => void;
+}
+
+const createStorageAdapter = (strategy: I18nStorageStrategy, key: string, cookieDomain?: string): StorageAdapter => {
+ switch (strategy) {
+ case 'cookie':
+ return {
+ read: (): string | null => getCookie(key),
+ write: (language: string): void => {
+ const domain: string =
+ cookieDomain ?? (typeof window !== 'undefined' ? deriveRootDomain(window.location.hostname) : '');
+ if (domain) setCookie(key, language, domain);
+ },
+ };
+ case 'localStorage':
+ return {
+ read: (): string | null => {
+ if (typeof window === 'undefined' || !window.localStorage) return null;
+ try {
+ return window.localStorage.getItem(key);
+ } catch {
+ return null;
+ }
+ },
+ write: (language: string): void => {
+ if (typeof window === 'undefined' || !window.localStorage) return;
+ try {
+ window.localStorage.setItem(key, language);
+ } catch {
+ logger.warn('Failed to persist language preference to localStorage.');
+ }
+ },
+ };
+ case 'none':
+ default:
+ return {read: (): null => null, write: (): void => {}};
}
- return null;
};
-/**
- * Stores the language in localStorage
- */
-const storeLanguage = (language: string): void => {
- if (typeof window !== 'undefined' && window.localStorage) {
- try {
- window.localStorage.setItem(I18N_LANGUAGE_STORAGE_KEY, language);
- } catch (error) {
- // localStorage might not be available or accessible
- logger.warn('Failed to store language preference:');
- }
+const detectUrlParamLanguage = (paramName: string): string | null => {
+ if (typeof window === 'undefined') return null;
+ try {
+ return new URLSearchParams(window.location.search).get(paramName);
+ } catch {
+ return null;
}
};
@@ -85,18 +136,35 @@ const I18nProvider: FC> = ({
// Get default bundles from the browser package
const defaultBundles: Record = getDefaultI18nBundles();
- // Determine the initial language based on preference order:
- // 1. User preference from config
- // 2. Stored language in localStorage
- // 3. Browser's default language
- // 4. Fallback language
+ const storageStrategy: I18nStorageStrategy = preferences?.storageStrategy ?? 'cookie';
+ const storageKey: string = preferences?.storageKey ?? DEFAULT_STORAGE_KEY;
+ const urlParamConfig: string | false = preferences?.urlParam === undefined ? DEFAULT_URL_PARAM : preferences.urlParam;
+
+ const resolvedCookieDomain: string | undefined = useMemo((): string | undefined => {
+ if (storageStrategy !== 'cookie') return undefined;
+ if (preferences?.cookieDomain) return preferences.cookieDomain;
+ return typeof window !== 'undefined' ? deriveRootDomain(window.location.hostname) : undefined;
+ }, [storageStrategy, preferences?.cookieDomain]);
+
+ const storage: StorageAdapter = useMemo(
+ () => createStorageAdapter(storageStrategy, storageKey, resolvedCookieDomain),
+ [storageStrategy, storageKey, resolvedCookieDomain],
+ );
+
const determineInitialLanguage = (): string => {
- const configLanguage: string | undefined = preferences?.language;
- const storedLanguage: string | null = getStoredLanguage();
+ if (preferences?.language) return preferences.language;
+ if (urlParamConfig !== false) {
+ const urlLanguage: string | null = detectUrlParamLanguage(urlParamConfig);
+ if (urlLanguage) {
+ storage.write(urlLanguage);
+ return urlLanguage;
+ }
+ }
+ const storedLanguage: string | null = storage.read();
+ if (storedLanguage) return storedLanguage;
const browserLanguage: string = detectBrowserLanguage();
- const fallbackLanguage: string = preferences?.fallbackLanguage || 'en-US';
-
- return configLanguage || storedLanguage || browserLanguage || fallbackLanguage;
+ if (browserLanguage) return browserLanguage;
+ return preferences?.fallbackLanguage || TranslationBundleConstants.FALLBACK_LOCALE;
};
const [currentLanguage, setCurrentLanguage] = useState(determineInitialLanguage);
@@ -110,13 +178,16 @@ const I18nProvider: FC> = ({
setInjectedBundles((prev: Record) => {
const merged: Record = {...prev};
Object.entries(newBundles).forEach(([key, bundle]: [string, I18nBundle]) => {
+ const normalizedTranslations: I18nTranslations = normalizeTranslations(
+ bundle.translations as unknown as Record>,
+ );
if (merged[key]) {
merged[key] = {
...merged[key],
- translations: deepMerge(merged[key].translations, bundle.translations),
+ translations: deepMerge(merged[key].translations, normalizedTranslations),
};
} else {
- merged[key] = bundle;
+ merged[key] = {...bundle, translations: normalizedTranslations};
}
});
return merged;
@@ -140,27 +211,33 @@ const I18nProvider: FC> = ({
// 2. Injected bundles (e.g., from flow metadata) — override defaults
Object.entries(injectedBundles).forEach(([key, bundle]: [string, I18nBundle]) => {
+ const normalizedTranslations: I18nTranslations = normalizeTranslations(
+ bundle.translations as unknown as Record>,
+ );
if (merged[key]) {
merged[key] = {
...merged[key],
- translations: deepMerge(merged[key].translations, bundle.translations),
+ translations: deepMerge(merged[key].translations, normalizedTranslations),
};
} else {
- merged[key] = bundle;
+ merged[key] = {...bundle, translations: normalizedTranslations};
}
});
// 3. User-provided bundles (from props) — highest priority, override everything
if (preferences?.bundles) {
Object.entries(preferences.bundles).forEach(([key, userBundle]: [string, I18nBundle]) => {
+ const normalizedTranslations: I18nTranslations = normalizeTranslations(
+ userBundle.translations as unknown as Record>,
+ );
if (merged[key]) {
merged[key] = {
...merged[key],
metadata: userBundle.metadata ? {...merged[key].metadata, ...userBundle.metadata} : merged[key].metadata,
- translations: deepMerge(merged[key].translations, userBundle.translations),
+ translations: deepMerge(merged[key].translations, normalizedTranslations),
};
} else {
- merged[key] = userBundle;
+ merged[key] = {...userBundle, translations: normalizedTranslations};
}
});
}
@@ -168,12 +245,12 @@ const I18nProvider: FC> = ({
return merged;
}, [defaultBundles, injectedBundles, preferences?.bundles]);
- const fallbackLanguage: string = preferences?.fallbackLanguage || 'en-US';
+ const fallbackLanguage: string = preferences?.fallbackLanguage || TranslationBundleConstants.FALLBACK_LOCALE;
- // Update stored language when current language changes
+ // Persist language changes to the configured storage.
useEffect(() => {
- storeLanguage(currentLanguage);
- }, [currentLanguage]);
+ storage.write(currentLanguage);
+ }, [currentLanguage, storage]);
// Translation function
const t: (key: string, params?: Record) => string = useCallback(
diff --git a/packages/react/src/hooks/useTranslation.ts b/packages/react/src/hooks/useTranslation.ts
index 3a0e122ea..826898af6 100644
--- a/packages/react/src/hooks/useTranslation.ts
+++ b/packages/react/src/hooks/useTranslation.ts
@@ -16,9 +16,10 @@
* under the License.
*/
-import {deepMerge, I18nPreferences} from '@asgardeo/browser';
-import {I18nBundle} from '@asgardeo/i18n';
+import {deepMerge, I18nPreferences, Preferences} from '@asgardeo/browser';
+import {I18nBundle, I18nTranslations, normalizeTranslations} from '@asgardeo/i18n';
import {useContext, useMemo} from 'react';
+import ComponentPreferencesContext from '../contexts/I18n/ComponentPreferencesContext';
import I18nContext from '../contexts/I18n/I18nContext';
export interface UseTranslation {
@@ -60,6 +61,8 @@ export interface UseTranslationWithPreferences extends UseTranslation {
*/
const useTranslation = (componentPreferences?: I18nPreferences): UseTranslationWithPreferences => {
const context: any = useContext(I18nContext);
+ const componentPrefs: Preferences | undefined = useContext(ComponentPreferencesContext);
+ const contextPreferences: I18nPreferences | undefined = componentPrefs?.i18n;
if (!context) {
throw new Error(
@@ -67,11 +70,14 @@ const useTranslation = (componentPreferences?: I18nPreferences): UseTranslationW
);
}
+ // Direct parameter takes precedence over context-provided preferences
+ const effectivePreferences: I18nPreferences | undefined = componentPreferences ?? contextPreferences;
+
const {t: globalT, currentLanguage, setLanguage, bundles: globalBundles, fallbackLanguage} = context;
// Merge global bundles with component-level bundles if provided
const mergedBundles: Record = useMemo(() => {
- if (!componentPreferences?.bundles) {
+ if (!effectivePreferences?.bundles) {
return globalBundles;
}
@@ -83,7 +89,10 @@ const useTranslation = (componentPreferences?: I18nPreferences): UseTranslationW
});
// Merge component-level bundles using deepMerge for better merging
- Object.entries(componentPreferences.bundles).forEach(([key, componentBundle]: [string, I18nBundle]) => {
+ Object.entries(effectivePreferences.bundles).forEach(([key, componentBundle]: [string, I18nBundle]) => {
+ const normalizedTranslations: I18nTranslations = normalizeTranslations(
+ componentBundle.translations as unknown as Record>,
+ );
if (merged[key]) {
// Deep merge component bundle with existing global bundle
merged[key] = {
@@ -91,20 +100,20 @@ const useTranslation = (componentPreferences?: I18nPreferences): UseTranslationW
metadata: componentBundle.metadata
? {...merged[key].metadata, ...componentBundle.metadata}
: merged[key].metadata,
- translations: deepMerge(merged[key].translations, componentBundle.translations),
+ translations: deepMerge(merged[key].translations, normalizedTranslations),
};
} else {
// No global bundle for this language, use component bundle as-is
- merged[key] = componentBundle;
+ merged[key] = {...componentBundle, translations: normalizedTranslations};
}
});
return merged;
- }, [globalBundles, componentPreferences?.bundles]);
+ }, [globalBundles, effectivePreferences?.bundles]);
// Create enhanced translation function that uses merged bundles
const enhancedT: (key: string, params?: Record) => string = useMemo(() => {
- if (!componentPreferences?.bundles) {
+ if (!effectivePreferences?.bundles) {
// No component preferences, use global translation function
return globalT;
}
@@ -114,14 +123,14 @@ const useTranslation = (componentPreferences?: I18nPreferences): UseTranslationW
// Try to get translation from current language bundle
const currentBundle: I18nBundle | undefined = mergedBundles[currentLanguage];
- if (currentBundle?.translations[key]) {
+ if (currentBundle?.translations?.[key]) {
translation = currentBundle.translations[key];
}
// Fallback to fallback language if translation not found
if (!translation && currentLanguage !== fallbackLanguage) {
const fallbackBundle: I18nBundle | undefined = mergedBundles[fallbackLanguage];
- if (fallbackBundle?.translations[key]) {
+ if (fallbackBundle?.translations?.[key]) {
translation = fallbackBundle.translations[key];
}
}
@@ -142,7 +151,7 @@ const useTranslation = (componentPreferences?: I18nPreferences): UseTranslationW
return translation;
};
- }, [mergedBundles, currentLanguage, fallbackLanguage, globalT, componentPreferences?.bundles]);
+ }, [mergedBundles, currentLanguage, fallbackLanguage, globalT, effectivePreferences?.bundles]);
return {
availableLanguages: Object.keys(mergedBundles),
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 4c51b7ac0..b13cc3bc5 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -193,6 +193,16 @@ export type {
export {CreateOrganization} from './components/presentation/CreateOrganization/CreateOrganization';
+export {default as BaseLanguageSwitcher} from './components/presentation/LanguageSwitcher/BaseLanguageSwitcher';
+export type {
+ BaseLanguageSwitcherProps,
+ LanguageOption,
+ LanguageSwitcherRenderProps,
+} from './components/presentation/LanguageSwitcher/BaseLanguageSwitcher';
+
+export {default as LanguageSwitcher} from './components/presentation/LanguageSwitcher/LanguageSwitcher';
+export type {LanguageSwitcherProps} from './components/presentation/LanguageSwitcher/LanguageSwitcher';
+
export {default as Button} from './components/primitives/Button/Button';
export * from './components/primitives/Button/Button';