Skip to content

Commit

Permalink
Add additional config flag to allow forcing RTL on LTR locales (expo#…
Browse files Browse the repository at this point in the history
…28129)

# Why and how

There's this issue that has been going on for some time 😅 
expo#26532

It's due to two things:
- first, the plugin on iOS still sets config keys even if there's no
extra config – this is a bug that this PR fixes.
- second, some apps are targeted for RTL markets and require an
additional option to always force RTL, and our locale detection messes
with that – I added a new field to the extra manifest field and config
plugin options.

New key is:

```
app.json > expo > extra > forcesRTL
```

# Test Plan

Performed the following tests:

| | prebuild app | expo go |

|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ios | if none set:<br><br>LTR/RTL depend on calling
I18nManager.(force/allow)RTL – built in react-native behavior<br><br>if
extra.forcesRTL true:<br><br>app RTL, I18nManager.isRTL true<br><br>if
extra.supportsRTL true:<br><br>LTR/RTL depend on locale | if none
set:<br><br>app LTR, I18nManager.isRTL false<br><br>if extra.forcesRTL
true:<br><br>app RTL, I18nManager.isRTL true<br><br>if extra.supportsRTL
true:<br><br>LTR/RTL depend on locale |
| android | if none set:<br><br>LTR/RTL depend on calling
I18nManager.(force/allow)RTL – built in react-native behavior<br><br>if
extra.forcesRTL true:<br><br>app RTL, I18nManager.isRTL true<br><br>if
extra.supportsRTL true:<br><br>LTR/RTL depend on locale | if none
set:<br><br>app LTR, I18nManager.isRTL false<br><br>if extra.forcesRTL
true:<br><br>app RTL, I18nManager.isRTL true<br><br>if extra.supportsRTL
true:<br><br>LTR/RTL depend on locale |

# Checklist

<!--
Please check the appropriate items below if they apply to your diff.
This is required for changes to Expo modules.
-->

- [ ] Documentation is up to date to reflect these changes (eg:
https://docs.expo.dev and README.md).
- [ ] Conforms with the [Documentation Writing Style
Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
- [ ] This diff will work correctly for `npx expo prebuild` & EAS Build
(eg: updated a module plugin).

---------

Co-authored-by: Expo Bot <[email protected]>
  • Loading branch information
aleqsio and expo-bot authored May 6, 2024
1 parent 9c8b6d4 commit 87efd0c
Show file tree
Hide file tree
Showing 13 changed files with 151 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ open class ExperienceActivity : BaseExperienceActivity(), StartReactInstanceDele
)
showOrReconfigureManagedAppSplashScreen(optimisticManifest)
setLoadingProgressStatusIfEnabled()
ExperienceRTLManager.setSupportsRTLFromManifest(this, optimisticManifest)
ExperienceRTLManager.setRTLPreferencesFromManifest(this, optimisticManifest)
}
}

Expand Down Expand Up @@ -499,7 +499,7 @@ open class ExperienceActivity : BaseExperienceActivity(), StartReactInstanceDele

BranchManager.handleLink(this, intentUri, detachSdkVersion)

ExperienceRTLManager.setSupportsRTLFromManifest(this, manifest)
ExperienceRTLManager.setRTLPreferencesFromManifest(this, manifest)

runOnUiThread {
if (!isInForeground) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ open class HomeActivity : BaseExperienceActivity() {
EventBus.getDefault().registerSticky(this)
kernel.startJSKernel(this)

ExperienceRTLManager.setSupportsRTL(this, false)
ExperienceRTLManager.setRTLPreferences(this, false, false)

SplashScreen.show(this, SplashScreenImageResizeMode.NATIVE, ReactRootView::class.java, true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,32 @@ import expo.modules.manifests.core.Manifest
// must be kept in sync with https://github.com/facebook/react-native/blob/main/ReactAndroid/src/main/java/com/facebook/react/modules/i18nmanager/I18nUtil.java
private const val SHARED_PREFS_NAME = "com.facebook.react.modules.i18nmanager.I18nUtil"
private const val KEY_FOR_PREFS_ALLOWRTL = "RCTI18nUtil_allowRTL"
private const val KEY_FOR_PREFS_FORCERTL = "RCTI18nUtil_forceRTL"

class ExperienceRTLManager {
companion object {
fun setSupportsRTL(context: Context, allowRTL: Boolean) {
fun setRTLPreferences(context: Context, allowRTL: Boolean, forceRTL: Boolean) {
// These keys are used by React Native here: https://github.com/facebook/react-native/blob/main/React/Modules/RCTI18nUtil.m
// We set them before React loads to ensure it gets rendered correctly the first time the app is opened.
context
.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.also {
it.putBoolean(KEY_FOR_PREFS_ALLOWRTL, allowRTL)
it.putBoolean(KEY_FOR_PREFS_FORCERTL, forceRTL)
it.apply()
}
}

fun setSupportsRTLFromManifest(context: Context, manifest: Manifest) {
setSupportsRTL(
context,
(manifest.getExpoClientConfigRootObject()?.optJSONObject("extra")?.optBoolean("supportsRTL") ?: false)
)
fun setRTLPreferencesFromManifest(context: Context, manifest: Manifest) {
// get supportsRTL from manifest and set it in shared preferences
val supportsRTL = manifest.getExpoClientConfigRootObject()?.optJSONObject("extra")?.optBoolean("supportsRTL") ?: false
val forcesRTL = manifest.getExpoClientConfigRootObject()?.optJSONObject("extra")?.optBoolean("forcesRTL") ?: false
if (forcesRTL) {
setRTLPreferences(context, true, true)
} else {
setRTLPreferences(context, supportsRTL, false)
}
}
}
}
15 changes: 12 additions & 3 deletions apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,19 @@ - (bool)_readSupportsRTLFromManifest:(EXManifestsManifest *)manifest
return manifest.supportsRTL;
}

- (bool)_readForcesRTLFromManifest:(EXManifestsManifest *)manifest
{
return manifest.forcesRTL;
}

- (void)appStateDidBecomeActive
{
if (_isHomeApp) {
[EXTextDirectionController setSupportsRTL:false];
[EXTextDirectionController setRTLPreferences:false :false];
} else if(_appRecord.appLoader.manifest != nil) {
[EXTextDirectionController setSupportsRTL:[self _readSupportsRTLFromManifest:_appRecord.appLoader.manifest]];
BOOL supportsRTL = [self _readSupportsRTLFromManifest:_appRecord.appLoader.manifest];
BOOL forceRTL = [self _readForcesRTLFromManifest:_appRecord.appLoader.manifest];
[EXTextDirectionController setRTLPreferences:supportsRTL :forceRTL];
}
dispatch_async(dispatch_get_main_queue(), ^{
// Reset the root view background color and window color if we switch between Expo home and project
Expand Down Expand Up @@ -465,7 +472,9 @@ - (void)appLoader:(EXAbstractLoader *)appLoader didFinishLoadingManifest:(EXMani
{
[self _showOrReconfigureManagedAppSplashScreen:manifest];
if (!_isHomeApp) {
[EXTextDirectionController setSupportsRTL:[self _readSupportsRTLFromManifest:_appRecord.appLoader.manifest]];
BOOL supportsRTL = [self _readSupportsRTLFromManifest:_appRecord.appLoader.manifest];
BOOL forceRTL = [self _readForcesRTLFromManifest:_appRecord.appLoader.manifest];
[EXTextDirectionController setRTLPreferences:supportsRTL :forceRTL];
}
[self _rebuildBridge];
if (self->_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ public class EXTextDirectionController: NSObject {
}

@objc
public class func setSupportsRTL(_ supportsRTL: Bool) {
public class func setRTLPreferences(_ supportsRTL: Bool, _ forceRTL: Bool) {
// These keys are used by React Native here: https://github.com/facebook/react-native/blob/main/React/Modules/RCTI18nUtil.m
// We set them before React loads to ensure it gets rendered correctly the first time the app is opened.
// On iOS we need to set both forceRTL and allowRTL so apps don't have to include localization strings.
UserDefaults.standard.set(supportsRTL, forKey: "RCTI18nUtil_allowRTL")
UserDefaults.standard.set(supportsRTL ? isRTLPreferredForCurrentLocale() : false, forKey: "RCTI18nUtil_forceRTL")
if forceRTL {
// Uses required reason API based on the following reason: CA92.1
UserDefaults.standard.set(true, forKey: "RCTI18nUtil_allowRTL")
UserDefaults.standard.set(true, forKey: "RCTI18nUtil_forceRTL")
} else {
UserDefaults.standard.set(supportsRTL, forKey: "RCTI18nUtil_allowRTL")
UserDefaults.standard.set(supportsRTL ? isRTLPreferredForCurrentLocale() : false, forKey: "RCTI18nUtil_forceRTL")
}

UserDefaults.standard.synchronize()
}
}
2 changes: 2 additions & 0 deletions packages/expo-localization/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### 🎉 New features

- Added a `forcesRTL` manifest flag for forcing RTL to be on. ([#28129](https://github.com/expo/expo/pull/28129) by [@aleqsio](https://github.com/aleqsio))

### 🐛 Bug fixes

### 💡 Others
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import java.util.*
// must be kept in sync with https://github.com/facebook/react-native/blob/main/ReactAndroid/src/main/java/com/facebook/react/modules/i18nmanager/I18nUtil.java
private const val SHARED_PREFS_NAME = "com.facebook.react.modules.i18nmanager.I18nUtil"
private const val KEY_FOR_PREFS_ALLOWRTL = "RCTI18nUtil_allowRTL"

private const val KEY_FOR_PREFS_FORCERTL = "RCTI18nUtil_forceRTL"
private const val LOCALE_SETTINGS_CHANGED = "onLocaleSettingsChanged"
private const val CALENDAR_SETTINGS_CHANGED = "onCalendarSettingsChanged"

Expand Down Expand Up @@ -70,14 +70,31 @@ class LocalizationModule : Module() {
// These keys are used by React Native here: https://github.com/facebook/react-native/blob/main/React/Modules/RCTI18nUtil.m
// We set them before React loads to ensure it gets rendered correctly the first time the app is opened.
val supportsRTL = appContext.reactContext?.getString(R.string.ExpoLocalization_supportsRTL)
if (supportsRTL != "true" && supportsRTL != "false") return
context
.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.also {
it.putBoolean(KEY_FOR_PREFS_ALLOWRTL, supportsRTL == "true")
it.apply()
val forcesRTL = appContext.reactContext?.getString(R.string.ExpoLocalization_forcesRTL)

if (forcesRTL == "true") {
context
.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.also {
it.putBoolean(KEY_FOR_PREFS_ALLOWRTL, true)
it.putBoolean(KEY_FOR_PREFS_FORCERTL, true)
it.apply()
}
} else {
if (supportsRTL == "true" || supportsRTL == "false") {
context
.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.also {
it.putBoolean(KEY_FOR_PREFS_ALLOWRTL, supportsRTL == "true")
if (forcesRTL == "false") {
it.putBoolean(KEY_FOR_PREFS_FORCERTL, false)
}
it.apply()
}
}
}
}

// TODO: Bacon: add set language
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="ExpoLocalization_supportsRTL" translatable="false">unset</string>
<string name="ExpoLocalization_forcesRTL" translatable="false">unset</string>
</resources>
21 changes: 16 additions & 5 deletions packages/expo-localization/ios/LocalizationModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ public class LocalizationModule: Module {
return Self.getCalendars()
}
OnCreate {
if let enableRTL = Bundle.main.object(forInfoDictionaryKey: "ExpoLocalization_supportsRTL") as? Bool {
self.setSupportsRTL(enableRTL)
if let forceRTL = Bundle.main.object(forInfoDictionaryKey: "ExpoLocalization_forcesRTL") as? Bool {
self.setRTLPreferences(true, forceRTL)
} else {
if let enableRTL = Bundle.main.object(forInfoDictionaryKey: "ExpoLocalization_supportsRTL") as? Bool {
self.setRTLPreferences(enableRTL, false)
}
}
}

Expand Down Expand Up @@ -53,13 +57,20 @@ public class LocalizationModule: Module {
return NSLocale.characterDirection(forLanguage: NSLocale.preferredLanguages.first ?? "en-US") == NSLocale.LanguageDirection.rightToLeft
}

func setSupportsRTL(_ supportsRTL: Bool) {
func setRTLPreferences(_ supportsRTL: Bool, _ forceRTL: Bool) {
// These keys are used by React Native here: https://github.com/facebook/react-native/blob/main/React/Modules/RCTI18nUtil.m
// We set them before React loads to ensure it gets rendered correctly the first time the app is opened.
// On iOS we need to set both forceRTL and allowRTL so apps don't have to include localization strings.
// Uses required reason API based on the following reason: CA92.1
UserDefaults.standard.set(supportsRTL, forKey: "RCTI18nUtil_allowRTL")
UserDefaults.standard.set(supportsRTL ? isRTLPreferredForCurrentLocale() : false, forKey: "RCTI18nUtil_forceRTL")

if forceRTL {
UserDefaults.standard.set(true, forKey: "RCTI18nUtil_allowRTL")
UserDefaults.standard.set(true, forKey: "RCTI18nUtil_forceRTL")
} else {
UserDefaults.standard.set(supportsRTL, forKey: "RCTI18nUtil_allowRTL")
UserDefaults.standard.set(supportsRTL ? isRTLPreferredForCurrentLocale() : false, forKey: "RCTI18nUtil_forceRTL")
}

UserDefaults.standard.synchronize()
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 26 additions & 9 deletions packages/expo-localization/plugin/build/withExpoLocalization.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 33 additions & 12 deletions packages/expo-localization/plugin/src/withExpoLocalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ import {

type ConfigPluginProps = {
supportsRTL?: boolean;
forcesRTL?: boolean;
allowDynamicLocaleChangesAndroid?: boolean;
};

function withExpoLocalizationIos(config: ExpoConfig) {
if (config.extra?.supportsRTL == null) return config;
function withExpoLocalizationIos(config: ExpoConfig, data: ConfigPluginProps) {
const mergedConfig = { ...config.extra, ...data };
if (mergedConfig?.supportsRTL == null && mergedConfig?.forcesRTL == null) return config;
if (!config.ios) config.ios = {};
if (!config.ios.infoPlist) config.ios.infoPlist = {};
config.ios.infoPlist.ExpoLocalization_supportsRTL = config.extra?.supportsRTL || false;
if (mergedConfig?.supportsRTL != null) {
config.ios.infoPlist.ExpoLocalization_supportsRTL = mergedConfig?.supportsRTL;
}
if (mergedConfig?.forcesRTL != null) {
config.ios.infoPlist.ExpoLocalization_forcesRTL = mergedConfig?.forcesRTL;
}
return config;
}

Expand All @@ -34,15 +41,29 @@ function withExpoLocalizationAndroid(config: ExpoConfig, data: ConfigPluginProps
});
}
return withStringsXml(config, (config) => {
config.modResults = AndroidConfig.Strings.setStringItem(
[
{
$: { name: 'ExpoLocalization_supportsRTL', translatable: 'false' },
_: String(data.supportsRTL ?? config.extra?.supportsRTL),
},
],
config.modResults
);
const mergedConfig = { ...config.extra, ...data };
if (mergedConfig?.supportsRTL != null) {
config.modResults = AndroidConfig.Strings.setStringItem(
[
{
$: { name: 'ExpoLocalization_supportsRTL', translatable: 'false' },
_: String(mergedConfig?.supportsRTL ?? 'unset'),
},
],
config.modResults
);
}
if (mergedConfig?.forcesRTL != null) {
config.modResults = AndroidConfig.Strings.setStringItem(
[
{
$: { name: 'ExpoLocalization_forcesRTL', translatable: 'false' },
_: String(mergedConfig?.forcesRTL ?? 'unset'),
},
],
config.modResults
);
}
return config;
});
}
Expand Down
10 changes: 10 additions & 0 deletions packages/expo-manifests/ios/EXManifests/Manifest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,16 @@ public class Manifest: NSObject {
return supportsRTL
}

public func forcesRTL() -> Bool {
guard let expoClientConfigRootObject = expoClientConfigRootObject(),
let extra: [String: Any]? = expoClientConfigRootObject.optionalValue(forKey: "extra"),
let forcesRTL: Bool = extra?.optionalValue(forKey: "forcesRTL") else {
return false
}

return forcesRTL
}

public func jsEngine() -> String {
let jsEngine = expoClientConfigRootObject().let { it in
Manifest.string(fromManifest: it, atPaths: [
Expand Down

0 comments on commit 87efd0c

Please sign in to comment.