Как стать автором
Обновить

Локализация Flutter приложения на сервере

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров706

g11n, i18n, l10n... или один из множества вариантов локализации приложения. Привет, меня зовут Константин Комков и я надеюсь данный пример и последовательность шагов сэкономят Вам время при разработке!

Есть два способа хранить локализованные данные — внутри приложения или запрашивать их с сервера. Второй подход сложнее и трудозатратнее, но дает следующее преимущество — возможность заменить ресурс или исправить ошибку в переводе без новой сборки приложения. В этой статье я опишу логику работы приложения если данные приходят с сервера.

Исходные данные

  1. Для локализации необходимо знать список языков и список переводов фраз. Данные придут с сервера, значит, интерфейс приложения должен обновится после выполнения запросов.

  2. В этой статье я рассматриваю пример, когда данные загружаются не сразу, а после выполнения какой‑либо бизнес логики — это чуть усложняет пример.

Шаги реализации

  1. Необходимо предусмотреть локаль по умолчанию и ресурсы (переводы для этой локали) должны быть в коде приложения. До того как сервер пришлет переводы — показываем данные которые зашиты в приложение. Можно добавить в приложение переводы для всех используемых языков, но следует помнить, что эти файлы нужно будет поддерживать в актуальном состоянии — достаточно одного или двух языков. Все запросы к API должны предусматривать передачу флага для выбранной локали, чтобы приходили локализованные данные.

  2. После получения данных с сервера — кешируем их. При повторном запуске приложения переводы должны быть взяты из кеша, т.к. данные в кеше уже более новые чем, записанные в приложение при сборке.

    Тонкости реализации кеширования данных: если запустить приложение в котором есть новые ключи, приложение получит данные из кеша, а переводов для новых ключей не будет.

  3. Необходимо добавить логику определения версии приложения — новая или старая, для этого будем получать текущую версию приложения и после сравнения её с версией из кеша, записывать в кеш. Если версия новая переводы получаем из кода приложения, иначе из кеша.

  4. При добавлении новых фраз, языков всегда нужно поднимать версию приложения. Для IOS приложений следует помнить что список поддерживаемых языков указывается в файле info.plist.

  5. После получения данных с сервера необходимо обновить интерфейс приложения — для обновления текста.

Последовательность действий

  1. Создать модели для локализуемых объектов. Для локализации текста создаем ключ для фразы, а потом находим значение для этого ключа в соответствии с выбранной локалью.

    Не используйте сущность для модели содержащей ключи и переводы - у Вас и так будет много мест где нужно вносить изменения при добавлении в приложение новой фразы - (Keep it simple stupid). Ключи для переводимых фраз лучше группировать по страницам pageNamePhrasePart, часто встречающиеся слова лучше записывать без названий страниц.

  2. По Clean Architecture создать внешнее и локальное хранилище данных для локализации.

  3. Создать репозиторий для локализации.

  4. Создать сервисы: сохранения данных в кеш, локализации, информации о версии приложения.

  5. Добавить в проект библиотеку easy localization.

    Мне больше нравится библиотека intl, но так сложилось, что «виновником» появления статьи является easy localization.

  6. Написать свой AssetLoader — AssetCacheRemoteLoader, т.к. у нас не простая логика, а подходящего загрузчика в списке доступных нет.

  7. Реализовать обновление интерфейса, в данном случае я использовал ValueListenableBuilder, но можно использовать и любой state manager.

  8. Запустить процесс получения данных с сервера в нужном вам месте приложения и обновить 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);
  }
}

Ознакомиться с кодом проекта

Теги:
Хабы:
+1
Комментарии0

Публикации

Работа

Ближайшие события