From a7944cce80d60889ae49fec2acfe99e6a85dab1a Mon Sep 17 00:00:00 2001 From: rvveber Date: Tue, 25 Feb 2025 15:41:03 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(app)=20get=20language=20from=20backen?= =?UTF-8?q?d;=20set=20browser-detected=20language=20if=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - adds useLanguageSynchronizer hook to update the: 1. frontend-language to the user-preference - if there is one. 2. user-preference to the (browser-detected) frontend-language - otherwise. --- CHANGELOG.md | 1 + .../apps/impress/src/core/AppProvider.tsx | 1 - .../src/core/config/ConfigProvider.tsx | 6 ++ .../language/hooks/useLanguageSynchronizer.ts | 82 +++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/frontend/apps/impress/src/features/language/hooks/useLanguageSynchronizer.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 84c25d3f..3920bba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to - 🔒️ Manage unsafe attachments #663 - ✨(frontend) Custom block quote with export #646 - ✨(frontend) add open source section homepage #666 +- ✨(frontend) synchronize language-choice #401 ## Changed diff --git a/src/frontend/apps/impress/src/core/AppProvider.tsx b/src/frontend/apps/impress/src/core/AppProvider.tsx index 52c227e7..6727c48c 100644 --- a/src/frontend/apps/impress/src/core/AppProvider.tsx +++ b/src/frontend/apps/impress/src/core/AppProvider.tsx @@ -4,7 +4,6 @@ import { useEffect } from 'react'; import { useCunninghamTheme } from '@/cunningham'; import { Auth } from '@/features/auth'; -import '@/i18n/initI18n'; import { useResponsiveStore } from '@/stores/'; import { ConfigProvider } from './config/'; diff --git a/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx b/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx index 39a6938e..e9bf019b 100644 --- a/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx +++ b/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx @@ -3,6 +3,7 @@ import { PropsWithChildren, useEffect } from 'react'; import { Box } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; +import { useLanguageSynchronizer } from '@/features/language/hooks/useLanguageSynchronizer'; import { CrispProvider, PostHogProvider } from '@/services'; import { useSentryStore } from '@/stores/useSentryStore'; @@ -12,6 +13,7 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => { const { data: conf } = useConfig(); const { setSentry } = useSentryStore(); const { setTheme } = useCunninghamTheme(); + const { synchronizeLanguage } = useLanguageSynchronizer(); useEffect(() => { if (!conf?.SENTRY_DSN) { @@ -29,6 +31,10 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => { setTheme(conf.FRONTEND_THEME); }, [conf?.FRONTEND_THEME, setTheme]); + useEffect(() => { + void synchronizeLanguage(); + }, [synchronizeLanguage]); + if (!conf) { return ( diff --git a/src/frontend/apps/impress/src/features/language/hooks/useLanguageSynchronizer.ts b/src/frontend/apps/impress/src/features/language/hooks/useLanguageSynchronizer.ts new file mode 100644 index 00000000..536e2e72 --- /dev/null +++ b/src/frontend/apps/impress/src/features/language/hooks/useLanguageSynchronizer.ts @@ -0,0 +1,82 @@ +import { useCallback, useMemo, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useConfig } from '@/core'; +import { useAuthQuery } from '@/features/auth/api'; +import { useChangeUserLanguage } from '@/features/language/api/useChangeUserLanguage'; +import { getMatchingLocales } from '@/features/language/utils/locale'; +import { availableFrontendLanguages } from '@/i18n/initI18n'; + +export const useLanguageSynchronizer = () => { + const { data: conf, isSuccess: confInitialized } = useConfig(); + const { data: user, isSuccess: userInitialized } = useAuthQuery(); + const { i18n } = useTranslation(); + const { mutateAsync: changeUserLanguage } = useChangeUserLanguage(); + const languageSynchronizing = useRef(false); + + const availableBackendLanguages = useMemo(() => { + return conf?.LANGUAGES.map(([locale]) => locale); + }, [conf]); + + const synchronizeLanguage = useCallback( + async (direction?: 'toBackend' | 'toFrontend') => { + if ( + languageSynchronizing.current || + !userInitialized || + !confInitialized || + !availableBackendLanguages || + !availableFrontendLanguages + ) { + return; + } + languageSynchronizing.current = true; + + try { + const userPreferredLanguages = user.language ? [user.language] : []; + const setOrDetectedLanguages = i18n.languages; + + // Default direction depends on whether a user already has a language preference + direction = + direction ?? + (userPreferredLanguages.length ? 'toFrontend' : 'toBackend'); + + if (direction === 'toBackend') { + // Update user's preference from frontends's language + const closestBackendLanguage = + getMatchingLocales( + availableBackendLanguages, + setOrDetectedLanguages, + )[0] || availableBackendLanguages[0]; + await changeUserLanguage({ + userId: user.id, + language: closestBackendLanguage, + }); + } else { + // Update frontends's language from user's preference + const closestFrontendLanguage = + getMatchingLocales( + availableFrontendLanguages, + userPreferredLanguages, + )[0] || availableFrontendLanguages[0]; + if (i18n.resolvedLanguage !== closestFrontendLanguage) { + await i18n.changeLanguage(closestFrontendLanguage); + } + } + } catch (error) { + console.error('Error synchronizing language', error); + } finally { + languageSynchronizing.current = false; + } + }, + [ + i18n, + user, + userInitialized, + confInitialized, + availableBackendLanguages, + changeUserLanguage, + ], + ); + + return { synchronizeLanguage }; +};