From 1d053f7ee95757847d080a55376694809c3b4562 Mon Sep 17 00:00:00 2001 From: Kai Vandivier Date: Mon, 26 Jan 2026 17:05:47 +0100 Subject: [PATCH 1/4] feat: add urlSlug (d2config.name) to app config --- adapter/src/components/ServerVersionProvider.js | 3 +++ adapter/src/index.js | 4 ++++ cli/src/lib/env/getEnv.js | 5 +++++ shell/src/App.jsx | 1 + 4 files changed, 13 insertions(+) diff --git a/adapter/src/components/ServerVersionProvider.js b/adapter/src/components/ServerVersionProvider.js index 5555594fc..439ba832f 100644 --- a/adapter/src/components/ServerVersionProvider.js +++ b/adapter/src/components/ServerVersionProvider.js @@ -10,6 +10,7 @@ import { useOfflineInterface } from './OfflineInterfaceContext.js' export const ServerVersionProvider = ({ appName, + appUrlSlug, appVersion, url, // url from env vars apiVersion, @@ -210,6 +211,7 @@ export const ServerVersionProvider = ({ const getShellEnv = (config) => { const shellEnv = { name: config.title, + // Added after 'name' key was already taken by the above (config.title): + url_slug: config.name, + // Currently an alias for 'name', but can be used to switch 'name' + // to config.name (it would be nice for these to match d2 config) + title: config.title, version: config.version, loginApp: config.type === 'login_app' ? 'true' : undefined, direction: config.direction, diff --git a/shell/src/App.jsx b/shell/src/App.jsx index dfdff99c4..aa24f28a8 100644 --- a/shell/src/App.jsx +++ b/shell/src/App.jsx @@ -42,6 +42,7 @@ const requiredPluginProps = parseRequiredProps( const appConfig = { url: getInjectedBaseUrl() || process.env.REACT_APP_DHIS2_BASE_URL, appName: process.env.REACT_APP_DHIS2_APP_NAME || '', + appUrlSlug: process.env.DHIS2_APP_URL_SLUG || '', appVersion: process.env.REACT_APP_DHIS2_APP_VERSION || '', apiVersion: parseInt(process.env.REACT_APP_DHIS2_API_VERSION), pwaEnabled: process.env.REACT_APP_DHIS2_APP_PWA_ENABLED === 'true', From 71a9bcca758a12e5bcddd3fa68d5aaefcc8f7dc8 Mon Sep 17 00:00:00 2001 From: Kai Vandivier Date: Mon, 26 Jan 2026 19:13:16 +0100 Subject: [PATCH 2/4] feat: check datastore for custom translations --- adapter/src/utils/localeUtils.js | 60 +++++++++++++++++++++++++- adapter/src/utils/useLocale.js | 8 +++- cli/src/lib/i18n/templates/locales.hbs | 7 ++- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/adapter/src/utils/localeUtils.js b/adapter/src/utils/localeUtils.js index 0fd5f4bb4..64db4253b 100644 --- a/adapter/src/utils/localeUtils.js +++ b/adapter/src/utils/localeUtils.js @@ -1,13 +1,71 @@ +import { useConfig, useDataQuery } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' import moment from 'moment' +import { useCallback } from 'react' // Init i18n namespace const I18N_NAMESPACE = 'default' i18n.setDefaultNamespace(I18N_NAMESPACE) +const customTranslationsQuery = { + customTranslations: { + resource: 'dataStore/custom-translations', + id: ({ appUrlSlug, dhis2Locale }) => `${appUrlSlug}--${dhis2Locale}`, + }, +} +/** + * Returns a function to look for custom translations for this app and locale + * in the datastore, using a key convention with the app name and user locale + * in the 'custom-translations' namespace. + * If the translations exist, they will be added to the translation bundle for + * the user's locale. This search will run asynchronously and is not awaited, + * but it will usually resolve before the app's main translation bundles are + * added, so steps are taken to make sure the custom translations take priority + * over (and don't get overwritten by) the main app translations + */ +export const useCustomTranslations = () => { + const { appUrlSlug } = useConfig() + const { refetch } = useDataQuery(customTranslationsQuery, { + lazy: true, + // dhis2locale should be sent as a variable at query time + variables: { appUrlSlug }, + }) + + const getCustomTranslations = useCallback( + /** + * Checks the datastore for custom translations and loads them if found + * @param {Object} params + * @param {Intl.Locale} params.locale - The parsed locale in BCP47 format + * @param {string} params.dhis2Locale - The locale in DHIS2 format + */ + async ({ locale, dhis2Locale }) => { + if (!dhis2Locale) { + return + } + try { + const data = await refetch({ dhis2Locale }) + i18n.addResourceBundle( + locale?.baseName ?? 'en', + I18N_NAMESPACE, + data.customTranslations, + true, // 'deep' -- add keys in this bundle to existing translations + true // 'overwrite' -- overwrite already existing keys + ) + } catch { + console.log( + `No custom translations found in the datastore for this app and locale (looked for the key ${appUrlSlug}--${dhis2Locale} in the custom-translations namespace)` + ) + } + }, + [refetch, appUrlSlug] + ) + + return getCustomTranslations +} + /** * userSettings.keyUiLocale is expected to be formatted by Java's - * Locale.toString(): + * Locale.toString()... kind of: [_][_