Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions adapter/src/components/ServerVersionProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useOfflineInterface } from './OfflineInterfaceContext.js'

export const ServerVersionProvider = ({
appName,
appUrlSlug,
appVersion,
url, // url from env vars
apiVersion,
Expand Down Expand Up @@ -210,6 +211,7 @@ export const ServerVersionProvider = ({
<Provider
config={{
appName,
appUrlSlug,
appVersion: parseVersion(appVersion),
baseUrl,
apiVersion: apiVersion || realApiVersion,
Expand All @@ -230,6 +232,7 @@ export const ServerVersionProvider = ({

ServerVersionProvider.propTypes = {
appName: PropTypes.string.isRequired,
appUrlSlug: PropTypes.string.isRequired,
appVersion: PropTypes.string.isRequired,
apiVersion: PropTypes.number,
children: PropTypes.element,
Expand Down
4 changes: 4 additions & 0 deletions adapter/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ServerVersionProvider } from './components/ServerVersionProvider.js'

const AppAdapter = ({
appName,
appUrlSlug,
appVersion,
url,
apiVersion,
Expand Down Expand Up @@ -38,6 +39,7 @@ const AppAdapter = ({
>
<ServerVersionProvider
appName={appName}
appUrlSlug={appUrlSlug}
appVersion={appVersion}
url={url}
apiVersion={apiVersion}
Expand All @@ -62,6 +64,7 @@ const AppAdapter = ({
<PWALoadingBoundary>
<ServerVersionProvider
appName={appName}
appUrlSlug={appUrlSlug}
appVersion={appVersion}
url={url}
apiVersion={apiVersion}
Expand All @@ -87,6 +90,7 @@ const AppAdapter = ({

AppAdapter.propTypes = {
appName: PropTypes.string.isRequired,
appUrlSlug: PropTypes.string.isRequired,
appVersion: PropTypes.string.isRequired,
apiVersion: PropTypes.number,
children: PropTypes.element,
Expand Down
67 changes: 67 additions & 0 deletions adapter/src/utils/customTranslations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useConfig, useDataQuery } from '@dhis2/app-runtime'
import i18n from '@dhis2/d2-i18n'
import { useCallback } from 'react'
import { I18N_NAMESPACE } from './localeUtils'

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 })
console.log('adding datastore resources', {
basename: locale.baseName,
tx: data.customTranslations,
})
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
)
console.log({
rb: i18n.getResourceBundle(locale.baseName, I18N_NAMESPACE),
})
} 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
}
16 changes: 11 additions & 5 deletions adapter/src/utils/localeUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@ import i18n from '@dhis2/d2-i18n'
import moment from 'moment'

// Init i18n namespace
const I18N_NAMESPACE = 'default'
export const I18N_NAMESPACE = 'default'
i18n.setDefaultNamespace(I18N_NAMESPACE)

/**
* userSettings.keyUiLocale is expected to be formatted by Java's
* Locale.toString():
* Locale.toString()... kind of: <language>[_<REGION>[_<Script>]]
* https://github.com/dhis2/dhis2-core/pull/22819
* https://docs.oracle.com/javase/8/docs/api/java/util/Locale.html#toString--
* We can assume there are no Variants or Extensions to locales used by DHIS2
* @param {Intl.Locale} locale
*
* Note: if a BCP 47 language tag-formatted locale is provided for the `locale`
* argument, this function happens to work as well
*
* @param {string} locale
* @returns Intl.Locale
*/
const parseJavaLocale = (locale) => {
const parseDhis2Locale = (locale) => {
const [language, region, script] = locale.split('_')

let languageTag = language
Expand All @@ -38,7 +44,7 @@ export const parseLocale = (userSettings) => {
}
// legacy property
if (userSettings.keyUiLocale) {
return parseJavaLocale(userSettings.keyUiLocale)
return parseDhis2Locale(userSettings.keyUiLocale)
}
} catch (err) {
console.error('Unable to parse locale from user settings:', {
Expand Down
8 changes: 7 additions & 1 deletion adapter/src/utils/useLocale.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useDataQuery } from '@dhis2/app-runtime'
import i18n from '@dhis2/d2-i18n'
import { useState, useEffect, useMemo } from 'react'
import { useCustomTranslations } from './customTranslations.js'
import {
setI18nLocale,
parseLocale,
Expand All @@ -9,6 +10,7 @@ import {
} from './localeUtils.js'

const useLocale = ({ userSettings, configDirection }) => {
const getCustomTranslations = useCustomTranslations()
const [result, setResult] = useState({
locale: undefined,
direction: undefined,
Expand All @@ -21,6 +23,10 @@ const useLocale = ({ userSettings, configDirection }) => {

const locale = parseLocale(userSettings)

// Asynchronous
getCustomTranslations({ locale, dhis2Locale: userSettings.keyUiLocale })

// Synchronous -- will resolve before state is set and the child app is rendered
setI18nLocale(locale)
setMomentLocale(locale)

Expand All @@ -30,7 +36,7 @@ const useLocale = ({ userSettings, configDirection }) => {
document.documentElement.setAttribute('lang', locale.baseName)

setResult({ locale, direction: localeDirection })
}, [userSettings, configDirection])
}, [userSettings, configDirection, getCustomTranslations])

return result
}
Expand Down
5 changes: 5 additions & 0 deletions cli/src/lib/env/getEnv.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ const prefixEnvForCRA = (env) =>
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,
Expand Down
7 changes: 5 additions & 2 deletions cli/src/lib/i18n/templates/locales.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ import {{ lang }}Translations from './{{ lang }}/translations.json'
{{/each}}

const namespace = '{{ namespace }}'

// Use 'deep' = true and 'overwrite' = false to add to, but not overwrite,
// custom translations from the datastore (added by the app adapter)
{{#each langs as |lang key|}}

i18n.addResources('{{ lang }}', namespace, {{ lang }}Translations)
i18n.addResources('{{ lookup ../standardLanguageCodes lang }}', namespace, {{ lang }}Translations)
i18n.addResourceBundle('{{ lang }}', namespace, {{ lang }}Translations, true, false)
i18n.addResourceBundle('{{ lookup ../standardLanguageCodes lang }}', namespace, {{ lang }}Translations, true, false)
{{/each}}

export default i18n
1 change: 1 addition & 0 deletions shell/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading