🌎

Flutterの多言語対応、riverpodを使用して便利に

2024/08/16に公開

Flutter の多言語化対応

はじめに

本記事では、Flutter アプリケーションの多言語化対応を段階的に実装する方法を詳しく解説します。基本的な多言語化の実装から始め、最後に Riverpod を導入して状態管理を改善します。
バージョンは最新のものをご使用ください。(本記事は 2024/08/16 の最新のものです)

使用パッケージの紹介

https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization
https://pub.dev/packages/intl
https://pub.dev/packages/shared_preferences
https://pub.dev/packages/flutter_riverpod
https://pub.dev/packages/riverpod_annotation
https://pub.dev/packages/build_runner
https://pub.dev/packages/riverpod_generator

パート 1: 基本的な多言語化の実装

1. 依存関係の設定

まず、pubspec.yaml ファイルに必要な依存関係を追加します:

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.19.0

flutter:
  generate: true

2. l10n.yaml の設定

プロジェクトのルートディレクトリに l10n.yaml ファイルを作成し、以下の内容を追加します:

arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

3. ARB ファイルの作成

lib/l10n ディレクトリに app_en.arbapp_ja.arb ファイルを作成します:

app_en.arb:

{
  "helloWorld": "Hello World!",
  "welcomeMessage": "Welcome to Flutter!"
}

app_ja.arb:

{
  "helloWorld": "こんにちは、世界!",
  "welcomeMessage": "Flutterへようこそ!"
}

4. flutter gen-l10n の使用

arv ファイルを作成した後、 flutter gen-l10n コマンドを使用して、必要な Dart コードを生成します。

プロジェクトのルートディレクトリで以下のコマンドを実行します:

flutter gen-l10n

このコマンドは、l10n.yaml ファイルの設定に基づいて動作します。

生成されたファイル

コマンドを実行すると、通常以下のようなファイルが生成されます:

  • app_localizations.dart: メインの生成ファイルで、AppLocalizations クラスが含まれています。
  • app_localizations_en.dart, app_localizations_ja.dart: 各言語用の翻訳を含むファイル。

重要: 生成されたファイルの場所はプロジェクトの設定によって異なる場合があります。以下のような場所に生成されることがあります:

/your_project_root/.dart_tool/flutter_gen/gen_l10n/app_localizations_en.dart

生成後、プロジェクト内で生成されたファイルの正確な場所を確認することをお勧めします。

生成されたコードの使用

生成された AppLocalizations クラスを使用するには、以下のようにインポートします:

import 'package:flutter_gen/gen_l10n/app_localizations.dart';

そして、ウィジェット内で以下のように使用します:

Text(AppLocalizations.of(context)!.helloWorld)

注意点

  • ARB ファイルを変更するたびに flutter gen-l10n コマンドを再実行する必要があります。
  • 生成されたファイルはバージョン管理システム(例:Git)にコミットしないことをお勧めします。
  • プロジェクトの設定によっては、生成されたファイルの場所が異なる場合があります。必ず実際の生成場所を確認してください。
トラブルシューティング

flutter gen-l10n コマンドがエラーを返す場合:

  1. l10n.yaml ファイルの設定が正しいか確認してください。
  2. ARB ファイルが正しい形式で、指定された場所に存在するか確認してください。
  3. Flutter SDK が最新版であることを確認してください。
  4. 生成されたファイルの実際の場所を確認し、必要に応じてインポートパスを調整してください。

flutter gen-l10n コマンドは、多言語化プロセスの重要な部分です。このコマンドを適切に使用することで、アプリケーションの多言語化が大幅に簡素化され、効率的になります。ただし、生成されたファイルの場所が予期せぬところにある可能性があるため、常に実際の生成場所を確認することが重要です。

5. main.dart の設定

main.dart ファイルを以下のように更新します:

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: AppLocalizations.supportedLocales,
      locale: Locale('en'), // デフォルト言語を英語に設定
      localeResolutionCallback: (locale, supportedLocales) {
        if (locale == null) {
          return supportedLocales.first;
        }
        for (final supportedLocale in supportedLocales) {
          if (supportedLocale.languageCode == locale.languageCode) {
            return supportedLocale;
          }
        }
        return supportedLocales.first;
      },
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(AppLocalizations.of(context)!.helloWorld),
      ),
      body: Center(
        child: Text(AppLocalizations.of(context)!.welcomeMessage),
      ),
    );
  }
}

パート 2: Riverpod の導入

1. Riverpod の依存関係追加

pubspec.yaml ファイルに以下の依存関係を追加します:

dependencies:
  flutter_riverpod: ^2.1.3
  riverpod_annotation: ^2.0.2
  shared_preferences: ^2.0.15

dev_dependencies:
  build_runner: ^2.3.3
  riverpod_generator: ^2.1.4

2. LocaleProvider の実装

lib/providers/locale_provider.dart ファイルを作成し、以下の内容を追加します:

import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';

part 'locale_provider.g.dart';


class LocaleNotifier extends _$LocaleNotifier {
  static const String _kLanguageCode = 'languageCode';

  
  Locale build() {
    _loadLocale();
    return const Locale('en');
  }

  Future<void> _loadLocale() async {
    final prefs = await SharedPreferences.getInstance();
    final String languageCode = prefs.getString(_kLanguageCode) ?? 'en';
    state = Locale(languageCode);
  }

  Future<void> setLocale(Locale locale) async {
    if (!['en', 'ja'].contains(locale.languageCode)) return;
    state = locale;

    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_kLanguageCode, locale.languageCode);
  }
}

3. Riverpod のコード生成

Riverpod を使用することで、より簡潔で再利用しやすいコードを書くことができます。

build_runner の実行

LocaleProvider を作成した後、以下のコマンドを実行して Riverpod の生成コードを作成します:

dart run build_runner build --delete-conflicting-outputs

このコマンドは、@riverpod アノテーションが付いたクラスに対して必要なコードを生成します。

--delete-conflicting-outputs オプションの説明:

  • このオプションは、既存の生成ファイルと競合がある場合に、古いファイルを削除して新しいファイルを生成します。
  • 特に、コードの大幅な変更や、生成ファイルの手動修正を行った後に有用です。
  • ただし、生成ファイルに手動で加えた変更がある場合、それらの変更が失われる可能性があるので注意が必要です。
生成されるファイル

コマンドを実行すると、locale_provider.g.dart というファイルが生成されます。このファイルには、LocaleNotifier クラスの実装に必要な補助コードが含まれています。

注意点

  • コードを変更するたびに dart run build_runner build --delete-conflicting-outputs コマンドを再実行する必要があります。
  • 継続的に監視して自動生成したい場合は、以下のコマンドを使用できます:
    dart run build_runner watch
    
    このコマンドは変更を監視し、必要に応じて自動的にファイルを再生成します。
トラブルシューティング

生成に問題がある場合:

  1. pubspec.yaml ファイルに必要な依存関係(riverpod_generatorbuild_runner)が正しく記載されているか確認してください。
  2. part ディレクティブが正しく記述されているか確認してください(例:part 'locale_provider.g.dart';)。
  3. クラス名の前に @riverpod アノテーションが正しく付けられているか確認してください。
  4. 生成プロセスで問題が発生する場合は、プロジェクトをクリーンしてから再試行してください:
    flutter clean
    flutter pub get
    dart run build_runner build --delete-conflicting-outputs
    

Riverpod のコード生成を活用することで、ボイラープレートコードを減らし、より保守性の高いコードを書くことができます。また、コンパイル時の型チェックが強化され、ランタイムエラーのリスクを減らすことができます。--delete-conflicting-outputs オプションを使用することで、生成プロセスがよりクリーンになり、競合を避けることができます。

4. main.dart の更新

main.dart ファイルを以下のように更新します:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:your_app/providers/locale_provider.dart';

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final locale = ref.watch(localeNotifierProvider);

    return MaterialApp(
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: AppLocalizations.supportedLocales,
      locale: locale,
      localeResolutionCallback: (locale, supportedLocales) {
        if (locale == null) {
          return supportedLocales.first;
        }
        for (final supportedLocale in supportedLocales) {
          if (supportedLocale.languageCode == locale.languageCode) {
            return supportedLocale;
          }
        }
        return supportedLocales.first;
      },
      home: MyHomePage(),
    );
  }
}

5. 言語切り替えウィジェットの更新

LanguageSwitcher ウィジェットを以下のように更新します:

class LanguageSwitcher extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final locale = ref.watch(localeNotifierProvider);

    return DropdownButton<String>(
      value: locale.languageCode,
      items: [
        DropdownMenuItem(value: 'en', child: Text('English')),
        DropdownMenuItem(value: 'ja', child: Text('日本語')),
      ],
      onChanged: (String? languageCode) {
        if (languageCode != null) {
          ref.read(localeNotifierProvider.notifier).setLocale(Locale(languageCode));
        }
      },
    );
  }
}

主な設定の解説

  1. localizationsDelegates:

    • これらのデリゲートは、アプリケーションに必要な翻訳リソースを提供します。
    • AppLocalizations.delegate: アプリ固有の翻訳を提供します。
    • GlobalMaterialLocalizations.delegate: Material Design ウィジェットの標準的な翻訳を提供します。
    • GlobalWidgetsLocalizations.delegate: 基本的な Flutter ウィジェットの翻訳を提供します。
    • GlobalCupertinoLocalizations.delegate: iOS スタイルのウィジェットの翻訳を提供します。
  2. supportedLocales:

    • アプリケーションがサポートする言語とリージョンのリストを指定します。
    • AppLocalizations.supportedLocales を使用することで、ARB ファイルで定義したすべての言語が自動的に含まれます。
  3. locale:

    • 現在のアプリケーションの言語設定を指定します。
    • Riverpod の localeNotifierProvider から取得した値を使用しています。
  4. localeResolutionCallback:

    • デバイスの言語設定に基づいて、アプリケーションが使用する適切な言語を選択するためのコールバック関数です。
    • デバイスの言語がサポートされている場合はその言語を使用し、そうでない場合は最初のサポートされている言語を使用します。

新しい文字の追加

アプリケーションの開発が進むにつれて、新しい文字を追加する必要が出てきます。以下に、新しい文字を追加する手順を説明します。

1. ARB ファイルの更新

まず、lib/l10n ディレクトリ内の各言語の ARB ファイルに新しい文字を追加します。

例えば、「設定」という文字を追加する場合:

app_en.arb:

{
  "helloWorld": "Hello World!",
  "welcomeMessage": "Welcome to Flutter!",
  "settings": "Settings"
}

app_ja.arb:

{
  "helloWorld": "こんにちは、世界!",
  "welcomeMessage": "Flutterへようこそ!",
  "settings": "設定"
}

2. flutter gen-l10n の実行

ARB ファイルを更新したら、以下のコマンドを実行して翻訳ファイルを再生成します:

flutter gen-l10n

3. Riverpod 関連ファイルの再生成 (任意)

Riverpod を使用している場合、念のため以下のコマンドも実行します:

dart run build_runner build --delete-conflicting-outputs

4. 新しい文字の使用

生成されたファイルが更新されたら、新しい文字を以下のように使用できます:

Text(AppLocalizations.of(context)!.settings)

注意点

  • すべての言語の ARB ファイルに新しい文字を追加することを忘れないでください。
  • 文字キー(例:settings)は全ての ARB ファイルで同じである必要があります。
  • 大規模なプロジェクトでは、未翻訳の文字を追跡するシステムを導入することを検討してください。

効率的な作業のためのヒント

  1. ARB ファイルのテンプレート(通常は英語版)を最新の状態に保ち、他の言語のファイルはこれをベースに更新します。

  2. 翻訳作業を定期的に行い、大量の未翻訳文字が蓄積しないようにします。

  3. コンテキストや説明を ARB ファイルに含めることで、翻訳者がより正確に翻訳できるようサポートします:

    {
      "settings": "Settings",
      "@settings": {
        "description": "Label for the settings menu item"
      }
    }
    
  4. 必要に応じて、プレースホルダーを使用して動的な文字を作成します:

    {
      "greeting": "Hello, {name}!",
      "@greeting": {
        "description": "Greeting message with user's name",
        "placeholders": {
          "name": {
            "type": "String",
            "example": "John"
          }
        }
      }
    }
    

    使用例:

    Text(AppLocalizations.of(context)!.greeting('Alice'))
    

新しい翻訳文字を追加する際は、これらの手順に従うことで、アプリケーションの多言語化を効率的に管理できます。

効率化

生成されたファイルが更新されたら、新しい文字を使用できます。AppLocalizations のインスタンスを効率的に使用するには、以下のアプローチを推奨します:

class MyLocalizedWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    // AppLocalizations のインスタンスを一度取得
    final l10n = AppLocalizations.of(context)!;

    return Column(
      children: [
        Text(l10n.helloWorld),
        Text(l10n.welcomeMessage),
        Text(l10n.settings),
        // 他のローカライズされたテキスト
      ],
    );
  }
}

このアプローチの利点:

  1. コードの可読性が向上します。
  2. AppLocalizations.of(context)! の繰り返しを避けられます。
  3. パフォーマンスが僅かに向上する可能性があります(特に多数のローカライズされたテキストを使用する場合)。

注意点:

  • この方法は、同じ build メソッド内で複数のローカライズされたテキストを使用する場合に特に有効です。
  • 異なるウィジェットや、build メソッド外でローカライズされたテキストを使用する場合は、それぞれの場所で AppLocalizations.of(context)! を呼び出す必要があります。

プレースホルダーを含む文字の使用例:

Text(l10n.greeting('Alice'))

この方法を採用することで、コードはより簡潔になり、管理しやすくなります。特に多数のローカライズされたテキストを使用するウィジェットでは、この方法が効果的です。

参考リンク

https://docs.flutter.dev/development/accessibility-and-localization/internationalization
https://riverpod.dev/
https://api.flutter.dev/flutter/flutter_localizations/flutter_localizations-library.html
https://flutter.dev/docs/development/accessibility-and-localization/internationalization#adding-your-own-localized-messages

GitHubで編集を提案
株式会社ドットログ

Discussion