diff --git a/apps/meteor/client/providers/MeteorProvider.tsx b/apps/meteor/client/providers/MeteorProvider.tsx index e4bd8c946e202a801a4af1ceb0f9aa9e4b19aa8f..e6896093a1d568da5e4f8da98417075ceedc2599 100644 --- a/apps/meteor/client/providers/MeteorProvider.tsx +++ b/apps/meteor/client/providers/MeteorProvider.tsx @@ -25,11 +25,11 @@ const MeteorProvider: FC = ({ children }) => ( <ConnectionStatusProvider> <ServerProvider> <RouterProvider> - <TranslationProvider> - <SessionProvider> - <TooltipProvider> - <ToastMessagesProvider> - <SettingsProvider> + <SettingsProvider> + <TranslationProvider> + <SessionProvider> + <TooltipProvider> + <ToastMessagesProvider> <LayoutProvider> <AvatarUrlProvider> <CustomSoundProvider> @@ -51,11 +51,11 @@ const MeteorProvider: FC = ({ children }) => ( </CustomSoundProvider> </AvatarUrlProvider> </LayoutProvider> - </SettingsProvider> - </ToastMessagesProvider> - </TooltipProvider> - </SessionProvider> - </TranslationProvider> + </ToastMessagesProvider> + </TooltipProvider> + </SessionProvider> + </TranslationProvider> + </SettingsProvider> </RouterProvider> </ServerProvider> </ConnectionStatusProvider> diff --git a/apps/meteor/client/providers/TranslationProvider.tsx b/apps/meteor/client/providers/TranslationProvider.tsx index 711779ae52cf8fc2c2a837770b7a13afe6fc8a47..8272f6acd7bbf84e091ae7a4dc1f0fe2f2adce0a 100644 --- a/apps/meteor/client/providers/TranslationProvider.tsx +++ b/apps/meteor/client/providers/TranslationProvider.tsx @@ -1,5 +1,6 @@ +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { TranslationContext, useAbsoluteUrl } from '@rocket.chat/ui-contexts'; +import { useSetting, TranslationContext, useAbsoluteUrl } from '@rocket.chat/ui-contexts'; import i18next from 'i18next'; import I18NextHttpBackend from 'i18next-http-backend'; import { TAPi18n, TAPi18next } from 'meteor/rocketchat:tap-i18n'; @@ -16,46 +17,105 @@ type TranslationNamespace = Extract<TranslationKey, `${string}.${string}`> exten : never : never; -const namespaces = ['onboarding', 'registration'] as TranslationNamespace[]; +const namespacesDefault = ['onboarding', 'registration'] as TranslationNamespace[]; + +const parseToJSON = (customTranslations: string) => { + try { + return JSON.parse(customTranslations); + } catch (e) { + return false; + } +}; const useI18next = (lng: string): typeof i18next => { const basePath = useAbsoluteUrl()('/i18n'); - const i18n = useState(() => { + const customTranslations = useSetting('Custom_Translations'); + + const parse = useMutableCallback((data: string, lngs?: string | string[], namespaces: string | string[] = []): { [key: string]: any } => { + const parsedCustomTranslations = typeof customTranslations === 'string' && parseToJSON(customTranslations); + + const source = JSON.parse(data); + const result: { [key: string]: any } = {}; + + for (const [key, value] of Object.entries(source)) { + const prefix = (Array.isArray(namespaces) ? namespaces : [namespaces]).find((namespace) => key.startsWith(`${namespace}.`)); + + if (prefix) { + result[key.slice(prefix.length + 1)] = value; + } + } + + if (lngs && parsedCustomTranslations) { + for (const language of Array.isArray(lngs) ? lngs : [lngs]) { + if (!parsedCustomTranslations[language]) { + continue; + } + + for (const [key, value] of Object.entries(parsedCustomTranslations[language])) { + const prefix = (Array.isArray(namespaces) ? namespaces : [namespaces]).find((namespace) => key.startsWith(`${namespace}.`)); + + if (prefix) { + result[key.slice(prefix.length + 1)] = value; + } + } + } + } + + return result; + }); + + const [i18n] = useState(() => { const i18n = i18next.createInstance().use(I18NextHttpBackend).use(initReactI18next); i18n.init({ lng, fallbackLng: 'en', - ns: namespaces, + ns: namespacesDefault, nsSeparator: '.', + partialBundledLanguages: true, debug: false, backend: { loadPath: `${basePath}/{{lng}}.json`, - parse: (data: string, _languages?: string | string[], namespaces: string | string[] = []): { [key: string]: any } => { - const source = JSON.parse(data); - const result: { [key: string]: any } = {}; - - for (const key of Object.keys(source)) { - const prefix = (Array.isArray(namespaces) ? namespaces : [namespaces]).find((namespace) => key.startsWith(`${namespace}.`)); - - if (prefix) { - result[key.slice(prefix.length + 1)] = source[key]; - } - } - - return result; - }, + parse, }, }); return i18n; - })[0]; + }); useEffect(() => { i18n.changeLanguage(lng); }, [i18n, lng]); + useEffect(() => { + if (!customTranslations || typeof customTranslations !== 'string') { + return; + } + + const parsedCustomTranslations: Record<string, Record<string, string>> = JSON.parse(customTranslations); + + for (const [ln, translations] of Object.entries(parsedCustomTranslations)) { + const namespaces = Object.entries(translations).reduce((acc, [key, value]): Record<string, Record<string, string>> => { + const namespace = key.split('.')[0]; + + if (namespacesDefault.includes(namespace as unknown as TranslationNamespace)) { + acc[namespace] = acc[namespace] ?? {}; + acc[namespace][key] = value; + acc[namespace][key.slice(namespace.length + 1)] = value; + return acc; + } + acc.project = acc.project ?? {}; + acc.project[key] = value; + return acc; + }, {} as Record<string, Record<string, string>>); + + for (const [namespace, translations] of Object.entries(namespaces)) { + i18n.addResourceBundle(ln, namespace, translations); + } + } + }, [customTranslations, i18n]); + return i18n; };