Closed sedubois closed 1 year ago
One way this could be done is by building many config files and endpoints to handle your various languages. In Jumpstart Pro Android we have this method in the MainSessionNavHostFragment.kt
:
override val pathConfigurationLocation: TurboPathConfiguration.Location
get() = TurboPathConfiguration.Location(
assetFilePath = LOCAL_CONFIG_FILE,
remoteFileUrl = REMOTE_CONFIG_URL
)
Instead of using constants, you could build a path based on the System locale of the device.
Examples:
configuration_en.json
configuration_es.json
configuration_fr.json
The server would have multiple remote endpoints that might be similar in nature:
/android/path_configuration_en.json
or /en/android/path_configuration.json
/android/path_configuration_es.json
or /es/android/path_configuration.json
/android/path_configuration_fr.json
or /fr/android/path_configuration.json
Then your method might look like this (pseudocode):
override val pathConfigurationLocation: TurboPathConfiguration.Location
get() = TurboPathConfiguration.Location(
assetFilePath = localConfigFor(Locale.getDefault().language),
remoteFileUrl = remoteConfigFor(Locale.getDefault().language)
)
fun localConfigFor(language: String?): String {
return "configuration_$language.json"
}
fun remoteConfigFor(language: String?): String {
return "/android/configuration_$language.json"
}
Then, in your various remote configs you'd set up different paths for different languages/etc if you needed to do so.
Would that work?
Thanks @donnfelker! Based on your idea I moved the config to the translation file:
override val pathConfigurationLocation: TurboPathConfiguration.Location
get() = TurboPathConfiguration.Location(
assetFilePath = getString(R.string.local_config_path),
remoteFileUrl = remoteUrl(R.string.remote_config_path)
)
fun Fragment.remoteUrl(@StringRes locationId: Int): String {
return BASE_URL + getString(locationId)
}
<!-- res/values/strings.xml -->
<resources>
...
<string name="local_config_path">json/configuration.json</string>
<string name="remote_config_path">/turbo/android/path_configuration.json</string>
</resources>
<!-- res/values-fr-rFR/strings.xml -->
<resources>
...
<string name="local_config_path">json/configuration_fr.json</string>
<string name="remote_config_path">/fr/turbo/android/path_configuration.json</string>
</resources>
The above works for loading the correct path config at boot, but it leaves the app in an incoherent state when the user changes their language: the new string resources get loaded but the tabs are still in the old language.
The code below listens to when the user clicks the switcher, waits for the translated page to load, then attempts to change the language client-side.
Server-side:
<% I18n.with_locale(new_locale) do %>
<%= link_to "switch to new language", url_in_new_locale,
data: { controller: "language",
action: "language#setLanguage",
'language-tag-value': I18n.locale } %>
<% end %>
// javascript/controllers/index.js
import LanguageController from "./language_controller.js"
application.register("language", LanguageController)
// javascript/controllers/language_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static get values() {
return { tag: String }
}
setLanguage() {
// update client right after the web page has switched to the new language
document.addEventListener('turbo:load', () => {
window.TurboNativeBridge.postMessage("setLanguage", { language: this.tagValue })
}, { once: true })
}
}
Client-side:
// build.gradle.kts
dependencies {
...
implementation(libs.androidx.appcompat)
implementation(libs.androidx.appcompatResources)
}
# gradle/lib.versions.toml
androidXAppCompat = "1.6.1"
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidXAppCompat" }
androidx-appcompatResources = { group = "androidx.appcompat", name = "appcompat-resources", version.ref = "androidXAppCompat" }
// Trigger this code when receiving the `setLanguage` JS event
// e.g. with Jumpstart Pro Android, add this to `MainSessionNavHostFragment`
// and bind it in bindNativeBridge(webView: TurboWebView)
// with `nativeAppJavaScriptInterface.onSetLanguage = ::onSetLanguage`
private fun onSetLanguage(tag: String) {
AppCompatDelegate.setApplicationLocales(forLanguageTags(tag))
// TODO: how to reload path config and update the navigation tabs and toolbar?
// This does not seem to do anything:
// reset()
// This does switch the language of the navigation tabs and toolbar,
// BUT the app gets minimized and the Android home screen is shown
// Handler(Looper.getMainLooper()).post(::reset)
// This "restarts" the app while keeping it visible, BUT:
// - goes back to the initial screen (current page is lost)
// - keeps the old web view running, according to what I see in the chrome debugger
// val name = ComponentName(context ?: return, MainActivity::class.java)
// val intent = makeRestartActivityTask(name)
// startActivity(intent)
}
@donnfelker would you know how to cleanly refresh the app language (see issues mentioned in the above code)?
NB: The above in-app language switching could be combined with Android 13's new app language settings screen. But it would also be necessary to restart the app when the language is changed there. How to do that?
@sedubois You will need to respond to Activity#onConfigurationChanged
most likely. From there you'll need to reload the views and/or tell Compose to re-render/recompose itself. More info: https://stackoverflow.com/a/33889985/5210 - I'm assuming this would be in MainActivity
in JSP Android.
This might require some additional work to get the config to reload. In the MainSessionNavHostFragment
you will probably need to make some changes to the onSessionCreated
method and tell it to reload the session config somehow. I havent dove too deep into it, but those are a couple things I'd look at.
Thanks @donnfelker! I got something working. At the moment I'm not even adding a MainActivity#onConfigurationChanged
override because it wasn't really necessary. Also in the beginning I didn't understand that for onConfigurationChanged to actually be called, I would have needed to add android:configChanges="layoutDirection|locale"
to <activity ...></activity>
in AndroidManifest.xml.
Here is the outline of the solution:
AppCompatDelegate.setApplicationLocales
.getString(R.string.my_string)
, it will properly change to the new language. However, the web page, top and bottom tab bar still need to be changed.android:configChanges="layoutDirection|locale"
isn't added in the Android manifest, it means that instead the whole activity will get recreated and MainActivity#onCreate
and onStart
will get called.MainActivity#onCreate
, the code needs to be adapted to instruct mainActivityViewModel
to reload its tabs based on the path configuration in the new locale. ...observe(this) { ... }
). The code passed in the block will then only be executed when the activity is properly recreated.Rails:
<% I18n.with_locale(new_locale) do %>
<%= link_to "switch to new language", url_in_new_locale,
data: { controller: "language",
action: "language#setLanguage",
'language-tag-value': I18n.locale, 'language-url-value': url_in_new_locale } %>
<% end %>
// javascript/controllers/index.js
import LanguageController from "./language_controller.js"
application.register("language", LanguageController)
// javascript/controllers/language_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static get values() {
return { tag: String, url: String }
}
setLanguage(event) {
event.preventDefault()
event.stopImmediatePropagation()
window.TurboNativeBridge.postMessage("setLanguage", { language: this.tagValue, url: this.urlValue })
}
}
Android:
// NativeAppJavaScriptInterface.kt
var onSetLanguage: (tag: String, url: String) -> Unit = { _, _ -> }
fun postMessage(jsonData: String?) {
...
when (val command = json.optString("name")) {
...
SET_LANGUAGE -> onSetLanguage(json.optString("language"), json.optString("url"))
}
}
// MainSessionNavHosFragment.kt
nativeAppJavaScriptInterface.onSetLanguage = { language, url ->
(activity as? MainActivity)?.let {
it.languageSwitchingUrl = url
Handler(Looper.getMainLooper()).post {
AppCompatDelegate.setApplicationLocales(forLanguageTags(language))
}
}
}
// Constants.kt
const val LANGUAGE_SWITCH_URL_KEY = "language-switch-url"
// MainActivity.kt
@Inject lateinit var sharedPreferences: SharedPreferences
var languageSwitchingUrl: String?
get() = sharedPreferences.getString(LANGUAGE_SWITCH_URL_KEY, null)
set(value) = sharedPreferences.edit().putString(LANGUAGE_SWITCH_URL_KEY, value).apply()
...
override fun onCreate(savedInstanceState: Bundle?) {
...
mainActivityViewModel.loadNavItems().observe(this) {
delegate.navigate(languageSwitchingUrl ?: startUrl)
languageSwitchingUrl = null
}
...
}
// MainActivityViewModel.kt
fun loadNavItems(): MutableLiveData<List<NavigationTabData>> {
return liveDataNavItems.apply { value = jumpstartConfiguration.getBottomMenuItems() }
}
The documentation instructs to store a Path Configuration JSON in the project; what is the proper way to store several of these, one per supported language, or alternately how to make sure the strings and URLs contained in this file get localized?