🌎

Flutterアプリでslangを使用した18n対応

2024/09/23に公開

英語と日本語の切り替えを実装する

👤対象者

  • Flutterでアプリ開発をしている方
  • アプリの多言語対応を検討している方
  • slangパッケージに興味がある方

完成品のソースコードはこちら
https://x.com/JBOY83062526/status/1837987141307216245

📕Overview

slangは、Flutterアプリケーションで効率的に多言語対応(i18n)を実装するためのパッケージです。本記事では、slangを使用して英語と日本語の切り替えを実装する方法を解説します。

🧷summary

  1. slangの特徴と利点
  2. slangのセットアップ方法
  3. 翻訳ファイルの作成と管理
  4. Flutterアプリでの言語切り替えの実装
  5. slangを使用する際のベストプラクティス

🤔やってみたいこと

  • slangを使用してFlutterアプリに英語と日本語の切り替え機能を実装する
  • 効率的な翻訳管理と使いやすいAPIを実現する

🚀やってみたこと

  1. slangのインストールとセットアップ

こちらは手入力で追加しないと正しい位置に配置できないのでコピー&ペーストしてください。

setup
name: localization_app
description: "A new Flutter project."
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: '>=3.4.4 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations: # add this
    sdk: flutter
    
  cupertino_icons: ^1.0.6
  slang: ^3.31.2
  slang_flutter: ^3.31.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.4.11
  slang_build_runner: ^3.31.0

  flutter_lints: ^3.0.0

flutter:

  uses-material-design: 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
  1. 翻訳ファイルの作成(例:lib/i18n/strings.i18n.yaml
lib/i18n
├── strings.g.dart
├── strings_en.i18n.json
└── strings_ja.i18n.json

strings_en.i18n.json:

{
    "mainScreen": {
      "title": "Japanese Title",
      "counter": {
        "one": "I pressed the button $n times.",
        "other": "I pressed the button $n times."
      },
      "tapMe": "Push it"
    },
    "locales(map)": {
      "en": "English",
      "ja": "Japanese"
    }
  }

strings_ja.i18n.json:

{
  "mainScreen": {
    "title": "日本語のタイトル",
    "counter": {
      "one": "ボタンを$n回押しました。",
      "other": "ボタンを$n回押しました。"
    },
    "tapMe": "押してね"
  },
  "locales(map)": {
    "en": "英語",
    "ja": "日本語"
  }
}
  1. コード生成の実行
    Built-in (recommended during development):
dart run slang 

Alternative (useful for CI and initial git checkout, requires slang_build_runner):

dart run build_runner build -d

iOSで使う場合は設定がいるのでios/Runner/Info.plistに設定を追加する。

Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>$(DEVELOPMENT_LANGUAGE)</string>
	<key>CFBundleDisplayName</key>
	<string>Localization App</string>
	<key>CFBundleExecutable</key>
	<string>$(EXECUTABLE_NAME)</string>
	<key>CFBundleIdentifier</key>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>localization_app</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>$(FLUTTER_BUILD_NAME)</string>
	<key>CFBundleSignature</key>
	<string>????</string>
	<key>CFBundleVersion</key>
	<string>$(FLUTTER_BUILD_NUMBER)</string>
	<key>LSRequiresIPhoneOS</key>
	<true/>
	<key>UILaunchStoryboardName</key>
	<string>LaunchScreen</string>
	<key>UIMainStoryboardFile</key>
	<string>Main</string>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UISupportedInterfaceOrientations~ipad</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>CFBundleLocalizations</key>
	<!-- n18i -->
	<array>
		<string>en</string>
		<string>ja</string>
	</array>
	<key>CADisableMinimumFrameDurationOnPhone</key>
	<true/>
	<key>UIApplicationSupportsIndirectInputEvents</key>
	<true/>
</dict>
</plist>
  1. Flutterアプリでの使用例

    import 'package:flutter/material.dart';
    import 'package:your_app/i18n/strings.g.dart';
    
    class MyHomePage extends StatelessWidget {
      
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(t.appTitle),
          ),
          body: Center(
            child: Text(t.welcomeMessage),
          ),
        );
      }
    }
    
  2. 言語切り替えの実装

    ElevatedButton(
      onPressed: () {
        LocaleSettings.setLocale(AppLocale.ja);
      },
      child: Text('日本語に切り替え'),
    )
    

今回作成したサンプル

AppBarにも言語を切り替えるボタンをつけてみたのですがこれが面白い機能でPopupMenuButtonを使用してBuildContextに変更を加えるとアプリ全体に日本語から英語に変更した状態を伝えることができるようです。

itemBuilderを使うとボタンが押されたときに、メニューに表示する項目を作成することができるそうです。

contextに変更があるとWidgetTreeの仕組みなのかアプリ全体の状態が変化してしまうようです。contextという単語は「文脈」と表現されたり「今の状態」という意味だそうです。

setState呼ばなくもできるようだ...

example
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:localization_app/i18n/strings.g.dart';
import 'package:localization_app/settings_page.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  LocaleSettings.useDeviceLocale(); // initialize with the right locale
  runApp(TranslationProvider(
    // wrap with TranslationProvider
    child: const MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      locale: TranslationProvider.of(context).flutterLocale,
      supportedLocales: AppLocaleUtils.supportedLocales,
      // error GlobalMaterialLocalizations
      localizationsDelegates: GlobalMaterialLocalizations.delegates,
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  
  _MyHomePageState createState() => _MyHomePageState();
}

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

  
  void initState() {
    super.initState();

    LocaleSettings.getLocaleStream().listen((event) {
      print('locale changed: $event');
    });
  }

  
  Widget build(BuildContext context) {
    // get t variable, will trigger rebuild on locale change
    // otherwise just call t directly (if locale is not changeable)
    final t = Translations.of(context);

    return Scaffold(
      appBar: AppBar(
        // change language
        actions: [
          PopupMenuButton<AppLocale>(
            icon: const Icon(Icons.language),
            onSelected: (AppLocale locale) {
              LocaleSettings.setLocale(locale);
            },
            itemBuilder: (BuildContext context) {
              return AppLocale.values.map((AppLocale locale) {
                return PopupMenuItem<AppLocale>(
                  value: locale,
                  child: Text(t.locales[locale.languageTag]!),
                );
              }).toList();
            },
          ),
        ],
        title: Text(t.mainScreen.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // error mainScreen
            Text(t.mainScreen.counter(n: _counter)),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,

              // lets loop over all supported locales
              children: AppLocale.values.map((locale) {
                // active locale
                AppLocale activeLocale = LocaleSettings.currentLocale;

                // typed version is preferred to avoid typos
                bool active = activeLocale == locale;

                return Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: OutlinedButton(
                    style: OutlinedButton.styleFrom(
                      backgroundColor: active ? Colors.blue.shade100 : null,
                    ),
                    onPressed: () {
                      // locale change, will trigger a rebuild (no setState needed)
                      LocaleSettings.setLocale(locale);
                    },
                    // error mainScreen
                    child: Text(t.locales[locale.languageTag]!),
                  ),
                );
              }).toList(),
            ),
            const SizedBox(height: 30),
            ElevatedButton(onPressed: () {
              Navigator.of(context).push(
                MaterialPageRoute(builder: (_) => const SettingsPage())
              );
            }, child: const Text('next')),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() => _counter++);
        },
        // error mainScreen
        tooltip: context.t.mainScreen.tapMe, // using extension method
        child: const Icon(Icons.add),
      ),
    );
  }
}

💡Tips

  • プレースホルダーを使用して、動的なテキストを効率的に扱う
  • コンテキストベースの翻訳を活用して、より自然な翻訳を実現する

プレースホルダー(英:placeholder)とは、あとから入力される文字・値の代わりに、仮に入力されている文字や値のことです。今回だとn18iのjsonファイルですかね。

🧑‍🎓thoughts

slangを使用することで、型安全な翻訳管理と効率的な多言語対応が可能になります。特に大規模なプロジェクトや、頻繁に翻訳を更新する必要があるアプリケーションで威力を発揮します。

🤔Think

  • slangと他の多言語対応ソリューション(例:intl、easy_localization)との比較
  • パフォーマンスへの影響と最適化の方法
  • 大規模プロジェクトでのslangの活用方法

easy_localizationなるものがあるそうですが、このパッケージだともの足りないそうだ。サービスがスケールするとslangを使った方が要件を満たすことができるようだ。

最後に

slangを使用することで、Flutterアプリの多言語対応がより簡単かつ効率的になります。本記事で紹介した方法を参考に、ぜひ皆さんのプロジェクトでも活用してみてください。

以前記事書いたことあるのですが内容がビミョーだった💦
https://zenn.dev/joo_hashi/articles/774c071a76edf8

補足情報

Discussion