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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -155,7 +155,7 @@ export const SettingsNavigationPage = ({
const titleRenderer = (node: TreeDataNode) => (
<div className="space-between">
{node.title as string}
{t(node.title as string)}
<Switch
checked={!hiddenKeys.includes(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({
t: (key) => key,
}),
detectBrowserLanguage: jest.fn().mockReturnValue('en-US'),
t: (key) => key,
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 LanguageDetector from 'i18next-browser-languagedetector';
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)
i18n

View File

@ -30,26 +30,7 @@ import ruRU from '../../locale/languages/ru-ru.json';
import thTH from '../../locale/languages/th-th.json';
import trTR from '../../locale/languages/tr-tr.json';
import zhCN from '../../locale/languages/zh-cn.json';
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',
}
import { SupportedLocales } from './LocalUtil.interface';
export const languageSelectOptions = map(SupportedLocales, (value, key) => ({
label: `${key} - ${upperCase(value.split('-')[0])}`,
@ -110,3 +91,23 @@ export const getCurrentLocaleForConstrue = () => {
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,
};