g11n, i18n, l10n... или один из множества вариантов локализации приложения. Привет, меня зовут Константин Комков и я надеюсь данный пример и последовательность шагов сэкономят Вам время при разработке!
Есть два способа хранить локализованные данные — внутри приложения или запрашивать их с сервера. Второй подход сложнее и трудозатратнее, но дает следующее преимущество — возможность заменить ресурс или исправить ошибку в переводе без новой сборки приложения. В этой статье я опишу логику работы приложения если данные приходят с сервера.
Исходные данные
-
Для локализации необходимо знать список языков и список переводов фраз. Данные придут с сервера, значит, интерфейс приложения должен обновится после выполнения запросов.
-
В этой статье я рассматриваю пример, когда данные загружаются не сразу, а после выполнения какой‑либо бизнес логики — это чуть усложняет пример.
Шаги реализации
-
Необходимо предусмотреть локаль по умолчанию и ресурсы (переводы для этой локали) должны быть в коде приложения. До того как сервер пришлет переводы — показываем данные которые зашиты в приложение. Можно добавить в приложение переводы для всех используемых языков, но следует помнить, что эти файлы нужно будет поддерживать в актуальном состоянии — достаточно одного или двух языков. Все запросы к API должны предусматривать передачу флага для выбранной локали, чтобы приходили локализованные данные.
-
После получения данных с сервера — кешируем их. При повторном запуске приложения переводы должны быть взяты из кеша, т.к. данные в кеше уже более новые чем, записанные в приложение при сборке.
Тонкости реализации кеширования данных: если запустить приложение в котором есть новые ключи, приложение получит данные из кеша, а переводов для новых ключей не будет.
-
Необходимо добавить логику определения версии приложения — новая или старая, для этого будем получать текущую версию приложения и после сравнения её с версией из кеша, записывать в кеш. Если версия новая переводы получаем из кода приложения, иначе из кеша.
-
При добавлении новых фраз, языков всегда нужно поднимать версию приложения. Для IOS приложений следует помнить что список поддерживаемых языков указывается в файле info.plist.
-
После получения данных с сервера необходимо обновить интерфейс приложения — для обновления текста.
Последовательность действий
-
Создать модели для локализуемых объектов. Для локализации текста создаем ключ для фразы, а потом находим значение для этого ключа в соответствии с выбранной локалью.
Не используйте сущность для модели содержащей ключи и переводы - у Вас и так будет много мест где нужно вносить изменения при добавлении в приложение новой фразы - (Keep it simple stupid). Ключи для переводимых фраз лучше группировать по страницам pageNamePhrasePart, часто встречающиеся слова лучше записывать без названий страниц.
-
По Clean Architecture создать внешнее и локальное хранилище данных для локализации.
-
Создать репозиторий для локализации.
-
Создать сервисы: сохранения данных в кеш, локализации, информации о версии приложения.
-
Добавить в проект библиотеку easy localization.
Мне больше нравится библиотека intl, но так сложилось, что «виновником» появления статьи является easy localization.
-
Написать свой AssetLoader — AssetCacheRemoteLoader, т.к. у нас не простая логика, а подходящего загрузчика в списке доступных нет.
-
Реализовать обновление интерфейса, в данном случае я использовал ValueListenableBuilder, но можно использовать и любой state manager.
-
Запустить процесс получения данных с сервера в нужном вам месте приложения и обновить EasyLocalizationProvider.
В этой последовательности действий нет ничего сложного, но пункты 6 и 8 могут вызвать затруднения поэтому приведу здесь пример реализации.
asset_cache_remote_loader.dart
import 'dart:async';
import 'dart:ui';
import 'package:easy_localization/easy_localization.dart';
import 'package:server_side_localization/features/localization/data/models/supported_translations.dart';
import 'package:server_side_localization/features/localization/data/models/translations.dart';
import 'package:server_side_localization/features/localization/domain/repositories/localization_cache_repository.dart';
import 'package:server_side_localization/generated/codegen_loader.g.dart';
import 'package:server_side_localization/services/package_info/package_info_service.dart';
class AssetCacheRemoteLoader extends RootBundleAssetLoader {
static final AssetCacheRemoteLoader _singleton =
AssetCacheRemoteLoader._internal();
factory AssetCacheRemoteLoader() => _singleton;
AssetCacheRemoteLoader._internal();
bool isFirstLoading = true;
SupportedTranslations? translations;
@override
Future<Map<String, dynamic>?> load(String path, Locale locale) async {
if (translations != null) {
final value = translations!.toJson()[locale.languageCode];
if (value is Translations) {
return value.toJson();
}
return CodegenLoader.mapLocales.containsKey(locale.languageCode)
? CodegenLoader.mapLocales[locale.languageCode]
: CodegenLoader.mapLocales['en'];
}
final (
localData,
isAnotherVersion,
) = await (
LocalizationCacheRepositoryImpl().getSupportedLocale(locale.languageCode),
PackageInfoService().isAnotherVersion(),
).wait;
if (isAnotherVersion && isFirstLoading) {
isFirstLoading = false;
final currentLocale =
CodegenLoader.mapLocales.containsKey(locale.languageCode)
? locale
: Locale('en');
return CodegenLoader.mapLocales[currentLocale.languageCode];
}
return localData == null
? CodegenLoader.mapLocales[locale.languageCode]
: localData.toJson();
}
}
Функция обновления локализуемых данных в easy localization
Future<void> _resetTranslations({
required SupportedTranslations translations,
required List<LanguageEntity> languages,
}) async {
final provider = EasyLocalization.of(context);
AssetCacheRemoteLoader().translations = translations;
if (LocalizationService().locale != null && provider != null) {
await provider.delegate.localizationController?.loadTranslations();
await provider.delegate.load(LocalizationService().locale!);
LocalizationService().setLanguages(languages);
}
}
Автор: KonstantinKomkov