🌐

Flutterの多言語対応(i18n):slangを使った効率的な実装

に公開

Flutter アプリケーションを世界中のユーザーに届けるためには、多言語対応(国際化・i18n)が欠かせません。Flutterにおける多言語対応で「slang」というパッケージが導入が楽でパフォーマンスも良さそうでした!今回はその実装方法をご紹介します

i18nとは?

「i18n」(internationalization)とは、アプリケーションを特定の言語や地域に依存せず、様々な言語や文化に対応できるように設計することです。「i」と「n」の間に18文字あることから、この略称が生まれました。

i18nの基本的な仕組み

i18nの基本的な仕組みは、以下のようなステップで実現されます:

  1. テキストの分離: UIから直接テキストを切り離し、言語ごとのリソースファイルに保存します
  2. 言語検出: ユーザーのデバイス設定または明示的な選択に基づいて使用する言語を決定します
  3. テキスト読み込み: 決定した言語のリソースファイルからテキストを読み込みます
  4. 表示: 読み込んだテキストをUIに表示します

例えば、「こんにちは」というテキストを英語と日本語で表示したい場合:

// 英語のリソースファイル
"greeting": "Hello"

// 日本語のリソースファイル
"greeting": "こんにちは"

アプリはユーザーの言語設定に応じて適切なファイルからテキストを取得し表示します。

Flutterにおけるi18nの特徴

Flutterでは、公式の「flutter_localizations」パッケージとARBファイル形式を使用した国際化がサポートされています。しかし、「slang」のような第三者製ライブラリも存在し、より効率的な実装が可能です。

Flutter公式のi18n実装方法

まず、Flutter公式の国際化実装方法について見てみましょう。

flutter_localizationsの基本

flutter_localizationsは、Flutterが提供する公式の国際化サポートパッケージです。このパッケージは以下の機能を提供します:

  • 言語ごとのリソース管理
  • 日付、数値、通貨などのフォーマット
  • 右から左へ書く言語(RTL)のサポート
  • Flutter標準ウィジェット(日付選択など)の翻訳

flutter_localizationsの実装手順

  1. 依存関係の追加:
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.18.0 #ここは最新バージョンを
  1. 言語リソース(ARBファイル)の作成:
// lib/l10n/app_en.arb(英語)
{
  "hello": "Hello",
  "welcomeMessage": "Welcome, {name}",
  "@welcomeMessage": {
    "description": "A welcome message",
    "placeholders": {
      "name": {}
    }
  }
}

// lib/l10n/app_ja.arb(日本語)
{
  "hello": "こんにちは",
  "welcomeMessage": "ようこそ、{name}さん"
}
  1. l10nの設定(pubspec.yaml):
flutter:
  generate: true
  uses-material-design: true
  
  # 国際化設定
  l10n:
    arb-dir: lib/l10n
    template-arb-file: app_en.arb
    output-localization-file: app_localizations.dart
  1. MaterialAppの設定:
return MaterialApp(
  // 言語委譲
  localizationsDelegates: const [
    AppLocalizations.delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  // サポート言語
  supportedLocales: const [
    Locale('en'), // 英語
    Locale('ja'), // 日本語
  ],
  // ...
);
  1. 翻訳の使用方法:
// コンテキストから翻訳を取得
final localizations = AppLocalizations.of(context)!;

// 単純なテキスト
Text(localizations.hello);

// パラメータ付きテキスト
Text(localizations.welcomeMessage('ユーザー'));

slangを使った効率的なi18n実装

次に、slangを使った国際化実装方法を見ていきましょう。

slangを使う理由

slangは以下の理由から、Flutterにおける最も効率的な国際化ライブラリの一つです:

  1. 型安全性: コンパイル時のチェックにより、タイプミスやキー不足によるエラーを防ぎます
  2. 豊富なデータ構造: リスト、マップ、ネストされた構造など、様々なデータ形式をサポートします
  3. 高速なアクセス: 解析が不要で、ネイティブのDartメソッド呼び出しを使用するため高速です
  4. 柔軟なファイル形式: JSON、YAML、CSV、ARBなど複数のファイル形式に対応しています
  5. コードの生成: 翻訳キーから自動的にDartコードを生成するため、編集時のサポートが充実しています

slangとflutter_localizationsの併用メリット

slangは単独でも強力ですが、flutter_localizationsと併用することでより完全なi18n環境を構築できます:

  1. slangの型安全な翻訳アクセス + flutter_localizationsの標準ウィジェット対応
  2. slangの効率的なデータ構造 + flutter_localizationsの数値/日付フォーマット
  3. slangの開発効率 + flutter_localizationsのRTLサポート

slangの実装手順

  1. 依存関係の追加:
dependencies:
  flutter:
    sdk: flutter
  slang: ^4.5.0
  slang_flutter: ^4.5.0
  flutter_localizations:
    sdk: flutter

dev_dependencies:
  slang_build_runner: ^4.5.0
  build_runner: ^2.4.6
  1. 翻訳ファイルの作成:
// lib/i18n/en.i18n.json(英語)
{
  "app": {
    "title": "My App"
  },
  "home": {
    "welcome": "Welcome, $name!",
    "items": {
      "zero": "No items",
      "one": "One item",
      "other": "$count items"
    }
  }
}

// lib/i18n/ja.i18n.json(日本語)
{
  "app": {
    "title": "マイアプリ"
  },
  "home": {
    "welcome": "ようこそ、$nameさん!",
    "items": {
      "zero": "アイテムはありません",
      "one": "1つのアイテム",
      "other": "$count個のアイテム"
    }
  }
}
  1. コード生成の実行:
dart run slang

または

dart run build_runner build
  1. MaterialAppの設定:
return MaterialApp(
  // slangの設定
  locale: LocaleSettings.currentLocale.flutterLocale,
  
  // flutter_localizationsの設定
  localizationsDelegates: const [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: AppLocale.values.map((e) => e.flutterLocale),
  // ...
);
  1. 翻訳の使用方法:
// グローバル変数を使用(最も簡単)
Text(t.home.welcome(name: 'ユーザー'));

// コンテキストを使用
Text(context.t.home.welcome(name: 'ユーザー'));

// 複数形
Text(t.home.items(count: 5));

実装例:slangを使った完全な多言語対応アプリ

実際のコード例を見ていきましょう。この例はslangを使った多言語対応アプリの基本構造を示しています。

プロジェクト構造

lib/
 ├── i18n/
 │   ├── en.i18n.json  # 英語の翻訳
 │   ├── ja.i18n.json  # 日本語の翻訳
 │   └── strings.g.dart # 自動生成されるコード
 ├── core/
 │   └── services/
 │       └── locale_preferences.dart # 言語設定の保存
 └── main.dart

翻訳ファイル

lib/i18n/en.i18n.json:

{
  "app": {
    "title": "My App"
  },
  "common": {
    "ok": "OK",
    "cancel": "Cancel",
    "save": "Save"
  },
  "auth": {
    "login": "Login",
    "signup": "Sign Up",
    "email": "Email",
    "password": "Password"
  },
  "settings": {
    "title": "Settings",
    "language": "Language",
    "theme": "Theme",
    "dark": "Dark Mode",
    "light": "Light Mode"
  },
  "languages": {
    "en": "English",
    "ja": "Japanese"
  }
}

lib/i18n/ja.i18n.json:

{
  "app": {
    "title": "マイアプリ"
  },
  "common": {
    "ok": "OK",
    "cancel": "キャンセル",
    "save": "保存"
  },
  "auth": {
    "login": "ログイン",
    "signup": "新規登録",
    "email": "メールアドレス",
    "password": "パスワード"
  },
  "settings": {
    "title": "設定",
    "language": "言語",
    "theme": "テーマ",
    "dark": "ダークモード",
    "light": "ライトモード"
  },
  "languages": {
    "en": "英語",
    "ja": "日本語"
  }
}

言語設定の保存サービス

lib/core/services/locale_preferences.dart:

import 'package:shared_preferences/shared_preferences.dart';
import 'package:myapp/i18n/strings.g.dart';

class LocalePreferences {
  static const String _localeKey = 'locale';

  // 選択された言語を保存
  static Future<void> saveLocale(AppLocale locale) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_localeKey, locale.languageTag);
  }

  // 保存された言語を読み込み
  static Future<AppLocale?> getSavedLocale() async {
    final prefs = await SharedPreferences.getInstance();
    final localeString = prefs.getString(_localeKey);
    
    if (localeString == null) return null;
    
    try {
      // 言語タグからAppLocaleを取得
      return AppLocaleUtils.fromTag(localeString);
    } catch (_) {
      return null;
    }
  }
}

アプリケーションのメインファイル

lib/main.dart:

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:myapp/core/services/locale_preferences.dart';
import 'package:myapp/i18n/strings.g.dart';

void main() async {
  // Flutter初期化
  WidgetsFlutterBinding.ensureInitialized();
  
  // 保存された言語設定を読み込み
  final savedLocale = await LocalePreferences.getSavedLocale();
  if (savedLocale != null) {
    LocaleSettings.setLocale(savedLocale);
  } else {
    // デバイスの言語設定を使用
    LocaleSettings.useDeviceLocale();
  }
  
  runApp(
    TranslationProvider(
      child: const MyApp(),
    ),
  );
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: t.app.title,
      
      // 言語設定
      locale: LocaleSettings.currentLocale.flutterLocale,
      localizationsDelegates: const [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: AppLocale.values.map((e) => e.flutterLocale),
      
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

ホーム画面

lib/pages/home_page.dart:

import 'package:flutter/material.dart';
import 'package:myapp/i18n/strings.g.dart';
import 'package:myapp/pages/settings_page.dart';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(t.app.title),
        actions: [
          IconButton(
            icon: const Icon(Icons.settings),
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (_) => const SettingsPage()),
              );
            },
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // パラメータを含む翻訳
            Text(
              t.auth.login,
              style: Theme.of(context).textTheme.headline4,
            ),
            const SizedBox(height: 20),
            
            // パラメータを含む翻訳
            Text(
              t.auth.signup,
              style: Theme.of(context).textTheme.subtitle1,
            ),
          ],
        ),
      ),
    );
  }
}

設定画面(言語切り替え機能付き)

lib/pages/settings_page.dart:

import 'package:flutter/material.dart';
import 'package:myapp/core/services/locale_preferences.dart';
import 'package:myapp/i18n/strings.g.dart';

class SettingsPage extends StatelessWidget {
  const SettingsPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(t.settings.title),
      ),
      body: ListView(
        children: [
          // 言語設定
          ListTile(
            leading: const Icon(Icons.language),
            title: Text(t.settings.language),
            onTap: () {
              _showLanguageDialog(context);
            },
          ),
          
          // テーマ設定(例)
          ListTile(
            leading: const Icon(Icons.color_lens),
            title: Text(t.settings.theme),
            onTap: () {
              // テーマ設定画面への遷移
            },
          ),
        ],
      ),
    );
  }
  
  // 言語選択ダイアログを表示
  void _showLanguageDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(t.settings.language),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            _buildLanguageOption(
              context, 
              AppLocale.en, 
              t.languages.en,
            ),
            _buildLanguageOption(
              context, 
              AppLocale.ja, 
              t.languages.ja,
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text(t.common.cancel),
          ),
        ],
      ),
    );
  }

  Widget _buildLanguageOption(
    BuildContext context,
    AppLocale locale,
    String languageName,
  ) {
    final isSelected = LocaleSettings.currentLocale == locale;
    
    return ListTile(
      title: Text(languageName),
      trailing: isSelected ? Icon(Icons.check, color: Theme.of(context).primaryColor) : null,
      onTap: () {
        // 言語を変更
        LocaleSettings.setLocale(locale);
        
        // 設定を保存
        LocalePreferences.saveLocale(locale);
        
        // ダイアログを閉じる
        Navigator.of(context).pop();
      },
    );
  }
}

備考:slangでできる高度な機能

slangには、基本的な翻訳機能以外にも多くの高度な機能があります。

1. リッチテキスト

テキスト内の一部を異なるスタイルで表示できます。

{
  "terms": {
    "agreement(rich)": "利用規約に$link(同意)する"
  }
}
Text.rich(t.terms.agreement(
  link: (text) => TextSpan(
    text: text,
    style: TextStyle(color: Colors.blue),
    recognizer: TapGestureRecognizer()..onTap = () {
      // タップ時の処理
    },
  ),
)),

2. 複数形対応

数量に応じて異なるテキストを表示できます。

{
  "notification": {
    "zero": "通知はありません",
    "one": "1件の通知があります",
    "other": "$count件の通知があります"
  }
}
Text(t.notification(count: 0)), // "通知はありません"
Text(t.notification(count: 1)), // "1件の通知があります"
Text(t.notification(count: 5)), // "5件の通知があります"

3. コンテキスト依存翻訳

性別など、特定のコンテキストに基づいた翻訳が可能です。

{
  "greeting(context=Gender)": {
    "male": "こんにちは、$nameくん",
    "female": "こんにちは、$nameさん"
  }
}
Text(t.greeting(name: '太郎', context: Gender.male)),
Text(t.greeting(name: '花子', context: Gender.female)),

4. 数値・日付のフォーマット

異なる言語や地域に応じた数値や日付のフォーマットを実現できます。

{
  "price": "価格: {amount: currency}",
  "date": "日付: {day: yMd}"
}
Text(t.price(amount: 1234.56)), // "価格: ¥1,234"(日本語環境)
Text(t.date(day: DateTime.now())), // "日付: 2025/3/1"(日本語環境)
株式会社Xronotech

Discussion