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); } }
ссылка на оригинал статьи https://habr.com/ru/articles/903568/
Добавить комментарий