Background
I need to use a translations-SDK (Lokalise, docs here) that is intended to load strings resources from their servers.
This means that if you use getString , it will prefer what's on the server instead of what's on the app. This includes also the cases of inflation of layout XML files.
The problem
It seems that Android doesn't have a global resource handling that I can use. This is why the SDK says I should use one of these :
- For Activity, I can override the callback of
attachBaseContext. - For all other cases, that I need to get the resources of them, I can use
LokaliseResources(context).
Thing is, a lot of code in the app I work on doesn't involve an Activity. A lot of the UI on the app is floating (using SAW permission, AKA "System Alert Window").
This means that there is a lot of inflation of Views using just the Application class.
What I've tried
First I made a simple manager for this:
object TranslationsManager {
var resources: LokaliseResources? = null
@UiThread
fun initOnAppOnCreate(context: App) {
Lokalise.init(context, Keys.LOCALISE_SDK_TOKEN, Keys.LOCALISE_PROJECT_ID)
Lokalise.updateTranslations()
resources = LokaliseResources(context)
}
fun getResources(context: Context): Resources {
return resources ?: context.resources
}
}
I tried to perform various things using the library, but they crashed as it's not how the library works.
So these failed:
For the
getResourcesof the class that extends Application, I tried to return the one of the SDKUse
attachBaseContextof the class that implements Application. This causes a crash since it needs to be initialized before, so I tried to initialize it right in this callback, but still got a crash.For LayoutInflater, I tried to use
LayoutInflater.from(new ContextThemeWrapper(...)), and override itsgetResourcescallback, but it didn't do anything.I tried to use Philology library by having this:
object MyPhilologyRepositoryFactory : PhilologyRepositoryFactory {
override fun getPhilologyRepository(locale: Locale): PhilologyRepository {
return object : PhilologyRepository {
override fun getPlural(key: String, quantityString: String): CharSequence? {
Log.d("AppLog", "getPlural $key")
return TranslationsManager.resources?.getString(quantityString)
?: super.getPlural(key, quantityString)
}
override fun getText(key: String): CharSequence? {
Log.d("AppLog", "getText $key")
return TranslationsManager.resources?.getString(key) ?: super.getText(key)
}
override fun getTextArray(key: String): Array<CharSequence>? {
Log.d("AppLog", "getTextArray $key")
TranslationsManager.resources?.getStringArray(key)?.let { stringArray ->
val result = Array<CharSequence>(stringArray.size) { index ->
stringArray[index]
}
return result
}
return super.getTextArray(key)
}
}
}
}
And on the class that extends Application, use this:
Philology.init(MyPhilologyRepositoryFactory)
ViewPump.init(ViewPump.builder().addInterceptor(PhilologyInterceptor).build())
But when inflation was used in the app (and actually everywhere), I never saw that this code is being used, ever.
That being said, this is what I've succeeded:
1.For all Activities/Services, indeed I've added usage of attachBaseContext as the SDK says:
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(LokaliseContextWrapper.wrap(newBase))
}
2.For all custom views, I've used what I've made:
override fun getResources(): Resources {
return TranslationsManager.getResources(context)
}
Both of these took quite some time to find and add manually, one after another.
Sadly, still there seem to be some important cases.
I've found that at least for layout inflation (in the custom views, for example), the layout XML files don't take the resources from the SDK.
I've found an article "Taming Android Resources and LayoutInflater for string manipulation" from 2020 (cache here) saying I could use some trick of ContextThemeWrapper a bit more complex than what I tried, but sadly it lacks some important information (implementation of cloneInContext for example) that I've failed to use:
class CustomContextWrapper(
private val base: Context,
private val dynamicStringMap: Map<String, String>
) : ContextWrapper(base) {
override fun getResources() = CustomResources(base.resources, dynamicStringMap)
override fun getSystemService(name: String): Any? {
if (Context.LAYOUT_INFLATER_SERVICE == name) {
return CustomLayoutInflater(LayoutInflater.from(baseContext), this)
}
return super.getSystemService(name)
}
}
class CustomLayoutInflater constructor(
original: LayoutInflater,
newContext: Context,
) : LayoutInflater(original, newContext) {
override fun cloneInContext(p0: Context?): LayoutInflater {
TODO("Not yet implemented")
}
override fun onCreateView(name: String, attrs: AttributeSet): View? {
try {
val view = createView(name, "android.widget.", attrs)
if (view is TextView) {
// Here we get original TextView and then return it after overriding text
return overrideTextView(view, attrs)
}
} catch (e: ClassNotFoundException) {
} catch (inflateException: InflateException) {
}
return super.onCreateView(name, attrs)
}
private fun overrideTextView(view: TextView, attrs: AttributeSet?): TextView {
val typedArray =
view.context.obtainStyledAttributes(attrs, intArrayOf(android.R.attr.text))
val stringResource = typedArray.getResourceId(0, -1)
view.text = view.resources.getText(stringResource)
typedArray.recycle()
return view
}
}
However, it said I could use a library called "ViewPump" (here, and it actually suggested to use Philology library here) that will do the trick for me, and that from Android 30 we could use ResourcesProvider and ResourcesLoader classes. Sadly I couldn't find an example to use any of these for the purpose I'm working on.
The questions
Is it really possible to use the trick that was mentioned on the article? What should be done to use it properly?
How can I use the "ViewPump"/"Philology" library to achieve the same thing?
Is there any way to offer resources globally instead of using all that I've mentioned? So that all resources will be using the translation SDK, no matter where and how I reach the resources ? This takes a lot of time already, as I need to go over many classes and add handling of resources myself...
Will any of the above cover all cases? For example not just the inflation, but other cases such as TextView.setText(resId) ?
As for the new classes of Android API 30, because they are very new, I've decided to ask about them in a new post, here.
EDIT: Talking with Lokalise support, they said they already do use ViewPump, which means that it probably works in cases that don't match what I have.