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
58 changes: 58 additions & 0 deletions adapter/src/utils/customTranslations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useConfig, useDataEngine } 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 engine = useDataEngine()

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 engine.query(customTranslationsQuery, {
variables: { appUrlSlug, 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)`
)
}
},
[engine, 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
33 changes: 30 additions & 3 deletions adapter/src/utils/useLocale.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
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,
setDocumentDirection,
setMomentLocale,
} from './localeUtils.js'

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

const locale = parseLocale(userSettings)

// Asynchronous - check datastore for custom translations if enabled
const customTranslationsPromise = customTranslationsEnabled
? getCustomTranslations({
locale,
dhis2Locale: userSettings.keyUiLocale,
})
: Promise.resolve()

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

Expand All @@ -29,8 +44,15 @@ const useLocale = ({ userSettings, configDirection }) => {
setDocumentDirection({ localeDirection, configDirection })
document.documentElement.setAttribute('lang', locale.baseName)

setResult({ locale, direction: localeDirection })
}, [userSettings, configDirection])
customTranslationsPromise.then(() => {
setResult({ locale, direction: localeDirection })
})
}, [
userSettings,
configDirection,
getCustomTranslations,
customTranslationsEnabled,
])

return result
}
Expand All @@ -39,13 +61,18 @@ const settingsQuery = {
userSettings: {
resource: 'userSettings',
},
customTranslationsEnabled: {
// TODO: Use new setting
resource: 'systemSettings', // /keyCustomTranslationsEnabled
},
}
// note: userSettings.keyUiLocale is expected to be in the Java format,
// e.g. 'ar', 'ar_IQ', 'uz_UZ_Cyrl', etc.
export const useCurrentUserLocale = (configDirection) => {
const { loading, error, data } = useDataQuery(settingsQuery)
const { locale, direction } = useLocale({
userSettings: data && data.userSettings,
customTranslationsEnabled: data && data.customTranslationsEnabled,
configDirection,
})

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