Skip to content

Instantly share code, notes, and snippets.

@NaingAungLuu
Last active February 6, 2023 09:49
Show Gist options
  • Save NaingAungLuu/c258288c947f8a348f337cb6002d27b8 to your computer and use it in GitHub Desktop.
Save NaingAungLuu/c258288c947f8a348f337cb6002d27b8 to your computer and use it in GitHub Desktop.
Dynamic Localization
enum class AppLanguage(val value: String) {
ENGLISH("English"),
BURMESE("Burmese"),
CHINESE("Chinese");
}
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="localization"
type="me.naingaungluu.dynamiclocalization.data.models.Localization" />
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{localization.lblGreeting}"
/>
</layout>
class HomeFragment() : BaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Collect Flow
viewModel.localizationFlow
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach {
binding.localization = it
}
.launchIn(lifecycleScope)
}
}
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel()
) {
HomeScreenStateless(
HomeScreenState(
data = HomeScreenState.Data(
localization = viewModel.localizationFlow.collectAsState(
initial = Localization.getDefaultLocalization()
),
appLanguage = viewModel.currentLanguageFlow.collectAsState(
initial = AppLanguage.ENGLISH
)
),
delegates = HomeScreenState.Delegate(
onTapEnglish = viewModel::switchToEnglish,
onTapBurmese = viewModel::switchToBurmese,
onTapChinese = viewModel::switchToChinese
)
)
)
}
@HiltViewModel
class HomeViewModel @Inject constructor() : LocalizedViewModel() {
private val _currentLanguage: MutableStateFlow<AppLanguage> = MutableStateFlow(AppLanguage.ENGLISH)
val currentLanguageFlow: Flow<AppLanguage> = _currentLanguage
fun switchToEnglish() = switchLanguage(AppLanguage.ENGLISH)
fun switchToChinese() = switchLanguage(AppLanguage.CHINESE)
fun switchToBurmese() = switchLanguage(AppLanguage.BURMESE)
private fun switchLanguage(language: AppLanguage) {
viewModelScope.launch {
localizationRepository.updateLanguage(language)
_currentLanguage.emit(language)
}
}
}
{
"en" : {
"lblGreeting" : "Hello",
"lblSelectedLanguage" : "Selected Language : %@",
"lblEnglish" : "English",
"lblBurmese" : "Burmese",
"lblChinese" : "Chinese"
},
"cn" : {
"lblGreeting" : "你好",
"lblSelectedLanguage" : "选择的语言 : %@",
"lblEnglish" : "英语",
"lblBurmese" : "缅甸语",
"lblChinese" : "中文"
},
"mm" : {
"lblGreeting" : "မင်္ဂလာပါ။",
"lblSelectedLanguage" : "ရွေးချယ်ထားသောဘာသာစကား : %@",
"lblEnglish" : "အင်္ဂလိပ်",
"lblBurmese" : "ဗမာ",
"lblChinese" : "တရုတ်"
}
}
class Localization : Serializable {
/**
* Properties
*/
var lblGreeting : String = ""
var lblSelectedLanguage : String = ""
var lblEnglish : String = ""
var lblBurmese : String = ""
var lblChinese : String = ""
companion object {
//dummy
private val dummy = Localization().apply {
lblGreeting = "Hello"
lblSelectedLanguage = "Selected Language : %@"
lblEnglish = "English"
lblChinese = "Chinese"
lblBurmese = "Burmese"
}
fun getDefaultLocalization() : Localization = dummy
}
}
class Localization : Serializable {
// ...
companion object {
const val TEMPLATE_HANDLE = "%@"
fun getTemplatedString(format: String, vararg params: String): String
= if (params.isNotEmpty() && format.contains(TEMPLATE_HANDLE))
getTemplatedString(
format.replaceFirst(TEMPLATE_HANDLE, params.first()),
*params.drop(1).toTypedArray()
)
else
format
// Localization.getTemplatedString("Value : %@" , "Hello') returns "Value: Hello"
}
// ...
}
fun getTemplatedString(format: String, vararg params: String): String
= if (params.isNotEmpty() && format.contains("%@")) {
getTemplatedString(
format.replaceFirst("%@", params.first()),
*params.drop(1).toTypedArray()
)
} else {
format
}
data class LocalizationBundle(
val en : Localization = Localization.getDefaultLocalization(),
val cn : Localization = Localization.getDefaultLocalization(),
val mm : Localization = Localization.getDefaultLocalization()
)
class LocalizationLocalImpl @Inject constructor(
@ApplicationContext private val context : Context
): LocalizationLocal {
private val moshiAdapter : JsonAdapter<LocalizationBundle> by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
.adapter(LocalizationBundle::class.java)
}
override fun getLocalizationBundle(): LocalizationBundle {
// Fetch Localization Bundle from Raw Asset
val localizationJson = getLocalizationJsonFromLocal()
val localizationBundle = moshiAdapter.fromJson(localizationJson)
return localizationBundle ?: LocalizationBundle()
}
private fun getLocalizationJsonFromLocal() : String
= context.resources
.openRawResource(R.raw.localization)
.bufferedReader()
.use { it.readText() }
}
interface LocalizationRepository {
val localizationFlow: StateFlow<Localization>
val currentAppLanguage: AppLanguage
suspend fun updateLanguage(language: AppLanguage)
}
class LocalizationRepositoryImpl @Inject constructor(
private val preferenceStorage: PreferenceStorage,
private val local: LocalizationLocal,
private val remote: LocalizationRemote,
) : LocalizationRepository {
private val _localizationFlow: MutableStateFlow<Localization> by lazy {
MutableStateFlow(
local.getLocalizationBundle().getLocalization(currentAppLanguage)
)
}
override val localizationFlow: StateFlow<Localization> = _localizationFlow
private var cachedLocalizationBundle : LocalizationBundle? = null
override val currentAppLanguage: AppLanguage
get() = preferenceStorage.getAppLanguage()
init {
CoroutineScope(Dispatchers.IO).launch {
getLocalizationFromRemote()
}
}
override suspend fun updateLanguage(language: AppLanguage) {
_localizationFlow.value = getLocalization(language)
preferenceStorage.saveAppLanguage(language)
}
private suspend fun getLocalizationFromRemote() {
// Fetch localization data from remote here
remote.getLocalizationBundle().let {
cachedLocalizationBundle = it
_localizationFlow.value = it.getLocalization(currentAppLanguage)
}
}
private fun getLocalization(language: AppLanguage) : Localization
= cachedLocalizationBundle?.getLocalization(language)
?: local.getLocalizationBundle().getLocalization(language)
fun LocalizationBundle.getLocalization(language: AppLanguage): Localization
= when(language) {
AppLanguage.ENGLISH -> en
AppLanguage.CHINESE -> cn
AppLanguage.BURMESE -> mm
}
}
// ...
private val _localizationFlow: MutableStateFlow<Localization> by lazy {
MutableStateFlow(
// ...
)
}
override val localizationFlow: StateFlow<Localization> = _localizationFlow
private var cachedLocalizationBundle : LocalizationBundle? = null
...
}
// ...
private fun getLocalization(language: AppLanguage) : Localization
= cachedLocalizationBundle?.getLocalization(language)
?: local.getLocalizationBundle().getLocalization(language)
fun LocalizationBundle.getLocalization(language: AppLanguage): Localization
= when(language) {
AppLanguage.ENGLISH -> en
AppLanguage.CHINESE -> cn
AppLanguage.BURMESE -> mm
}
// ...
abstract class LocalizedViewModel : ViewModel() {
@Inject
protected lateinit var localizationRepository: LocalizationRepository
val localizationFlow : StateFlow<Localization>
get() = localizationRepository.localizationFlow
// To get Localization without having to collect flow
val localization : Localization
get() = localizationFlow.value
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment