👨‍👨‍👧‍👧

Flutterで多言語対応なるものに挑戦してみた

に公開

そもそもどうやって言語を切り替えるのか?

多言語対応ってどんな方法がいいのかご質問をされました。僕もやったことないので参考になりそうな海外の動画をみながら学習して、多分いい感じのデモアプリを作れました。
この動画参考に色々と改良を加えました

https://www.youtube.com/watch?v=o5NngkDJcSQ&t=630s

こちらが完成したソースコード
https://github.com/sakurakotubaki/FlutterLocalizations

プロジェクトを作成してパッケージを追加する

https://pub.dev/packages/flutter_localization
https://pub.dev/packages/intl
https://pub.dev/packages/shared_preferences

必要なパッケージを追加するのですが、配置する位置に注意が必要です。以下のコードと同じにしてください。

pubspec.yaml
name: global_app
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter
  flutter_localization: ^0.1.10
  intl: ^0.18.0


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  shared_preferences: ^2.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true # 追加する。コマンドを使うのに必要
  generate: true
  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

多言語対応の設定をする

libディレクトリ配下に、l10nディレクトリを作成します。その中に、app_[言語].arbファイルを作成します。英語なら、app_en.arbとします。今回は3ヶ国語を使うので、以下のファイルを用意して、コマンド打つと設定ファイルが.dart_tool/flutter_gen/gen_l10nに、自動生成されます。

英語はこのファイル

app_en.arb
{
  "helloWorld": "Hello World!",
  "@helloWorld": {
    "description": "The conventional newborn programmer greeting"
  },
  "countMsg": "You have pushed the button this many times:",
  "@helloWorld": {
    "description": "You have pushed the button this many times:"
  }
  
}

ヒンディー語はこのファイル

app_hi.arb
{
  "helloWorld": "हैलो वर्ल्ड",
  "countMsg": "आपने यह बटन कई बार दबाया है:"
}

日本語はこのファイル

app_ja.arb
{
  "helloWorld": "こんにちは世界!",
  "countMsg": "これだけ何度もボタンを押しているのだから"
}

実行するコマンド
l10nディレクトリを指定して実行する

flutter gen-l10n

自動生成されたファイル

多言語対応の設定ファイル。

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;

import 'app_localizations_en.dart';
import 'app_localizations_hi.dart';
import 'app_localizations_ja.dart';

/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'gen_l10n/app_localizations.dart';
///
/// return MaterialApp(
///   localizationsDelegates: AppLocalizations.localizationsDelegates,
///   supportedLocales: AppLocalizations.supportedLocales,
///   home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
///   # Internationalization support.
///   flutter_localizations:
///     sdk: flutter
///   intl: any # Use the pinned version from flutter_localizations
///
///   # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, you’ll need to edit this
/// file.
///
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// project’s Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
  AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());

  final String localeName;

  static AppLocalizations of(BuildContext context) {
    return Localizations.of<AppLocalizations>(context, AppLocalizations)!;
  }

  static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();

  /// A list of this localizations delegate along with the default localizations
  /// delegates.
  ///
  /// Returns a list of localizations delegates containing this delegate along with
  /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
  /// and GlobalWidgetsLocalizations.delegate.
  ///
  /// Additional delegates can be added by appending to this list in
  /// MaterialApp. This list does not have to be used at all if a custom list
  /// of delegates is preferred or required.
  static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
    delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ];

  /// A list of this localizations delegate's supported locales.
  static const List<Locale> supportedLocales = <Locale>[
    Locale('en'),
    Locale('hi'),
    Locale('ja')
  ];

  /// You have pushed the button this many times:
  ///
  /// In en, this message translates to:
  /// **'Hello World!'**
  String get helloWorld;

  /// No description provided for @countMsg.
  ///
  /// In en, this message translates to:
  /// **'You have pushed the button this many times:'**
  String get countMsg;
}

class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
  const _AppLocalizationsDelegate();

  
  Future<AppLocalizations> load(Locale locale) {
    return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
  }

  
  bool isSupported(Locale locale) => <String>['en', 'hi', 'ja'].contains(locale.languageCode);

  
  bool shouldReload(_AppLocalizationsDelegate old) => false;
}

AppLocalizations lookupAppLocalizations(Locale locale) {


  // Lookup logic when only language code is specified.
  switch (locale.languageCode) {
    case 'en': return AppLocalizationsEn();
    case 'hi': return AppLocalizationsHi();
    case 'ja': return AppLocalizationsJa();
  }

  throw FlutterError(
    'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
    'an issue with the localizations generation tool. Please file an issue '
    'on GitHub with a reproducible sample app and the gen-l10n configuration '
    'that was used.'
  );
}

これは英語の設定ファイル

import 'app_localizations.dart';

/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
  AppLocalizationsEn([String locale = 'en']) : super(locale);

  
  String get helloWorld => 'Hello World!';

  
  String get countMsg => 'You have pushed the button this many times:';
}

アプリで使うロジック

端末に言語を切り替えて設定を保存するコードが書かれたファイルを作成する。

helper.dart
import 'package:shared_preferences/shared_preferences.dart';

class UserPreferences {
  static late SharedPreferences _preferences;

  static const _keyLanguage = 'language';

  static Future init() async =>
      _preferences = await SharedPreferences.getInstance();

  static Future setLanguage(String languageCode) async =>
      await _preferences.setString(_keyLanguage, languageCode);

  static String? getLanguage() => _preferences.getString(_keyLanguage);
}

アプリを実行するコード

AppBar右上のドロップダウンボタンを押すと、英語、ヒンディー語、日本語を選択することができます。選択した言語の設定は端末に保存できるので、アプリを停止しても状態を維持することができます。

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await UserPreferences.init();
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  MyApp({Key? key}) : super(key: key);

  
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Locale? _locale;

  
  void initState() {
    super.initState();
    _fetchLocale().then((locale) {
      setState(() {
        _locale = locale;
      });
    });
  }

  Future<Locale> _fetchLocale() async {
    var languageCode = UserPreferences.getLanguage();
    return Locale(languageCode ?? 'ja'); // デフォルトを日本語に設定します
  }

  void _changeLanguage(String languageCode) async {
    await UserPreferences.setLanguage(languageCode); // 言語設定を保存します
    var locale = await _fetchLocale();
    setState(() {
      _locale = locale;
    });
  }

  
  Widget build(BuildContext context) {
    if (_locale == null) {
      // Localeがまだ読み込まれていない場合は、ローディングスピナーを表示します
      return CircularProgressIndicator();
    } else {
      return MaterialApp(
        locale: _locale,
        localizationsDelegates: [
          AppLocalizations.delegate,
          GlobalMaterialLocalizations.delegate,
        ],
        supportedLocales: [
          Locale('en'),
          Locale('hi'),
          Locale('ja'),
        ],
        home: MyHomePage(
          title: 'Flutter Demo Home Page',
          onLanguageChanged: _changeLanguage,
        ),
      );
    }
  }
}

class MyHomePage extends StatefulWidget {
  final String title;
  final ValueChanged<String> onLanguageChanged;

  MyHomePage({Key? key, required this.title, required this.onLanguageChanged})
      : super(key: key);

  
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.amber,
        title: Text(AppLocalizations.of(context).helloWorld),
        actions: [
          DropdownButton<String>(
            items: [
              DropdownMenuItem(value: "en", child: Text("English")),
              DropdownMenuItem(value: "ja", child: Text("日本語")),
              DropdownMenuItem(value: "hi", child: Text("हिंदी")),
            ],
            onChanged: (value) {
              widget.onLanguageChanged(value!);
            },
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              AppLocalizations.of(context).countMsg,
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineLarge,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

まとめ

色々試してみて分かったのですが、今のところintl: ^0.18.0でないとエラー起きるらしいです!
他にも多言語対応する方法を検討中です。個人的にarbファイルなるものが好みでないですね笑
Riverpodで書いてみたコードもあるので、興味ある方いたら見てください。

Riverpod2.0で書いてみたコード
https://github.com/sakurakotubaki/FlutterLocalizations/tree/future/riverpod

Discussion