From 6f696238a02863a92d2fc12c708eaffba68b38d0 Mon Sep 17 00:00:00 2001 From: Ildar Kamalov Date: Mon, 11 Aug 2025 18:38:28 +0300 Subject: [PATCH] fix language change --- client_v2/src/__locales/ru.json | 2 +- client_v2/src/actions/index.tsx | 14 +++-- client_v2/src/api/Api.ts | 10 ++-- client_v2/src/common/intl/index.ts | 61 ++++++++++++++++++--- client_v2/src/common/ui/Footer/Footer.tsx | 3 +- client_v2/src/helpers/localStorageHelper.ts | 1 + client_v2/src/reducers/dashboard.ts | 5 ++ 7 files changed, 75 insertions(+), 21 deletions(-) diff --git a/client_v2/src/__locales/ru.json b/client_v2/src/__locales/ru.json index 94bd0d88..d24f567f 100644 --- a/client_v2/src/__locales/ru.json +++ b/client_v2/src/__locales/ru.json @@ -1,3 +1,3 @@ { - "client_settings": "Настройки клиентов" + "privacy_policy": "Политика конфиденциальности" } diff --git a/client_v2/src/actions/index.tsx b/client_v2/src/actions/index.tsx index a4049cc5..e2970671 100644 --- a/client_v2/src/actions/index.tsx +++ b/client_v2/src/actions/index.tsx @@ -7,7 +7,7 @@ import escapeRegExp from 'lodash/escapeRegExp'; import React from 'react'; import { compose } from 'redux'; import type { Dispatch } from 'redux'; -import intl from 'panel/common/intl'; +import intl, { type LocalesType } from 'panel/common/intl'; import { splitByNewLine, sortClients, @@ -23,6 +23,7 @@ import { SETTINGS_NAMES, MANUAL_UPDATE_LINK, DISABLE_PROTECTION_TIMINGS, + THEMES, } from '../helpers/constants'; import { areEqualVersions } from '../helpers/version'; import { getTlsStatus } from './encryption'; @@ -32,6 +33,7 @@ import { getFilteringStatus, setRules } from './filtering'; type SafeSearchConfig = Record & { enabled: boolean }; type ToggleSettingKey = keyof typeof SETTINGS_NAMES; +type Theme = typeof THEMES[keyof typeof THEMES]; export const toggleSettingStatus = createAction<{ settingKey: ToggleSettingKey; @@ -441,13 +443,13 @@ export const testUpstreamWithFormValues = (formValues: any) => async (dispatch: export const changeLanguageRequest = createAction('CHANGE_LANGUAGE_REQUEST'); export const changeLanguageFailure = createAction('CHANGE_LANGUAGE_FAILURE'); -export const changeLanguageSuccess = createAction('CHANGE_LANGUAGE_SUCCESS'); +export const changeLanguageSuccess = createAction<{ language: LocalesType }>('CHANGE_LANGUAGE_SUCCESS'); -export const changeLanguage = (lang: any) => async (dispatch: any) => { +export const changeLanguage = (lang: LocalesType) => async (dispatch: Dispatch) => { dispatch(changeLanguageRequest()); try { await apiClient.changeLanguage({ language: lang }); - dispatch(changeLanguageSuccess()); + dispatch(changeLanguageSuccess({ language: lang })); } catch (error) { dispatch(addErrorToast({ error })); dispatch(changeLanguageFailure()); @@ -456,9 +458,9 @@ export const changeLanguage = (lang: any) => async (dispatch: any) => { export const changeThemeRequest = createAction('CHANGE_THEME_REQUEST'); export const changeThemeFailure = createAction('CHANGE_THEME_FAILURE'); -export const changeThemeSuccess = createAction('CHANGE_THEME_SUCCESS'); +export const changeThemeSuccess = createAction<{ theme: Theme }>('CHANGE_THEME_SUCCESS'); -export const changeTheme = (theme: any) => async (dispatch: any) => { +export const changeTheme = (theme: Theme) => async (dispatch: Dispatch) => { dispatch(changeThemeRequest()); try { await apiClient.changeTheme({ theme }); diff --git a/client_v2/src/api/Api.ts b/client_v2/src/api/Api.ts index 89acf6a5..549f9bb8 100644 --- a/client_v2/src/api/Api.ts +++ b/client_v2/src/api/Api.ts @@ -1,5 +1,7 @@ import axios from 'axios'; +import type { LocalesType } from 'panel/common/intl'; + import { BASE_URL } from '../../constants'; import { getPathWithQueryString } from '../helpers/helpers'; @@ -7,6 +9,8 @@ import { QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART, THEMES } from '../ import i18n from '../i18n'; import { LANGUAGES } from '../helpers/twosky'; +type Theme = typeof THEMES[keyof typeof THEMES]; + class Api { baseUrl: string; @@ -258,8 +262,7 @@ class Api { // } // Language - - async changeLanguage(config: any) { + async changeLanguage(config: { language: LocalesType }) { const profile = await this.getProfile(); profile.language = config.language; @@ -267,8 +270,7 @@ class Api { } // Theme - - async changeTheme(config: any) { + async changeTheme(config: { theme: Theme }) { const profile = await this.getProfile(); profile.theme = config.theme; diff --git a/client_v2/src/common/intl/index.ts b/client_v2/src/common/intl/index.ts index f12149ef..7c396cf0 100644 --- a/client_v2/src/common/intl/index.ts +++ b/client_v2/src/common/intl/index.ts @@ -15,6 +15,7 @@ import ptPt from 'panel/__locales/pt-pt.json'; import ru from 'panel/__locales/ru.json'; import zhCn from 'panel/__locales/zh-cn.json'; import zhTw from 'panel/__locales/zh-tw.json'; +import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper'; export type LocalesType = ReturnType; @@ -37,14 +38,55 @@ const LOCALES = { const messages: LocalesTypes = LOCALES; -export const i18n = (lang: LocalesType) => ({ - getMessage: (key: string) => messages[lang]?.[key] || '', - getUILanguage: () => (lang.includes('zh') ? 'zh' : lang), - getBaseMessage: (key: string) => messages.en![key] || key, - getBaseUILanguage: () => BASE_LOCALE as LocalesType, -}); +const resolveLanguage = (lng: string): LocalesType => { + const l = lng.toLowerCase(); -let currentLanguage: LocalesType = BASE_LOCALE as LocalesType; + if (messages[l as LocalesType]) { + return l as LocalesType; + } + + // Chinese locales + if (l.startsWith('zh')) { + if (l.includes('tw') || l.includes('hk') || l.includes('mo')) { + return 'zh-tw' as LocalesType; + } + return 'zh-cn' as LocalesType; + } + + // Portuguese locales + if (l.startsWith('pt')) { + if (l.includes('br')) { + return 'pt-br' as LocalesType; + } + return 'pt-pt' as LocalesType; + } + + // Try base language (e.g., en-us -> en) + const base = l.split('-')[0] as LocalesType; + if (messages[base]) { + return base; + } + + return BASE_LOCALE as LocalesType; +}; + +export const i18n = (lang: LocalesType) => { + const resolved = resolveLanguage(lang); + return { + getMessage: (key: string) => messages[resolved]?.[key] || '', + getUILanguage: () => resolved, + getBaseMessage: (key: string) => messages.en![key] || key, + getBaseUILanguage: () => BASE_LOCALE as LocalesType, + }; +}; + +const detectedLanguage = ( + (typeof window !== 'undefined' && typeof localStorage !== 'undefined' && LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.LANGUAGE)) || + (typeof navigator !== 'undefined' && (navigator.language as string)) || + BASE_LOCALE +) as LocalesType; + +let currentLanguage: LocalesType = resolveLanguage(detectedLanguage); let translator = translate.createReactTranslator(i18n(currentLanguage), React, { tags: [ @@ -77,8 +119,9 @@ const intl = { getBaseUILanguage: () => BASE_LOCALE as LocalesType, changeLanguage: (newLang: LocalesType) => { - currentLanguage = newLang; - translator = translate.createReactTranslator(i18n(newLang), React, { + const resolved = resolveLanguage(newLang); + currentLanguage = resolved; + translator = translate.createReactTranslator(i18n(resolved), React, { tags: [ { key: 'br', diff --git a/client_v2/src/common/ui/Footer/Footer.tsx b/client_v2/src/common/ui/Footer/Footer.tsx index fefb548c..87ba62c8 100644 --- a/client_v2/src/common/ui/Footer/Footer.tsx +++ b/client_v2/src/common/ui/Footer/Footer.tsx @@ -7,6 +7,7 @@ import { Dropdown } from 'panel/common/ui/Dropdown'; import { Icon } from 'panel/common/ui/Icon'; import intl, { LocalesType } from 'panel/common/intl'; +import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper'; import { REPOSITORY, PRIVACY_POLICY_LINK, THEMES } from '../../../helpers/constants'; import { LANGUAGES } from '../../../helpers/twosky'; import { setHtmlLangAttr, setUITheme } from '../../../helpers/helpers'; @@ -73,11 +74,11 @@ export const Footer = () => { }; const changeLanguage = async (newLang: LocalesType) => { - intl.changeLanguage(newLang); setHtmlLangAttr(newLang); try { await dispatch(changeLanguageAction(newLang)); + LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.LANGUAGE, newLang); window.location.reload(); } catch (error) { console.error('Failed to save language preference:', error); diff --git a/client_v2/src/helpers/localStorageHelper.ts b/client_v2/src/helpers/localStorageHelper.ts index c4860390..0176ba92 100644 --- a/client_v2/src/helpers/localStorageHelper.ts +++ b/client_v2/src/helpers/localStorageHelper.ts @@ -5,6 +5,7 @@ export const LOCAL_STORAGE_KEYS = { CLIENTS_PAGE_SIZE: 'clients_page_size', REWRITES_PAGE_SIZE: 'rewrites_page_size', AUTO_CLIENTS_PAGE_SIZE: 'auto_clients_page_size', + LANGUAGE: 'language', }; export const LocalStorageHelper = { diff --git a/client_v2/src/reducers/dashboard.ts b/client_v2/src/reducers/dashboard.ts index 91f2b262..072f0f68 100644 --- a/client_v2/src/reducers/dashboard.ts +++ b/client_v2/src/reducers/dashboard.ts @@ -163,6 +163,10 @@ const dashboard = handleActions( theme: payload.theme, processingProfile: false, }), + [actions.changeLanguageSuccess.toString()]: (state, { payload }: any) => ({ + ...state, + language: payload.language, + }), [actions.changeThemeSuccess.toString()]: (state, { payload }: any) => ({ ...state, theme: payload.theme, @@ -188,6 +192,7 @@ const dashboard = handleActions( supportedTags: [], name: '', theme: undefined, + language: undefined, checkUpdateFlag: false, }, );