diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.tsx index 4d3209ec7cd..68f865f6b85 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppContainer/AppContainer.tsx @@ -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 ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebarItem.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebarItem.component.tsx index bb9a46c485a..981a48189b5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebarItem.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebarItem.component.tsx @@ -27,7 +27,7 @@ const LeftSidebarItem = ({ to={{ pathname: redirect_url, }}> - {title} + {t(title)} {isBeta && ( - {title} + {t(title)} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx b/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx index 419b224ebe4..647ee59c5bb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/NavBar/NavBar.tsx @@ -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(); 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 = () => { = [ { 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 = [ }, { 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 = [ }, { 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 = [ 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}`, }; diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.ts index e3b32aca3f4..613f4c6b480 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/currentUserStore/useCurrentUserStore.ts @@ -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; } @@ -32,6 +35,7 @@ interface Store { const defaultPreferences: UserPreferences = { isSidebarCollapsed: false, + language: detectBrowserLanguage(), selectedEntityTableColumns: {}, // Add default values for other preferences }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SettingsNavigationPage/SettingsNavigationPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SettingsNavigationPage/SettingsNavigationPage.tsx index 771fa1ab812..11135c1563f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SettingsNavigationPage/SettingsNavigationPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SettingsNavigationPage/SettingsNavigationPage.tsx @@ -155,7 +155,7 @@ export const SettingsNavigationPage = ({ const titleRenderer = (node: TreeDataNode) => (
- {node.title as string} + {t(node.title as string)} handleRemoveToggle(checked, node.key as string)} diff --git a/openmetadata-ui/src/main/resources/ui/src/setupTests.js b/openmetadata-ui/src/main/resources/ui/src/setupTests.js index 7994d1ee9a2..7bb75df957b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/setupTests.js +++ b/openmetadata-ui/src/main/resources/ui/src/setupTests.js @@ -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'), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.interface.ts b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.interface.ts new file mode 100644 index 00000000000..0a545e588d7 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.interface.ts @@ -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', +} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.ts b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.ts index 46309fcfa0b..dd1ec3cf72e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/LocalUtil.ts @@ -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 diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts index 54174343eb3..e91e4265e48 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/i18next/i18nextUtil.ts @@ -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 = { + 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, +};