Fix(UI) : Added browser language support (#22132)

* support browser language

* moved map to utils

* fixed sidebar not rendering and unit tests

* fixed localization rendering

---------

Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
Dhruv Parmar 2025-07-08 10:34:39 +05:30 committed by GitHub
parent e8bd7ea8a0
commit 85da793e6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 114 additions and 57 deletions

View File

@ -17,10 +17,12 @@ import { useCallback, useEffect } from 'react';
import { useLimitStore } from '../../context/LimitsProvider/useLimitsStore'; import { useLimitStore } from '../../context/LimitsProvider/useLimitsStore';
import { LineageSettings } from '../../generated/configuration/lineageSettings'; import { LineageSettings } from '../../generated/configuration/lineageSettings';
import { SettingType } from '../../generated/settings/settings'; import { SettingType } from '../../generated/settings/settings';
import { useCurrentUserPreferences } from '../../hooks/currentUserStore/useCurrentUserStore';
import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useApplicationStore } from '../../hooks/useApplicationStore';
import { getLimitConfig } from '../../rest/limitsAPI'; import { getLimitConfig } from '../../rest/limitsAPI';
import { getSettingsByType } from '../../rest/settingConfigAPI'; import { getSettingsByType } from '../../rest/settingConfigAPI';
import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase'; import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase';
import i18n from '../../utils/i18next/LocalUtil';
import { LimitBanner } from '../common/LimitBanner/LimitBanner'; import { LimitBanner } from '../common/LimitBanner/LimitBanner';
import LeftSidebar from '../MyData/LeftSidebar/LeftSidebar.component'; import LeftSidebar from '../MyData/LeftSidebar/LeftSidebar.component';
import NavBar from '../NavBar/NavBar'; import NavBar from '../NavBar/NavBar';
@ -32,6 +34,9 @@ const { Content } = Layout;
const AppContainer = () => { const AppContainer = () => {
const { currentUser, setAppPreferences, appPreferences } = const { currentUser, setAppPreferences, appPreferences } =
useApplicationStore(); useApplicationStore();
const {
preferences: { language },
} = useCurrentUserPreferences();
const AuthenticatedRouter = applicationRoutesClass.getRouteElements(); const AuthenticatedRouter = applicationRoutesClass.getRouteElements();
const ApplicationExtras = applicationsClassBase.getApplicationExtension(); const ApplicationExtras = applicationsClassBase.getApplicationExtension();
const { isAuthenticated } = useApplicationStore(); const { isAuthenticated } = useApplicationStore();
@ -61,6 +66,12 @@ const AppContainer = () => {
} }
}, [currentUser?.id]); }, [currentUser?.id]);
useEffect(() => {
if (language) {
i18n.changeLanguage(language);
}
}, [language]);
return ( return (
<Layout> <Layout>
<LimitBanner /> <LimitBanner />

View File

@ -27,7 +27,7 @@ const LeftSidebarItem = ({
to={{ to={{
pathname: redirect_url, pathname: redirect_url,
}}> }}>
{title} {t(title)}
{isBeta && ( {isBeta && (
<Badge <Badge
@ -42,7 +42,7 @@ const LeftSidebarItem = ({
<span <span
className="left-panel-item left-panel-label p-0" className="left-panel-item left-panel-label p-0"
data-testid={dataTestId}> data-testid={dataTestId}>
{title} {t(title)}
</span> </span>
); );
}; };

View File

@ -75,10 +75,8 @@ import {
getEntityType, getEntityType,
prepareFeedLink, prepareFeedLink,
} from '../../utils/FeedUtils'; } from '../../utils/FeedUtils';
import { import { languageSelectOptions } from '../../utils/i18next/i18nextUtil';
languageSelectOptions, import { SupportedLocales } from '../../utils/i18next/LocalUtil.interface';
SupportedLocales,
} from '../../utils/i18next/i18nextUtil';
import { isCommandKeyPress, Keys } from '../../utils/KeyboardUtil'; import { isCommandKeyPress, Keys } from '../../utils/KeyboardUtil';
import { getHelpDropdownItems } from '../../utils/NavbarUtils'; import { getHelpDropdownItems } from '../../utils/NavbarUtils';
import { getSettingPath } from '../../utils/RouterUtils'; import { getSettingPath } from '../../utils/RouterUtils';
@ -118,7 +116,7 @@ const NavBar = () => {
const [version, setVersion] = useState<string>(); const [version, setVersion] = useState<string>();
const [isDomainDropdownOpen, setIsDomainDropdownOpen] = useState(false); const [isDomainDropdownOpen, setIsDomainDropdownOpen] = useState(false);
const { const {
preferences: { isSidebarCollapsed }, preferences: { isSidebarCollapsed, language },
setPreference, setPreference,
} = useCurrentUserPreferences(); } = useCurrentUserPreferences();
@ -159,13 +157,6 @@ const NavBar = () => {
} }
}; };
const language = useMemo(
() =>
(cookieStorage.getItem('i18next') as SupportedLocales) ||
SupportedLocales.English,
[]
);
const { socket } = useWebSocketConnector(); const { socket } = useWebSocketConnector();
const handleTaskNotificationRead = () => { const handleTaskNotificationRead = () => {
@ -436,6 +427,7 @@ const NavBar = () => {
const handleLanguageChange = useCallback(({ key }: MenuInfo) => { const handleLanguageChange = useCallback(({ key }: MenuInfo) => {
i18next.changeLanguage(key); i18next.changeLanguage(key);
setPreference({ language: key as SupportedLocales });
navigate(0); navigate(0);
}, []); }, []);
@ -519,10 +511,7 @@ const NavBar = () => {
<Button <Button
className="flex-center gap-2 p-x-xs font-medium" className="flex-center gap-2 p-x-xs font-medium"
type="text"> type="text">
{upperCase( {upperCase(language.split('-')[0])} <DropDownIcon width={12} />
(language || SupportedLocales.English).split('-')[0]
)}{' '}
<DropDownIcon width={12} />
</Button> </Button>
</Dropdown> </Dropdown>
<Dropdown <Dropdown

View File

@ -20,7 +20,7 @@ import {
} from '../../../constants/regex.constants'; } from '../../../constants/regex.constants';
import { PipelineType } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { PipelineType } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { fetchMarkdownFile } from '../../../rest/miscAPI'; import { fetchMarkdownFile } from '../../../rest/miscAPI';
import { SupportedLocales } from '../../../utils/i18next/i18nextUtil'; import { SupportedLocales } from '../../../utils/i18next/LocalUtil.interface';
import { getActiveFieldNameForAppDocs } from '../../../utils/ServiceUtils'; import { getActiveFieldNameForAppDocs } from '../../../utils/ServiceUtils';
import Loader from '../Loader/Loader'; import Loader from '../Loader/Loader';
import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer'; import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer';

View File

@ -11,7 +11,6 @@
* limitations under the License. * limitations under the License.
*/ */
import i18next from 'i18next';
import { ReactComponent as GovernIcon } from '../assets/svg/bank.svg'; import { ReactComponent as GovernIcon } from '../assets/svg/bank.svg';
import { ReactComponent as ClassificationIcon } from '../assets/svg/classification.svg'; import { ReactComponent as ClassificationIcon } from '../assets/svg/classification.svg';
import { ReactComponent as ExploreIcon } from '../assets/svg/explore.svg'; import { ReactComponent as ExploreIcon } from '../assets/svg/explore.svg';
@ -39,48 +38,48 @@ export const SIDEBAR_NESTED_KEYS = {
export const SIDEBAR_LIST: Array<LeftSidebarItem> = [ export const SIDEBAR_LIST: Array<LeftSidebarItem> = [
{ {
key: ROUTES.MY_DATA, key: ROUTES.MY_DATA,
title: i18next.t('label.home'), title: 'label.home',
redirect_url: ROUTES.MY_DATA, redirect_url: ROUTES.MY_DATA,
icon: HomeIcon, icon: HomeIcon,
dataTestId: `app-bar-item-${SidebarItem.HOME}`, dataTestId: `app-bar-item-${SidebarItem.HOME}`,
}, },
{ {
key: ROUTES.EXPLORE, key: ROUTES.EXPLORE,
title: i18next.t('label.explore'), title: 'label.explore',
redirect_url: ROUTES.EXPLORE, redirect_url: ROUTES.EXPLORE,
icon: ExploreIcon, icon: ExploreIcon,
dataTestId: `app-bar-item-${SidebarItem.EXPLORE}`, dataTestId: `app-bar-item-${SidebarItem.EXPLORE}`,
}, },
{ {
key: ROUTES.PLATFORM_LINEAGE, key: ROUTES.PLATFORM_LINEAGE,
title: i18next.t('label.lineage'), title: 'label.lineage',
redirect_url: ROUTES.PLATFORM_LINEAGE, redirect_url: ROUTES.PLATFORM_LINEAGE,
icon: PlatformLineageIcon, icon: PlatformLineageIcon,
dataTestId: `app-bar-item-${SidebarItem.LINEAGE}`, dataTestId: `app-bar-item-${SidebarItem.LINEAGE}`,
}, },
{ {
key: ROUTES.OBSERVABILITY, key: ROUTES.OBSERVABILITY,
title: i18next.t('label.observability'), title: 'label.observability',
icon: ObservabilityIcon, icon: ObservabilityIcon,
dataTestId: SidebarItem.OBSERVABILITY, dataTestId: SidebarItem.OBSERVABILITY,
children: [ children: [
{ {
key: ROUTES.DATA_QUALITY, key: ROUTES.DATA_QUALITY,
title: i18next.t('label.data-quality'), title: 'label.data-quality',
redirect_url: ROUTES.DATA_QUALITY, redirect_url: ROUTES.DATA_QUALITY,
icon: DataQualityIcon, icon: DataQualityIcon,
dataTestId: `app-bar-item-${SidebarItem.DATA_QUALITY}`, dataTestId: `app-bar-item-${SidebarItem.DATA_QUALITY}`,
}, },
{ {
key: ROUTES.INCIDENT_MANAGER, key: ROUTES.INCIDENT_MANAGER,
title: i18next.t('label.incident-manager'), title: 'label.incident-manager',
redirect_url: ROUTES.INCIDENT_MANAGER, redirect_url: ROUTES.INCIDENT_MANAGER,
icon: IncidentMangerIcon, icon: IncidentMangerIcon,
dataTestId: `app-bar-item-${SidebarItem.INCIDENT_MANAGER}`, dataTestId: `app-bar-item-${SidebarItem.INCIDENT_MANAGER}`,
}, },
{ {
key: ROUTES.OBSERVABILITY_ALERTS, key: ROUTES.OBSERVABILITY_ALERTS,
title: i18next.t('label.alert-plural'), title: 'label.alert-plural',
redirect_url: ROUTES.OBSERVABILITY_ALERTS, redirect_url: ROUTES.OBSERVABILITY_ALERTS,
icon: AlertIcon, icon: AlertIcon,
dataTestId: `app-bar-item-${SidebarItem.OBSERVABILITY_ALERT}`, dataTestId: `app-bar-item-${SidebarItem.OBSERVABILITY_ALERT}`,
@ -89,7 +88,7 @@ export const SIDEBAR_LIST: Array<LeftSidebarItem> = [
}, },
{ {
key: ROUTES.DATA_INSIGHT, key: ROUTES.DATA_INSIGHT,
title: i18next.t('label.insight-plural'), title: 'label.insight-plural',
redirect_url: ROUTES.DATA_INSIGHT_WITH_TAB.replace( redirect_url: ROUTES.DATA_INSIGHT_WITH_TAB.replace(
PLACEHOLDER_ROUTE_TAB, PLACEHOLDER_ROUTE_TAB,
DataInsightTabs.DATA_ASSETS DataInsightTabs.DATA_ASSETS
@ -99,34 +98,34 @@ export const SIDEBAR_LIST: Array<LeftSidebarItem> = [
}, },
{ {
key: ROUTES.DOMAIN, key: ROUTES.DOMAIN,
title: i18next.t('label.domain-plural'), title: 'label.domain-plural',
redirect_url: ROUTES.DOMAIN, redirect_url: ROUTES.DOMAIN,
icon: DomainsIcon, icon: DomainsIcon,
dataTestId: `app-bar-item-${SidebarItem.DOMAIN}`, dataTestId: `app-bar-item-${SidebarItem.DOMAIN}`,
}, },
{ {
key: 'governance', key: 'governance',
title: i18next.t('label.govern'), title: 'label.govern',
icon: GovernIcon, icon: GovernIcon,
dataTestId: SidebarItem.GOVERNANCE, dataTestId: SidebarItem.GOVERNANCE,
children: [ children: [
{ {
key: ROUTES.GLOSSARY, key: ROUTES.GLOSSARY,
title: i18next.t('label.glossary'), title: 'label.glossary',
redirect_url: ROUTES.GLOSSARY, redirect_url: ROUTES.GLOSSARY,
icon: GlossaryIcon, icon: GlossaryIcon,
dataTestId: `app-bar-item-${SidebarItem.GLOSSARY}`, dataTestId: `app-bar-item-${SidebarItem.GLOSSARY}`,
}, },
{ {
key: ROUTES.TAGS, key: ROUTES.TAGS,
title: i18next.t('label.classification'), title: 'label.classification',
redirect_url: ROUTES.TAGS, redirect_url: ROUTES.TAGS,
icon: ClassificationIcon, icon: ClassificationIcon,
dataTestId: `app-bar-item-${SidebarItem.TAGS}`, dataTestId: `app-bar-item-${SidebarItem.TAGS}`,
}, },
{ {
key: ROUTES.METRICS, key: ROUTES.METRICS,
title: i18next.t('label.metric-plural'), title: 'label.metric-plural',
redirect_url: ROUTES.METRICS, redirect_url: ROUTES.METRICS,
icon: MetricIcon, icon: MetricIcon,
dataTestId: `app-bar-item-${SidebarItem.METRICS}`, dataTestId: `app-bar-item-${SidebarItem.METRICS}`,
@ -137,7 +136,7 @@ export const SIDEBAR_LIST: Array<LeftSidebarItem> = [
export const SETTING_ITEM = { export const SETTING_ITEM = {
key: ROUTES.SETTINGS, key: ROUTES.SETTINGS,
title: i18next.t('label.setting-plural'), title: 'label.setting-plural',
redirect_url: ROUTES.SETTINGS, redirect_url: ROUTES.SETTINGS,
icon: SettingsIcon, icon: SettingsIcon,
dataTestId: `app-bar-item-${SidebarItem.SETTINGS}`, dataTestId: `app-bar-item-${SidebarItem.SETTINGS}`,
@ -145,7 +144,7 @@ export const SETTING_ITEM = {
export const LOGOUT_ITEM = { export const LOGOUT_ITEM = {
key: SidebarItem.LOGOUT, key: SidebarItem.LOGOUT,
title: i18next.t('label.logout'), title: 'label.logout',
icon: LogoutIcon, icon: LogoutIcon,
dataTestId: `app-bar-item-${SidebarItem.LOGOUT}`, dataTestId: `app-bar-item-${SidebarItem.LOGOUT}`,
}; };

View File

@ -13,10 +13,13 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware'; import { createJSONStorage, persist } from 'zustand/middleware';
import { detectBrowserLanguage } from '../../utils/i18next/LocalUtil';
import { SupportedLocales } from '../../utils/i18next/LocalUtil.interface';
import { useApplicationStore } from '../useApplicationStore'; import { useApplicationStore } from '../useApplicationStore';
export interface UserPreferences { export interface UserPreferences {
isSidebarCollapsed: boolean; isSidebarCollapsed: boolean;
language: SupportedLocales;
selectedEntityTableColumns: Record<string, string[]>; selectedEntityTableColumns: Record<string, string[]>;
} }
@ -32,6 +35,7 @@ interface Store {
const defaultPreferences: UserPreferences = { const defaultPreferences: UserPreferences = {
isSidebarCollapsed: false, isSidebarCollapsed: false,
language: detectBrowserLanguage(),
selectedEntityTableColumns: {}, selectedEntityTableColumns: {},
// Add default values for other preferences // Add default values for other preferences
}; };

View File

@ -155,7 +155,7 @@ export const SettingsNavigationPage = ({
const titleRenderer = (node: TreeDataNode) => ( const titleRenderer = (node: TreeDataNode) => (
<div className="space-between"> <div className="space-between">
{node.title as string} {t(node.title as string)}
<Switch <Switch
checked={!hiddenKeys.includes(node.key as string)} checked={!hiddenKeys.includes(node.key as string)}
onChange={(checked) => handleRemoveToggle(checked, node.key as string)} onChange={(checked) => handleRemoveToggle(checked, node.key as string)}

View File

@ -95,6 +95,7 @@ jest.mock('utils/i18next/LocalUtil', () => ({
useTranslation: jest.fn().mockReturnValue({ useTranslation: jest.fn().mockReturnValue({
t: (key) => key, t: (key) => key,
}), }),
detectBrowserLanguage: jest.fn().mockReturnValue('en-US'),
t: (key) => key, t: (key) => key,
dir: jest.fn().mockReturnValue('ltr'), dir: jest.fn().mockReturnValue('ltr'),
})); }));

View File

@ -0,0 +1,32 @@
/*
* Copyright 2025 Collate.
* Licensed 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.
*/
// to remove circular dependency
export enum SupportedLocales {
English = 'en-US',
= 'ko-KR',
Français = 'fr-FR',
= 'zh-CN',
= 'ja-JP',
'Português (Brasil)' = 'pt-BR',
'Português (Portugal)' = 'pt-PT',
Español = 'es-ES',
Galego = 'gl-ES',
Русский = 'ru-RU',
Deutsch = 'de-DE',
Hebrew = 'he-HE',
Nederlands = 'nl-NL',
Persian = 'pr-PR',
Thai = 'th-TH',
= 'mr-IN',
Türkçe = 'tr-TR',
}

View File

@ -14,7 +14,27 @@
import i18n, { t as i18nextT } from 'i18next'; import i18n, { t as i18nextT } from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import { getInitOptions } from './i18nextUtil'; import { getInitOptions, languageMap } from './i18nextUtil';
import { SupportedLocales } from './LocalUtil.interface';
// Function to detect browser language
export const detectBrowserLanguage = (): SupportedLocales => {
const browserLang = navigator.language;
const browserLangs = navigator.languages || [browserLang];
let browserLanguage = undefined;
for (const lang of browserLangs) {
const langCode = lang.split('-')[0];
if (languageMap[langCode]) {
browserLanguage = languageMap[langCode];
return browserLanguage;
}
}
// English is the default language when we don't support browser language
return browserLanguage ?? SupportedLocales.English;
};
// Initialize i18next (language) // Initialize i18next (language)
i18n i18n

View File

@ -30,26 +30,7 @@ import ruRU from '../../locale/languages/ru-ru.json';
import thTH from '../../locale/languages/th-th.json'; import thTH from '../../locale/languages/th-th.json';
import trTR from '../../locale/languages/tr-tr.json'; import trTR from '../../locale/languages/tr-tr.json';
import zhCN from '../../locale/languages/zh-cn.json'; import zhCN from '../../locale/languages/zh-cn.json';
import { SupportedLocales } from './LocalUtil.interface';
export enum SupportedLocales {
English = 'en-US',
= 'ko-KR',
Français = 'fr-FR',
= 'zh-CN',
= 'ja-JP',
'Português (Brasil)' = 'pt-BR',
'Português (Portugal)' = 'pt-PT',
Español = 'es-ES',
Galego = 'gl-ES',
Русский = 'ru-RU',
Deutsch = 'de-DE',
Hebrew = 'he-HE',
Nederlands = 'nl-NL',
Persian = 'pr-PR',
Thai = 'th-TH',
= 'mr-IN',
Türkçe = 'tr-TR',
}
export const languageSelectOptions = map(SupportedLocales, (value, key) => ({ export const languageSelectOptions = map(SupportedLocales, (value, key) => ({
label: `${key} - ${upperCase(value.split('-')[0])}`, label: `${key} - ${upperCase(value.split('-')[0])}`,
@ -110,3 +91,23 @@ export const getCurrentLocaleForConstrue = () => {
return i18next.resolvedLanguage.split('-')[0]; return i18next.resolvedLanguage.split('-')[0];
}; };
// Map common language codes to supported locales
export const languageMap: Record<string, SupportedLocales> = {
mr: SupportedLocales.मर, // Marathi
en: SupportedLocales.English,
ko: SupportedLocales.한국어,
fr: SupportedLocales.Français,
zh: SupportedLocales.简体中文,
ja: SupportedLocales.日本語,
pt: SupportedLocales['Português (Brasil)'], // Default to Brazilian Portuguese
es: SupportedLocales.Español,
gl: SupportedLocales.Galego,
ru: SupportedLocales.Русский,
de: SupportedLocales.Deutsch,
he: SupportedLocales.Hebrew,
nl: SupportedLocales.Nederlands,
pr: SupportedLocales.Persian,
th: SupportedLocales.Thai,
tr: SupportedLocales.Türkçe,
};