DynamicResourceTranslator is an Android library that simplifies internationalization for your app. You only need to create
a single strings.xml file in your native language (not necessarily English), and the library will
automatically translate your app into the system language set on the user's phone "on-the-fly".
res/
├── values/
│ └── strings.xml
├── values-es/
│ └── strings.xml
├── values-fr/
│ └── strings.xml
res/
├── values/
│ └── strings.xml
Note: Language-specific directories (e.g.,
values-es/,values-fr/) are optional. If they exist, the API will use them.
- Dynamically translates string resources at runtime, eliminating the need for multiple language-specific
strings.xml. - Respects existing language-specific
strings.xml. - Supports fine-tuning of translation when automatic translations should be corrected ot shortened.
- Provides a pluggable translation engine architecture.
The library intercepts calls to getString() and stringResource(), which read from strings.xml resource files.
It then uses a Google translation service to translate the strings based on the language set in the phone's settings.
Translated values are stored in local storage for reuse and better performance.
To avoid blocking the UI thread, translated strings are stored in the persistent cache in the background, and the UI will update later when the screen is refreshed. If you
prefer to see the UI update immediately after translation, you can use getStringBlocking() and stringResourceBlocking() functions,
but beware that your UI may block the for some time the first time the screen is loaded.
Your app must have Internet access, at least the first time your app runs, to perform the initial translations. Ensure you add the following permissions to your manifest:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />Add the following to your settings.gradle file:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
}Add the following to your build.gradle file:
dependencies {
implementation 'com.github.izivkov:DynamicResourceTranslator:Tag'
}The easiest way to get access to the API is through a Singleton object DynamicResourceApi, which wraps the class containing the API methods.
Initialize DynamicResourceApi once, typically in MainActivity or your Application class:
DynamicResourceApi.init()Optionally, during initialization, you can also set language, overWrites and Translation Engine like this:
DynamicResourceApi.init()
.setOverwrites(arrayOf(
ResourceLocaleKey(R.string.hello, Locale("es")) to {"Hola"},
ResourceLocaleKey(R.string.hello, Locale("bg")) to {"Здравей %1\$s"}
))
.setAppLocale(Locale("es"))Setting the App Locale tells the library that your default strings.xml contains string in the specified language, Spanish in this case.
For more information, see Application-language-vs-Default-Language.
Then retrieve the API anywhere in your program:
val api = DynamicResourceApi.getApi() val api = DynamicTranslator()
.init ()
.setAppLocale(Locale("es")) // optional
.setOverwrites( // optional
arrayOf(
ResourceLocaleKey(R.string.hello, Locale("es")) to {"Hola"},
ResourceLocaleKey(R.string.hello, Locale("bg")) to {"Здравей %1\$s"}
)
)This method is better suitable if you like to use Dagger / Hilt and inject the API in you code.
Replace context.getString calls with api.getString:
// Before:
val text = context.getString(R.string.hello_world)
// After:
val text = api.getString(context, R.string.hello_world)For Jetpack Compose, replace stringResource with api.stringResource:
// Before:
val text = stringResource(id = R.string.hello_world)
// After:
val text = api.stringResource(LocalContext.current, R.string.hello_world)Application language is the language of your default strings.xml file. If you create this file with German strings instead of English,
your Application Language is German. To avoid unnecessary translation, you should call
setAppLocale(Locale("de")) during library initialization. If your strings.xml file is in English, there is no need to call this function.
On the other hand, the Default Language refers to the language currently set on the user's device. For example, if a user in Spain has their device set to Spanish, the default system language will be Spanish. This is the language into which strings should be translated.
API documentation can ge found here:
You can override specific translations in one of the following two ways:
-
Language-Specific
strings.xmlFile
Add a partialstrings.xmlfile for specific languages with only the strings you like to overwrite.Example for Spanish (
values-es/strings.xml):<resources> <string name="title_time">Hora</string> </resources>
The string with the Id
R.string.title_timewill always be translated asHora, regardless of the automatic translation. -
Providing Overwrites in Code
Call thesetOverwrites()method during initialization:
DynamicResourceApi.init()
.setOverwrites( // optional
arrayOf(
ResourceLocaleKey(R.string.hello, Locale("es")) to {"Hola"},
ResourceLocaleKey(R.string.hello, Locale("bg")) to {"Здравей %1\$s"}
)
)In addition, the API provides two functions to add overwrites from anywhere in your code:
api.addOverwrites(arrayOf(
ResourceLocaleKey(R.string.hello, Locale("es")) to {"Hola"},
ResourceLocaleKey(R.string.hello, Locale("bg")) to {"Здравей %1\$s"}
))
api.addOverwrite(ResourceLocaleKey(R.string.hello, Locale("es")) to {"Hola"})Translation Overwrites map a pair of (ID, Locale) to a lambda that returns a String.
The lambda can perform arbitrary transformations on the string, providing flexibility for dynamic content generation.
For example, the following overwrite customizes the English string with the ID R.string.hello to give a time-of-day-specific greeting:
ResourceLocaleKey(R.string.hello, Locale("en")) to {
val currentHour = java.time.LocalTime.now().hour
when (currentHour) {
in 5..11 -> "Good Morning"
in 12..17 -> "Good Afternoon"
in 18..21 -> "Good Evening"
else -> "Good Night"
} + ", %1\$s"
}Keep in mind that overwrites take precedence over both translations and strings defined in strings.xml, ensuring they are applied even if a translation exists for the target locale.
Run-time translation is triggered when the following conditions are met:
-
No Overwrite Exists:
If anoverWritevalue exists for this string and language, it will be used directly without further translation. -
Not Cached in Local Storage:
If the string is already cached in local storage, the cached value will be used, bypassing translation. -
Missing in Resource Files:
If the string ID cannot be found in the device's resource files (e.g.,strings.xmlfor the current language), translation will proceed. -
Network Connection Available:
A network connection is required to access translation services.
When all these conditions are met, the library translates the string and stores the result in the cache for future use.
This applies to any translation engine, even if the translation logic is trivial. Additionally, for multiple chained engines, either all will execute if the conditions are satisfied, or none will execute.
By default, the library uses the built-in BushTranslationEngine, based on this library.
You can provide your own translation engine for customized translations.
For the purposes of illustration, here’s a trivial engine that converts all strings to uppercase.
class UppercaseTranslationEngine : ITranslationEngine {
override fun translate(text: String, target: Locale): String = text.uppercase()
}To use your custom engine, register it during initialization:
DynamicResourceApi.init().setEngine(UppercaseTranslationEngine())After that, all translations will use the UppercaseTranslationEngine.
Suppose you want to convert your translated text to uppercase after translating. You can achieve this by cascading (or chaining) two or more engines like this:
// Using the Singleton object:
DynamicResourceApi.init().setEngines(
listOf(
BushTranslationEngine(),
UppercaseTranslationEngine()))
// Or directly in the dynamic translator:
DynamicTranslator().init()
.setEngines(
listOf(
BushTranslationEngine(),
UppercaseTranslationEngine()))
// Or to add an engine, you would call:
DynamicTranslator().init()
.addEngine(
UppercaseTranslationEngine())
// Or:
DynamicTranslator().init()
.addEngines(
listOf(
UppercaseTranslationEngine(),
// more engines here...
))The output of BushTranslationEngine will be fed to UppercaseTranslationEngine for further transformation.
Note that values in the Overwrites list are not translated and are passed as they are.
Adding translation engines, you can perform all sorts of transformations on the text. For example, if you want to remove offensive words or expressions for certain languages, you can write an engine to do that.
When loading the app for the first time, the library will update the the persistent cache with the translations asynchronously. This way the UI will not be blocked. This may initially show some untranslated strings, but this will be fixed the next time the screen is loaded or refreshed.
The Casio GShock Smart Sync app is an open-source alternative to the Casio app. It uses this library to provide localisation to GShock users around the World.
If you like us to list your project which uses this library, contact us and we will include a link.
- This project is using the great translator Kotlin library.
- Google Translate
This is using an unofficial Google API. This may cease to work at any point in time, and you should be prepared to use a different translation engine if needed.
This project is licensed under the LICENSE.