Skip to content

Instantly share code, notes, and snippets.

@ildar2
Created August 27, 2021 10:31
Show Gist options
  • Save ildar2/9da59118051fa4f21c3680cd09e0287c to your computer and use it in GitHub Desktop.
Save ildar2/9da59118051fa4f21c3680cd09e0287c to your computer and use it in GitHub Desktop.
Описание трёх подходов работы с локализацией
/**
Расскажу про три типа локализации:
- полностью с сервера
- нативная со статичной выгрузкой
- через StringResource
*/
//
//полностью с сервера
//
// Загружаем с бэка всё
// http://digital-dev.sberbank.kz/api/translate/preload
{
"successfull_registration": null,
"common_city": "Город",
"mobile.bug_exchange_rates": "Ошибка. Курсы валют - Покупка, продажа"
}
// через JsonToKotlinClass конвертим в класс
class PreLocalize { // или LocalizationFromServer
@SerializedName("common_city")
val commonCity: String = "Город"
@SerializedName("mobile.bug_exchange_rates")
val mobileBugExchangeRates: String = "Ошибка. Курсы валют - Покупка, продажа"
}
// загружаем с бэка
class SplashRepository {
suspend fun loadPreLocz() = apiCall {
val preLoczAll = openApi.getPrelocalizationAll()
setPreLocalizationAll(preLoczAll)
}
/**
* пересериализуем объект, чтобы вместо null подставились дефолтные значения
*/
fun setPreLocalizationAll(preLocalizationAll: PreLocalizationAll) = Gson().run {
val strippedOfNulls = toJson(preLocalizationAll)
val type: Type = object : TypeToken<PreLocalizationAll>() {}.type
LocaleHolder.preLocalizationAll = fromJson(strippedOfNulls, type)
}
}
// далее в коде уже вытаскиваем из статческого класса:
use_password.text = LocaleHolder.preLocalization.usePassword
// или через удобный экстеншн:
val Any?.preLocz: PreLocalize by FieldProperty { LocaleHolder.preLocalization }
use_password.text = preLocz.usePassword
// проблема!
// проблема в том, что FieldProperty создаёт для объекта Map, в котором хранит preLocz
// поэтому для долгоживущих объектов нужно пользоваться getPreLocz()
fun getPreLocz(): PreLocalize = LocaleHolder.preLocalization
плюсы:
- можно на бэке менять все строки
- удобный статический объект, можно использовать из любого места preLocz, getPreLocz()
минусы:
- каждый раз грузить много данных
- админы могут криво написать форматирование, тогда могут быть краши
val mobileEqLengthInSymbols: String = "Длина поля %s должна быть равна %d символам"
- объект на 2000+ строк в редакторе
- баги и утечки памяти из-за FieldProperty
//
// нативная со статичной выгрузкой
//
// определяем строки в strings.xml как обычно
// генерируем в активити статический объект StaticLocalization
class MainActivity : AppCompatActivity(R.layout.activity_main) {
private val sessionManager: SessionManager by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
StaticLocalization.generate(this)
}
}
class StaticLocalization {
companion object {
private var staticStrings: HashMap<Int, String> = hashMapOf()
/**
* Собираем все строки из ресурсов в пары id - value
*/
fun generate(context: Context?): HashMap<Int, String> {
val fields: Array<Field> = R.string::class.java.fields
val strings = HashMap<Int, String>()
fields.forEach {
val id = it.get(this)?.cast<Int>().orZero()
strings[id] = context?.getString(id) ?: it.name
}
staticStrings = strings
return strings
}
}
/**
* Для удобства использования ресурсных строк
* можно получать строки без контекста
*/
fun static(@StringRes id: Int): String = staticStrings[id] ?: "--"
}
// в прогуарде нужно написать исключение для R.string
# static localization
-keep class kz.infin.R$string { *; }
плюсы:
- почти нативная локализация со почти всеми родными фичами
- статический объект, можно юзать из любого места
минусы:
- чтобы поменять строку, нужно собрать новый apk
- нельзя юзать стандартное форматирование (<b>, <i> ... ), plurals
- строки хранятся в необфусцированном виде в памяти
//
// через StringResource
//
sealed class ResourceString {
abstract fun format(context: Context): String
}
data class IdResourceString(private val id: Int) : ResourceString() {
override fun format(context: Context): String = context.getString(id)
override fun toString() = "IdResourceString: $id"
}
data class TextResourceString(private val text: String?) : ResourceString() {
override fun format(context: Context): String = text ?: ""
override fun toString() = "TextResourceString: $text"
}
class FormatResourceString(private val id: Int, vararg val args: Any) : ResourceString() {
override fun format(context: Context): String = context.getString(id, *args)
override fun equals(other: Any?) = other is FormatResourceString && id == other.id && args.contentEquals(other.args)
override fun hashCode() = id * 37 xor args.contentHashCode()
override fun toString() = "FormatResourceString: $id ${args.contentToString()}"
}
// создаём в любом месте:
IdResourceString(R.string.request_http_error_500)
TextResourceString(message ?: error ?: default)
FormatResourceString(R.string.request_error, e::class.java.simpleName, e.localizedMessage)
// во фрагменте вытаскиваем:
tv_error.text = message.format(requireContext())
плюсы:
- полноценная нативная локализация
- можно юзать в любом месте. во время отображения юзеру всегда есть контекст, чтобы вызвать format(Context)
минусы:
- чтобы поменять строку, нужно собрать новый apk
- длинная запись
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment