Skip to content
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

Merged
merged 17 commits into from
Jan 25, 2024

Conversation

KaiVandivier
Copy link
Contributor

@KaiVandivier KaiVandivier commented Jan 17, 2024

DHIS2-16480

This extends the CLI, App Shell, and App Adapter to set up the dir HTML attribute to support RTL layouts.

The "global" dir at the top level of the document is set to the direction that's specified by the d2.config option so that it affects modals, alerts, and other portal elements, and then the user's locale direction is passed down to the header bar.

This also includes some logic to improve the handling of locales: currently, specific locales like "Arabic (Egypt)" (ar_EG) which are available from the back end aren't well supported -- for one thing, we should extend our efforts in Transifex to provide translations for those. In the App Adapter, however, we can check to see if translations exist for that specific locale, and if not, try to fall back to a more general locale like ar. That behavior can be seen in the console of the RTL screenshots below.

It also includes fixes for Moment, which doesn't accept locales in the format used before. I also added some fallback logic there to use a similar locale when possible

ar_EG with direction: 'auto' (notice console and dir attributes in the DOM)
Screenshot 2024-01-17 at 5 57 06 PM

ar_EG with default direction (ltr)
Screenshot 2024-01-17 at 5 55 53 PM

en
Screenshot 2024-01-17 at 5 58 48 PM

@KaiVandivier KaiVandivier requested a review from kabaros January 17, 2024 17:15
@@ -12,6 +12,7 @@ const AppAdapter = ({
appVersion,
url,
apiVersion,
direction,
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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

Comment on lines 21 to 39
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`)
}

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

const locale = new Intl.Locale('ja-Jpan-JP');

console.log(locale.language, locale.region, locale.script);
// will give us language = ja, region = JP, script = Jpan
// and we can try to get the locale for language or `${language}-${region}``

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 uz_UZ_Cyrl isn't in the order that Intl.Locale wants, but I'll see what I can do (when using hyphens, new Intl.Locale('uz-UZ-Cyrl') throws an error)

Here's a screenshot from the Dashboard app's locales:

Screenshot 2024-01-19 at 1 42 58 PM

adapter/src/utils/useLocale.js Outdated Show resolved Hide resolved
// (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 }) => {
// for i18n.dir, need JS-formatted locale
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible i18n.dir is already quite robust in that way -- you can pass it any string (or an array even, lol), and it will default to 'ltr'

Was there something else here you think is fragile?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I had locale being undefined in mind .. I thought it might be on first-render or something, and it's better to be safe. But maybe a try/catch around the whole useLocale effect is even safer (the stuff with splitting locale strings could throw as well in a couple of places)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

adapter/src/utils/useLocale.js Outdated Show resolved Hide resolved
adapter/src/utils/useLocale.js Outdated Show resolved Hide resolved

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 }
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 useDataQuery and i18n.dir, but I'll see what I can do 👍

Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, just the useLocale hook will be quite a bit simpler -- this comment was placed in a different hook though 😁

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests added

Comment on lines +35 to +38
// proposed property
if (userSettings.keyUiLanguageTag) {
return new Intl.Locale(userSettings.keyUiLanguageTag)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the new property Austin and I proposed, as a proof of concept of forward and backward compatibilty

* function tries permutations of the locale to find one that's supported.
* NB: None of them use both a region AND a script.
* @param {Intl.Locale} locale
*/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New fixes in here to Moment locale loading since the last review, and also split to its own function

* try less-specific versions.
* Both "Java Locale.toString()" and BCP 47 language tag formats are tested
* @param {Intl.Locale} locale
*/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworked the below take advantage of Intl.Locale semantics

@KaiVandivier KaiVandivier changed the base branch from master to alpha January 25, 2024 16:45
@KaiVandivier KaiVandivier merged commit e605143 into alpha Jan 25, 2024
8 checks passed
@KaiVandivier KaiVandivier deleted the dhis2-16480-lang-direction-config branch January 25, 2024 16:52
dhis2-bot added a commit that referenced this pull request Jan 25, 2024
# [10.5.0-alpha.1](v10.4.0...v10.5.0-alpha.1) (2024-01-25)

### Features

* `dir` config for language directions and other i18n fixes [DHIS2-16480] ([#825](#825)) ([e605143](e605143))
@dhis2-bot
Copy link
Contributor

🎉 This PR is included in version 10.5.0-alpha.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

kabaros pushed a commit that referenced this pull request Feb 16, 2024
…2-16480] (#825)

* feat: change text direction based on locale

* feat: add direction config to d2.config

* refactor: split up handleLocale

* refactor: rename function; add dir default to adapter

* fix: remove direction from d2.config defaults in CLI

* refactor: parse locale to Intl.Locale object

* refactor: better i18nLocale logic

* fix: moment locale formatting & add fallbacks

* refactor: fn rename

* refactor: move locale utils to a new file

* test: set up test file for useLocale.test

* fix: skip logic for en and en-US

* test: add first useLocale tests

* test: userSettings cases

* test: add document direction tests

* fix: handle nonstandard configDirections

* feat: set document `lang` attribute
kabaros added a commit that referenced this pull request Feb 19, 2024
* feat: `dir` config for language directions and other i18n fixes [DHIS2-16480] (#825)

* feat: change text direction based on locale

* feat: add direction config to d2.config

* refactor: split up handleLocale

* refactor: rename function; add dir default to adapter

* fix: remove direction from d2.config defaults in CLI

* refactor: parse locale to Intl.Locale object

* refactor: better i18nLocale logic

* fix: moment locale formatting & add fallbacks

* refactor: fn rename

* refactor: move locale utils to a new file

* test: set up test file for useLocale.test

* fix: skip logic for en and en-US

* test: add first useLocale tests

* test: userSettings cases

* test: add document direction tests

* fix: handle nonstandard configDirections

* feat: set document `lang` attribute

* fix: remove unused export for useLocale (and update tests) (#826)

* test: update tests to target useCurrentUserLocale

* fix: remove unused export on useLocale

---------

Co-authored-by: Kai Vandivier <[email protected]>
dhis2-bot added a commit that referenced this pull request Feb 19, 2024
# [10.5.0](v10.4.1...v10.5.0) (2024-02-19)

### Features

* changes to support right-to-left ([#830](#830)) ([6d5433c](6d5433c)), closes [#825](#825) [#826](#826)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants