-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add configurable dir
for language directions to app adapter [DHIS2-16480]
#825
Changes from 2 commits
dc43300
b309510
b05fa8f
9f2f8fa
19cb6d7
c5c06a3
7d25dd2
cbc0d1a
fa0e182
d2faf2a
805e3a6
3f6220a
d4bd3e4
f7df0f2
2e880e2
1d62c34
0c48e4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,16 +3,45 @@ import i18n from '@dhis2/d2-i18n' | |
import moment from 'moment' | ||
import { useState, useEffect } from 'react' | ||
|
||
i18n.setDefaultNamespace('default') | ||
const I18N_NAMESPACE = 'default' | ||
i18n.setDefaultNamespace(I18N_NAMESPACE) | ||
|
||
const simplifyLocale = (locale) => { | ||
const idx = locale.indexOf('-') | ||
if (idx === -1) { | ||
const transformJavaLocale = (locale) => { | ||
return locale.replace('_', '-') | ||
} | ||
|
||
// if translation resources aren't found for the given locale, try shorter | ||
// versions of the locale | ||
// e.g. 'pt_BR_Cyrl_asdf' => 'pt_BR', or 'ar-NotFound' => 'ar' | ||
const validateLocaleByBundle = (locale) => { | ||
if (i18n.hasResourceBundle(locale, I18N_NAMESPACE)) { | ||
return locale | ||
} | ||
|
||
console.log(`Translations for locale ${locale} not found`) | ||
|
||
// see if we can try basic versions of the locale | ||
// (e.g. 'ar' instead of 'ar_IQ') | ||
const match = /[_-]/.exec(locale) | ||
if (!match) { | ||
return locale | ||
} | ||
return locale.substr(0, idx) | ||
|
||
const separator = match[0] // '-' or '_' | ||
const splitLocale = locale.split(separator) | ||
for (let i = splitLocale.length - 1; i > 0; i--) { | ||
const shorterLocale = splitLocale.slice(0, i).join(separator) | ||
if (i18n.hasResourceBundle(shorterLocale, I18N_NAMESPACE)) { | ||
return shorterLocale | ||
} | ||
console.log(`Translations for locale ${shorterLocale} not found`) | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for this logic for getting the shorter version, instead of doing it manually by splitting .. couldn't we use the Locale methods, i.e.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm yeah that's a nice helper function -- if that works, I think that can help make that code look a lot more semantic 👍 Implementing it is a bit complex given the existing translation files we have to work with, for example Here's a screenshot from the Dashboard app's locales: |
||
// if nothing else works, use the initially provided locale | ||
return locale | ||
} | ||
|
||
// Set locale for Moment and i18n | ||
const setGlobalLocale = (locale) => { | ||
if (locale !== 'en' && locale !== 'en-us') { | ||
import( | ||
|
@@ -23,22 +52,41 @@ const setGlobalLocale = (locale) => { | |
} | ||
moment.locale(locale) | ||
|
||
const simplifiedLocale = simplifyLocale(locale) | ||
i18n.changeLanguage(simplifiedLocale) | ||
const resolvedLocale = validateLocaleByBundle(locale) | ||
kabaros marked this conversation as resolved.
Show resolved
Hide resolved
|
||
i18n.changeLanguage(resolvedLocale) | ||
|
||
console.log('🗺 Global d2-i18n locale initialized:', resolvedLocale) | ||
} | ||
|
||
// Sets the global direction based on the app's configured direction | ||
// (which should be done to affect modals, alerts, and other portal elements), | ||
// then returns the locale's direction for use on the header bar | ||
const handleDirection = ({ locale, configDirection }) => { | ||
kabaros marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// for i18n.dir, need JS-formatted locale | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also for all of this, maybe we can be more defensive and wrap it in a try/catch and fallback to 'ltr' - there are few external inputs here (the Jav locale) that might go wrong so it'd be good to make sure it doesn't crash. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's possible Was there something else here you think is fragile? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I had There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my recent changes, I approached error handling in a bit more specific way at each step with useful fall-backs -- my idea is that if one step doesn't work, the other steps can still succeed and provide useful localization. Not many things in here can actually throw, but those things are wrapped in try/catch |
||
const jsLocale = transformJavaLocale(locale) | ||
const localeDirection = i18n.dir(jsLocale) | ||
kabaros marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const globalDirection = | ||
configDirection === 'auto' ? localeDirection : configDirection | ||
// set `dir` globally (then override in app wrapper if needed) | ||
document.documentElement.setAttribute('dir', globalDirection) | ||
|
||
return localeDirection | ||
} | ||
|
||
export const useLocale = (locale) => { | ||
const [result, setResult] = useState(undefined) | ||
export const useLocale = ({ locale, configDirection }) => { | ||
const [result, setResult] = useState({}) | ||
|
||
useEffect(() => { | ||
if (!locale) { | ||
return | ||
} | ||
|
||
const direction = handleDirection({ locale, configDirection }) | ||
setGlobalLocale(locale) | ||
setResult(locale) | ||
setResult({ locale, direction }) | ||
}, [locale, configDirection]) | ||
|
||
console.log('🗺 Global d2-i18n locale initialized:', locale) | ||
}, [locale]) | ||
return result | ||
} | ||
|
||
|
@@ -47,16 +95,21 @@ const settingsQuery = { | |
resource: 'userSettings', | ||
}, | ||
} | ||
export const useCurrentUserLocale = () => { | ||
// 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 = useLocale( | ||
data && (data.userSettings.keyUiLocale || window.navigator.language) | ||
) | ||
const { locale, direction } = useLocale({ | ||
locale: | ||
data && | ||
(data.userSettings.keyUiLocale || window.navigator.language), | ||
configDirection, | ||
}) | ||
|
||
if (error) { | ||
// This shouldn't happen, trigger the fatal error boundary | ||
throw new Error('Failed to fetch user locale: ' + error) | ||
} | ||
|
||
return { loading: loading || !locale, locale } | ||
return { loading: loading || !locale, locale, direction } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it would be nice to add a test for this hook .. I know it's not particularly part of this PR since it didn't exist before, but there is quite a bit of logic here and having a test to validate all the permuations would be nice There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That may take me some time since I'm a bit out of practice with mocking things like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, we can delay it for later too .. but I didn't mean to test the whole thing, just the useLocale hook with something like react-hooks-testing-library There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah okay, just the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tests added |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to set the default to 'ltr' here in case no value was provided?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose that would be good, in case something goes wrong with the env var -- which makes me wonder, should we handle 'default to LTR' here only, and leave out the default env var? 🤔 It would tidy up the env vars slightly in the default case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I would say so .. it would keep the default env vars tidier