Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/i18n/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export * from './translations';

// Utils
export {default as getDefaultI18nBundles} from './utils/getDefaultI18nBundles';
export {default as normalizeTranslations} from './utils/normalizeTranslations';
68 changes: 68 additions & 0 deletions packages/i18n/src/utils/normalizeTranslations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2025, 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 {I18nTranslations} from '../models/i18n';

/**
* Accepts translations in either flat or namespaced format and normalizes them
* to the flat format required by the SDK.
*
* Flat format (already correct):
* ```ts
* { "signin.heading": "Sign In" }
* ```
*
* Namespaced format (auto-converted):
* ```ts
* { signin: { heading: "Sign In" } }
* ```
*
* Both formats can be mixed within the same object — a top-level string value
* is kept as-is, while a top-level object value is flattened one level deep
* using `"namespace.key"` concatenation.
*
* @param translations - Translations in flat or namespaced format.
* @returns Normalized flat translations compatible with `I18nTranslations`.
*/
const normalizeTranslations = (
translations: Record<string, string | Record<string, string>> | null | undefined,
): I18nTranslations => {
if (!translations || typeof translations !== 'object') {
return {} as unknown as I18nTranslations;
}

const result: Record<string, string> = {};

Object.entries(translations).forEach(([topKey, value]: [string, string | Record<string, string>]) => {
if (typeof value === 'string') {
// Already flat — keep as-is (e.g., "signin.heading": "Sign In")
result[topKey] = value;
} else if (value !== null && typeof value === 'object') {
// Namespaced — flatten one level (e.g., signin: { heading: "Sign In" } → "signin.heading": "Sign In")
Object.entries(value).forEach(([subKey, subValue]: [string, string]) => {
if (typeof subValue === 'string') {
result[`${topKey}.${subKey}`] = subValue;
}
});
}
});

return result as unknown as I18nTranslations;
};

export default normalizeTranslations;
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* under the License.
*/

import {CreateOrganizationPayload, createPackageComponentLogger} from '@asgardeo/browser';
import {CreateOrganizationPayload, createPackageComponentLogger, Preferences} from '@asgardeo/browser';
import {cx} from '@emotion/css';
import {ChangeEvent, CSSProperties, FC, FormEvent, ReactElement, ReactNode, useState} from 'react';
import useStyles from './BaseCreateOrganization.styles';
Expand Down Expand Up @@ -62,6 +62,13 @@ export interface BaseCreateOrganizationProps {
renderAdditionalFields?: () => ReactNode;
style?: CSSProperties;
title?: string;

/**
* 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;
}

/**
Expand Down Expand Up @@ -95,13 +102,14 @@ export const BaseCreateOrganization: FC<BaseCreateOrganizationProps> = ({
onSubmit,
onSuccess,
open = false,
preferences,
renderAdditionalFields,
style,
title = 'Create Organization',
}: BaseCreateOrganizationProps): ReactElement => {
const {theme, colorScheme} = useTheme();
const styles: ReturnType<typeof useStyles> = useStyles(theme, colorScheme);
const {t} = useTranslation();
const {t} = useTranslation(preferences?.i18n);
const [formData, setFormData] = useState<OrganizationFormData>({
description: '',
handle: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* under the License.
*/

import {AllOrganizationsApiResponse, Organization} from '@asgardeo/browser';
import {AllOrganizationsApiResponse, Organization, Preferences} from '@asgardeo/browser';
import {cx} from '@emotion/css';
import {CSSProperties, FC, MouseEvent, ReactElement, ReactNode, useMemo} from 'react';
import useStyles from './BaseOrganizationList.styles';
Expand Down Expand Up @@ -120,6 +120,13 @@ export interface BaseOrganizationListProps {
* Title for the popup dialog (only used in popup mode)
*/
title?: string;

/**
* 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;
}

/**
Expand Down Expand Up @@ -269,10 +276,11 @@ export const BaseOrganizationList: FC<BaseOrganizationListProps> = ({
style,
title = 'Organizations',
showStatus,
preferences,
}: BaseOrganizationListProps): ReactElement => {
const {theme, colorScheme} = useTheme();
const styles: ReturnType<typeof useStyles> = useStyles(theme, colorScheme);
const {t} = useTranslation();
const {t} = useTranslation(preferences?.i18n);

const organizationsWithSwitchAccess: OrganizationWithSwitchAccess[] = useMemo(() => {
if (!allOrganizations?.organizations) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* under the License.
*/

import {OrganizationDetails, formatDate} from '@asgardeo/browser';
import {OrganizationDetails, formatDate, Preferences} from '@asgardeo/browser';
import {cx} from '@emotion/css';
import {FC, ReactElement, ReactNode, useState, useCallback} from 'react';
import useStyles from './BaseOrganizationProfile.styles';
Expand Down Expand Up @@ -108,6 +108,13 @@ export interface BaseOrganizationProfileProps {
* Custom title for the profile.
*/
title?: string;

/**
* 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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* under the License.
*/

import {OrganizationDetails, createPackageComponentLogger} from '@asgardeo/browser';
import {OrganizationDetails, createPackageComponentLogger, Preferences} from '@asgardeo/browser';
import {FC, ReactElement, useEffect, useState} from 'react';
import BaseOrganizationProfile, {BaseOrganizationProfileProps} from './BaseOrganizationProfile';
import getOrganization from '../../../api/getOrganization';
Expand Down Expand Up @@ -144,10 +144,11 @@ const OrganizationProfile: FC<OrganizationProfileProps> = ({
popupTitle,
loadingFallback,
errorFallback,
preferences,
...rest
}: OrganizationProfileProps): ReactElement => {
const {baseUrl, instanceId} = useAsgardeo();
const {t} = useTranslation();
const {t} = useTranslation(preferences?.i18n);
const [organization, setOrganization] = useState<OrganizationDetails | null>(null);

const fetchOrganization = async (): Promise<void> => {
Expand Down Expand Up @@ -206,6 +207,7 @@ const OrganizationProfile: FC<OrganizationProfileProps> = ({
open={open}
onOpenChange={onOpenChange}
title={popupTitle || t('organization.profile.heading')}
preferences={preferences}
{...rest}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/

// Removed BEM and vendor prefix utilities
import {Preferences} from '@asgardeo/browser';
import {cx} from '@emotion/css';
import {
useFloating,
Expand Down Expand Up @@ -159,6 +160,13 @@ export interface BaseOrganizationSwitcherProps {
* Custom styles for the component.
*/
style?: CSSProperties;

/**
* 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;
}

/**
Expand All @@ -185,12 +193,13 @@ export const BaseOrganizationSwitcher: FC<BaseOrganizationSwitcherProps> = ({
showTriggerLabel = true,
avatarSize = 24,
fallback = null,
preferences,
}: BaseOrganizationSwitcherProps): ReactElement => {
const {theme, colorScheme, direction} = useTheme();
const styles: Record<string, string> = useStyles(theme, colorScheme);
const [isOpen, setIsOpen] = useState(false);
const [hoveredItemIndex, setHoveredItemIndex] = useState<number | null>(null);
const {t} = useTranslation();
const {t} = useTranslation(preferences?.i18n);
const isRTL: boolean = direction === 'rtl';

const {refs, floatingStyles, context} = useFloating({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* under the License.
*/

import {Preferences} from '@asgardeo/browser';
import {FC, ReactElement, useState} from 'react';

import {
Expand Down Expand Up @@ -87,6 +88,7 @@ export const OrganizationSwitcher: FC<OrganizationSwitcherProps> = ({
fallback = null,
onOrganizationSwitch: propOnOrganizationSwitch,
organizations: propOrganizations,
preferences,
...props
}: OrganizationSwitcherProps): ReactElement => {
const {isSignedIn} = useAsgardeo();
Expand All @@ -100,7 +102,7 @@ export const OrganizationSwitcher: FC<OrganizationSwitcherProps> = ({
const [isCreateOrgOpen, setIsCreateOrgOpen] = useState(false);
const [isProfileOpen, setIsProfileOpen] = useState(false);
const [isOrganizationListOpen, setIsOrganizationListOpen] = useState(false);
const {t} = useTranslation();
const {t} = useTranslation(preferences?.i18n);

if (!isSignedIn && fallback) {
return fallback;
Expand Down Expand Up @@ -155,6 +157,7 @@ export const OrganizationSwitcher: FC<OrganizationSwitcherProps> = ({
error={error}
menuItems={menuItems}
onManageProfile={handleManageOrganization}
preferences={preferences}
{...props}
/>
<CreateOrganization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* under the License.
*/

import {User, withVendorCSSClassPrefix, WellKnownSchemaIds, bem} from '@asgardeo/browser';
import {User, withVendorCSSClassPrefix, WellKnownSchemaIds, bem, Preferences} from '@asgardeo/browser';
import {cx} from '@emotion/css';
import {FC, ReactElement, useState, useCallback} from 'react';
import useStyles from './BaseUserProfile.styles';
Expand Down Expand Up @@ -82,6 +82,13 @@ export interface BaseUserProfileProps {
schemas?: Schema[];
showFields?: string[];
title?: string;

/**
* 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;
}

// Fields to skip based on schema.name
Expand Down Expand Up @@ -125,14 +132,15 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
open = false,
error = null,
isLoading = false,
preferences,
showFields = [],
hideFields = [],
displayNameAttributes = [],
}: BaseUserProfileProps): ReactElement => {
const {theme, colorScheme} = useTheme();
const [editedUser, setEditedUser] = useState(flattenedProfile || profile);
const [editingFields, setEditingFields] = useState<Record<string, boolean>>({});
const {t} = useTranslation();
const {t} = useTranslation(preferences?.i18n);

/**
* Determines if a field should be visible based on showFields, hideFields, and fieldsToSkip arrays.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* under the License.
*/

import {AsgardeoError, User} from '@asgardeo/browser';
import {AsgardeoError, User, Preferences} from '@asgardeo/browser';
import {FC, ReactElement, useState} from 'react';
// eslint-disable-next-line import/no-named-as-default
import BaseUserProfile, {BaseUserProfileProps} from './BaseUserProfile';
Expand Down Expand Up @@ -64,10 +64,10 @@ export type UserProfileProps = Omit<BaseUserProfileProps, 'user' | 'profile' | '
* />
* ```
*/
const UserProfile: FC<UserProfileProps> = ({...rest}: UserProfileProps): ReactElement => {
const UserProfile: FC<UserProfileProps> = ({preferences, ...rest}: UserProfileProps): ReactElement => {
const {baseUrl, instanceId} = useAsgardeo();
const {profile, flattenedProfile, schemas, onUpdateProfile} = useUser();
const {t} = useTranslation();
const {t} = useTranslation(preferences?.i18n);

const [error, setError] = useState<string | null>(null);

Expand Down Expand Up @@ -95,6 +95,7 @@ const UserProfile: FC<UserProfileProps> = ({...rest}: UserProfileProps): ReactEl
schemas={schemas}
onUpdate={handleProfileUpdate}
error={error}
preferences={preferences}
{...rest}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* under the License.
*/

import {FlowMetadataResponse} from '@asgardeo/browser';
import {FlowMetadataResponse, Preferences} from '@asgardeo/browser';
import {cx} from '@emotion/css';
import {FC, ReactElement, ReactNode, useCallback, useEffect, useRef, useState} from 'react';
import useStyles from './BaseAcceptInvite.styles';
Expand Down Expand Up @@ -227,6 +227,13 @@ export interface BaseAcceptInviteProps {
* Theme variant for the component.
*/
variant?: CardProps['variant'];

/**
* 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;
}

/**
Expand Down Expand Up @@ -254,13 +261,14 @@ const BaseAcceptInvite: FC<BaseAcceptInviteProps> = ({
onGoToSignIn,
className = '',
children,
preferences,
size = 'medium',
variant = 'outlined',
showTitle = true,
showSubtitle = true,
}: BaseAcceptInviteProps): ReactElement => {
const {meta} = useAsgardeo();
const {t} = useTranslation();
const {t} = useTranslation(preferences?.i18n);
const {theme} = useTheme();
const styles: any = useStyles(theme, theme.vars.colors.text.primary);
const [isLoading, setIsLoading] = useState(false);
Expand Down
Loading
Loading