2.16. Project: интернационализация

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

Вот что они означают:

Интернационализация (или сокращённо i18n: i-18букв-n) — это процесс, который делает ваше мобильное приложение готовым к использованию на разных языках и в разных регионах.

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

Локализация (или сокращённо l10n: l-10букв-n) — это конкретная реализация интернационализации, которая заключается в адаптации вашего приложения для определённой локали или языка.

Это означает, что вы создаёте переводы содержимого вашего приложения на конкретный язык, чтобы пользователи из этой локали могли использовать приложение на своём родном языке.

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

Во Flutter интернационализации можно добиться с помощью пакета flutter_localizations — он предоставляет набор инструментов и утилит для работы со строками, датами, временем и другими типами данных, которые различаются в зависимости от языка или региона.

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

Весь код, приведённый в примерах, можно найти в репозитории — в нём собрано приложение с настроенной интернационализацией.

Настройка flutter_localizations и генерации переводов

Работа с библиотекой состоит из нескольких частей:

  1. Настройка конфигурации в файле l10n.yaml.
  2. Описание файлов локализации.
  3. Подключение конфигурации в коде приложения.
  4. Использование локализованных строк в приложении.

Чтобы всё настроить, нужно пойти по такому алгоритму:

  1. Добавить в файл pubspec.yaml в зависимости (dependencies) проекта flutter_localizations:

    dependencies:
      flutter:
        sdk: flutter
    
      flutter_localizations: # Добавить эти
        sdk: flutter         # строки
    
      # ...другие зависимости
    
  2. Включить генерацию:

    flutter:
      generate: true # Добавить эту строку
    
  3. Добавить файл l10n.yaml в корневую директорию проекта. Этот файл описывает конфигурацию интернационализации:

    arb-dir: l10n # Директория, где хранятся файлы переводов
    template-arb-file: app_en.arb # Файл-шаблон, где описываются параметры строк
    output-localization-file: app_localizations.dart # Сгенерированный файл локализации
    nullable-getter: false # Доступ к строкам можно получить через no-nullable-метод of(context)
    
  4. С помощью файла l10n.yaml можно сделать и более детальную параметризацию: указать название генерируемого класса, который будет предоставлять наши переводы в коде, путь для сгенерированных файлов и другие расширенные возможности. Все параметры можно изучить в документации. Мы же ограничимся параметрами, описанными выше, и позже инкапсулируем всю логику работы с интернационализацией в отдельном файле, в том числе и список поддерживаемых языков и список делегатов. Подробнее о делегатах будет рассказано ниже.

  5. В корне проекта нужно создать папку l10n, а в ней — файлы переводов в формате .arb (App Resource Bundle). Файл app_en.arb с английским переводом должен использоваться как шаблон — создайте его в той же папке l10n. В файле-шаблоне описываются не только сами строки, но и их описание и параметры (о них поговорим немного позже). В качестве шаблона можно выбрать и другой файл — например, если изначально у вас нет английской локализации. Указывать описание не обязательно, но может быть полезно для понимания, где строка будет использоваться.

    {
      "appTitle": "Flutter i18n Demo",
      "@appTitle": {
        "description": "AppBar title"
      },
      "pushCount": "You have pushed the button this many times:",
      "@pushCount": {
        "description": "How many times the button has been pushed"
      }
    }
    
  6. Выполнить команду flutter gen-l10n — она сгенерирует необходимые файлы для использования переводов в коде. Либо выполнить команду flutter run — вместе с запуском приложения всё, что нужно, сгенерируется автоматически. Весь сгенерированный код можно найти по следующему пути: .dart_tool/flutter_gen/gen_l10n/app_localizations.dart.

Настройка MaterialApp

MaterialApp, CupertinoApp и WidgetApp — это виджеты, которые обеспечивают базовую конфигурацию и настройку приложения, в том числе и конфигурируют его интернационализацию.

  1. В файл, где находится MaterialApp (в нашем примере это main.dart), добавить импорт: import 'package:flutter_localizations/flutter_localizations.dart'.

  2. В параметры MaterialApp добавить supportedLocales — пока мы поддержали только английский язык, поэтому указываем только локаль Locale(’en’). И в параметр localizationsDelegates добавить три делегата: GlobalWidgetsLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate. Каждый из этих делегатов нужен для поддержки локализации и правильного отображения стандартных элементов пользовательского интерфейса в зависимости от выбранного языка: общих, Material- и Cupertino-специфичных. Например, в Material- и Cupertino-компонентах есть стандартный виджет для выбора даты — соответствующие Material и Cupertino делегаты отвечают за перевод таких стандартных виджетов. Ещё один пример — при выделении текста появляется подсказка: «Вырезать / копировать / вставить», стандартные делегаты отвечают за перевод этого всплывающего окна.

    @override
      Widget build(BuildContext context) {
        return MaterialApp(
          supportedLocales: [Locale('en')],
          localizationsDelegates: [
            GlobalWidgetsLocalizations.delegate,
            GlobalMaterialLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
          ],
          home: HomePage(),
        );
      }
    
  3. Добавьте импорт: import 'package:flutter_gen/gen_l10n/app_localizations.dart'.

  4. В список делегатов добавьте AppLocalizations.delegate — это делегат, добавляющий строки, подготовленные в файлах .arb в проекте.

С этого момента можно начать использовать ваши строки в самом приложении.

Использование строк

MaterialApp предоставляет доступ к локализованным строкам через метод AppLocalizations.of(context).

  1. Добавьте заголовок в AppBar:

    appBar: AppBar(
      title: Text(AppLocalizations.of(context).appTitle),
    ),
    
  2. Добавьте текст по ключу:

    Text(
      AppLocalizations.of(context).pushCount,
    ),
    
  3. Теперь приложение готово к локализации на другие языки. Добавьте новый файл .arb с переводами в директорию lib/l10n. Файл нужно назвать в формате app_[код_языка].arb, например app_ru.arb. При этом указывать описание повторно не обязательно.

    {
      "appTitle": "Flutter i18n Демо",
      "pushCount": "Вы нажали на кнопку столько раз:"
    }
    
  4. Добавьте новую локаль в список supportedLocales: supportedLocales: [Locale('en'), Locale('ru')].

По умолчанию приложение будет использовать текущую локаль устройства, но вы можете передать желаемую локаль через параметр locale в MaterialApp: locale: Locale('ru').

Класс для работы со строками

Базовая реализация интернационализации готова, но давайте проделаем ещё несколько простых действий, чтобы повысить удобство использования:

  1. Создайте файл s.dart. Внутри него создайте класс S. Вас может насторожить такое название — обычно файлам и классам стараются давать говорящие названия, а S явно к ним не относится. Но мы даём такое имя осознанно — это своего рода договорённость, что S — это сокращение от Strings, для доступа к нашим строкам. Таким образом громоздкий вызов AppLocalizations.of(context)!.pushCount мы можем укоротить до S.of(context).pushCount.

  2. Перенесите в класс S импорты, связанные с интернационализацией:

    import 'package:flutter_localizations/flutter_localizations.dart';
    import 'package:flutter_gen/gen_l10n/app_localizations.dart';
    
  3. Перенесите в класс S локаль, поддерживаемые локали и делегаты — всё в виде статических полей.

    static const locale = Locale('ru');
    
    static const supportedLocales = [Locale('en'), Locale('ru')];
    
    static const localizationDelegates = <LocalizationsDelegate>[
      GlobalWidgetsLocalizations.delegate,
      GlobalMaterialLocalizations.delegate,
      GlobalCupertinoLocalizations.delegate,
      AppLocalizations.delegate,
    ];
    
  4. Воспользуйтесь этими полями в MaterialApp:

    return const MaterialApp(
      supportedLocales: S.supportedLocales,
      locale: S.locale,
      localizationsDelegates: S.localizationDelegates,
      home: HomePage(),
    );
    
  5. Добавьте в класс S метод of:

    static AppLocalizations of(BuildContext context) =>
          AppLocalizations.of(context);
    
  6. В местах, где вы использовали AppLocalizations.of(context), начните использовать S.of(context). Кроме того, теперь можно полностью избавиться от импортов flutter_localizations и app_localizations (которые мы переносили выше) и использовать только импорт import 's.dart'.

Описанный выше алгоритм объединения локализации в класс S не обязательный шаг, и вместо него можно воспользоваться детальными настройками из файла l10n.yaml. Но подход с отдельным файлом s.dart и классом S позволяет полностью сконцентрировать детали реализации внутри и использовать их как точку входа в инструменты интернационализации.

Это упростит написание текущего кода, а в будущем, если потребуются какие-то крупные изменения в интернационализации, их можно будет сделать, не затрагивая остальной код приложения.

Расширенные возможности

Интерполяция

До сих пор наша строка выглядела так: “Вы нажали на кнопку столько раз:” — и ниже, в отдельном виджете, мы отображали значение количества нажатий.

Но, допустим, появились новые требования, и теперь текст нужно писать вот так: «Вы нажали на кнопку 1 раз». Было бы удобно получить локализованную строку целиком, но при этом иметь возможность передать в эту строку собственное значение. И такая возможность существует — она называется интерполяция, по аналогии с передачей динамических значений в обычные строки. Реализуется это так:

  1. В файл-шаблон app_en.arb к строке, для которой хотите сделать интерполяцию, добавьте описание плейсхолдера. Описывать плейсхолдер достаточно только в файле-шаблоне и не нужно в каждом файле локализации. В качестве типа можно использовать любой примитивный тип или стандартную коллекцию из Dart.

      "pushCount": "You have pushed the button {count} times",
      "@pushCount": {
        "description": "How many times the button has been pushed",
    
        // Часть, которую нужно добавить
        "placeholders": {
          "count": {
            "type": "int",
            "example": "1"
          }
        }
    
      }
    
  2. Добавьте использование плейсхолдера в саму строку: "pushCount": "You have pushed the button {count} times".

  3. Не забудьте добавить использование плейсхолдера в каждый файл локализации — иначе на неисправленных локалях строка останется прежней.

  4. При следующем запуске приложения вы получите ошибку компиляции — потому что теперь вместо геттера вы получаете метод, которому нужно передать необходимое значение. Поправьте использование в коде — и всё готово!

    S.of(context).pushCount(_counter)
    

Но после добавления интерполяции мы сталкиваемся с новой проблемой: предложение становится несогласованным:

  • You have pushed the button 1 times — у слова times должно быть единственное число;
  • Вы нажали на кнопку 2 раз — тут, напротив, слово «раз» должно стоять во множественном числе.

Чтобы ещё больше усложнить себе жизнь и заодно разобрать расширенный пример, давайте изменим нашу строку на такую: «Вы заработали {count} печенек». Теперь нам нужно согласовать аж три варианта написания: «Вы заработали 1 печеньку», «Вы заработали 2/3/4 печеньки», «Вы заработали 5+ печенек».

Здесь нам поможет механизм плюрализации.

Плюрализация

Плюрализация — это отображение правильных форм множественного числа в зависимости от количества элементов. Она особенно полезна там, где требуется правильное склонение слов в зависимости от числа.

Давайте изменим строку в файле app_ru.arb и разберём её детальнее:

"pushCount": "Вы заработали {count, plural, =0{ни одной печеньки} =1{{count} печеньку} few{{count} печеньки} many{{count} печенек} other{{count} печенек}}"

Нас интересует часть, расположенная в фигурных скобках:

  1. count — имя плейсхолдера.
  2. plural — тип выбора, используемый в плейсхолдере. Ещё один из вариантов — select.
  3. =0{ни одной печеньки} — какой текст нужно использовать, если плейсхолдер принимает конкретное значение — в данном случае 0.
  4. =1{{count} печеньку} — то же, что и для 0, только для 1 и с использованием значения самой переменной. Вы можете использовать любое количество блоков =N для определения текста под каждое конкретное значение, если в этом есть необходимость. Этот случай будет также влиять на все числа вида 21, 31, 41 и т. д.
  5. few{{count} печеньки} — текст, который должен использоваться, если значение count относится к категории «Несколько». В русском языке это числа 2, 3, 4 и все числа больше 20, у которых в разряде единиц стоят 2, 3 и 4.
  6. many{{count} печенек} — текст для больших значений, в русском языке — начиная с 5 (за исключением значений для few).
  7. other{{count} печенек} — блок other должен быть переопределён всегда — на случай, если какой-то из случаев не обработан.

С помощью этого механизма можно гибко настраивать обработку множественных чисел в ваших строках.

Select

Похожий механизм позволяет реализовать выбор текста на основе любого текстового значения — этот механизм называется select.

Давайте доработаем нашу строку так, чтобы иметь возможность разговаривать с пользователем формальным или неформальным языком: с использованием местоимения «вы» или «ты».

В файле-шаблоне app_en.arb добавьте новый плейсхолдер к нашей строке pushCount:

"placeholders": {
  "count": {
    "type": "int",
    "example": "1"
  },

  // Часть, которую нужно добавить
  "style": {
    "type": "String"
  }
}

В файле русской локали замените начало строки на следующий вид: {style, select, formal{Вы заработали} informal{Ты заработал} other{Вы заработали}}. Структура идентична той, которую мы использовали для типа plural:

  1. style — имя плейсхолдера.
  2. select — тип выбора, используемый в плейсхолдере.
  3. formal{Вы заработали} informal{Ты заработал} — строки, которые должны использоваться для разных значений плейсхолдера style.
  4. other{Вы заработали} — блок other должен быть переопределён всегда: это строка, которая должна использоваться, если ни одно из указанных значений style не подошло.

Итоговый вид файла app_ru.arb:

{
  "appTitle": "Flutter i18n Демо",
  "pushCount": "{style, select, formal{Вы заработали} informal{Ты заработал} other{Вы заработали}} {count, plural, =0{ни одной печеньки} =1{{count} печеньку} few{{count} печеньки} many{{count} печенек} other{{count} печенек}}"
}

После очередного запуска приложения вы снова получите ошибку компиляции — потому что генерация кода добавила к методу pushCount ещё один параметр, style. Опишем класс PronounStyle для удобства указания стиля нашего текста. Каждый стиль — это просто строковая константа:

class PronounStyle {
  static const formal = 'formal';
  static const informal = 'informal';
}

И передадим одно из значений в метод pushCount.

S.of(context).pushCount(_counter, PronounStyle.informal)

Локализация даты

Аналогично типам plural и select можно использовать тип date — это позволит передавать тип DateTime в качестве параметра к строке, и для разных языков строка будет приобретать соответствующий локализованный вид. Для форматирования будет использоваться DateTimeFormat.

  1. Добавим новый плейсхолдер с типом DateTime и укажем формат:

    "placeholders": {
      "date": {
        "type": "DateTime",
        "format": "yMd"
      }
      // Другие типы
    }
    
  2. Добавим использование в строку в поддерживаемых языках. В нашем случае строки приобретают следующий вид:
    app_en.arb

    "pushCount": "You have earned {count, plural, =0{no cookies} =1{{count} cookie} many{{count} cookies} other{{count} cookies}} ({date})",
    

    app_ru.arb

    "pushCount": "{style, select, formal{Вы заработали} informal{Ты заработал} other{Вы заработали}} {count, plural, =0{ни одной печеньки} =1{{count} печеньку} few{{count} печеньки} many{{count} печенек} other{{count} печенек}} ({date})"
    
  3. Выполним команду flutter gen-l10n, чтобы новый плейсхолдер добавился в параметры в нашем коде. И добавим дату в метод:

    pushCount(DateTime.now(), _counter, PronounStyle.informal)
    

Смена языка

До сих пор мы использовали два подхода к выбору локали:

  1. системный язык устройства — если не переопределять параметр locale в MaterialApp;
  2. строго фиксированный язык — если переопределять параметр locale в MaterialApp.

Но мы можем изменять язык динамически, в зависимости от состояния приложения. Например, вы можете сделать экран настроек, в котором пользователь сможет выбирать язык приложения.

Эта задача сводится к тому, что мы должны уметь изменять значение locale в MaterialApp в зависимости от некоторой переменной в приложении. Реализуем простейший вариант:

  1. Изменим класс S, чтобы он соответствовал следующему коду:

    class S {
    
      // Создали константы с поддерживаемыми языками
      static const en = Locale('en');
      static const ru = Locale('ru');
    
      // Метод, который поможет сравнить языки между собой, чтобы их переключать
      static bool isEn(Locale locale) => locale == en;
    
      // Используем константы в качестве списка поддерживаемых локалей
      static const supportedLocales = [en, ru];
    
      // Ниже — без изменений
      static const localizationDelegates = <LocalizationsDelegate>[
        GlobalWidgetsLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        AppLocalizations.delegate,
      ];
    
      static AppLocalizations of(BuildContext context) =>
          AppLocalizations.of(context)!;
    }
    
  2. Превратим наш виджет App в StatefulWidget и добавим переменную Locale _locale в State.

  3. В MaterialApp добавим параметр builder, который будет описывать кнопку смены языка в верхнем левом углу экрана. Весь код App будет выглядеть так:

    class App extends StatefulWidget {
      const App({super.key});
    
      @override
      State<App> createState() => _AppState();
    }
    
    class _AppState extends State<App> {
      // Переменная локали
      var _locale = S.en;
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          supportedLocales: S.supportedLocales,
    
          // Используем переменную для указания локали в MaterialApp
          locale: _locale,
          localizationsDelegates: S.localizationDelegates,
    
          // Параметр builder с отображением кнопки смены локали
          builder: (context, child) => Material(
            child: SafeArea(
              child: Stack(
                children: [
                  child ?? const SizedBox.shrink(),
                  Align(
                    alignment: Alignment.topRight,
                    child: Padding(
                      padding: const EdgeInsets.all(12.0),
                      child: InkResponse(
                        child: Text(
                          _locale.languageCode.toUpperCase(),
                          style: const TextStyle(fontSize: 24, color: Colors.white),
                        ),
                        onTap: () {
    
                          // Проверяем текущую локаль и изменяем на противоположную
                          final newLocale = S.isEn(_locale) ? S.ru : S.en;
                          setState(() => _locale = newLocale);
                        },
                      ),
                    ),
                  )
                ],
              ),
            ),
          ),
          home: const HomePage(),
        );
      }
    }
    

Теперь в верхнем левом углу приложения появляется значение текущей локали — нажатие на него переключает локаль с русской на английскую.

Это простая реализация для наглядного примера, но вы можете организовать динамическое изменение локали удобным для вас способом, например через InheritedWidget или любой удобный State-management.

Подробнее про интернационализацию Flutter-приложений можно узнать в документации.

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф2.15. Project: navigation
Следующий параграф2.17. Flutter: архитектура фреймворка. Виды сборки