Created
August 27, 2021 10:31
-
-
Save ildar2/9da59118051fa4f21c3680cd09e0287c to your computer and use it in GitHub Desktop.
Описание трёх подходов работы с локализацией
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
Расскажу про три типа локализации: | |
- полностью с сервера | |
- нативная со статичной выгрузкой | |
- через 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