[Flutter]日本語アプリでのロケール設定(中華フォント対策)

Flutter

Flutterでアプリを作っていると、TextWidgetを使って文字を表示することが必ずあると思います。
英語の表示だけで済めば何の問題もありませんが、日本語を表示させたときに変な漢字が表示されたことはありませんか?
いわゆる中華フォントと言われていますが、これはフォントだけの問題ではありません。

Flutterの公式ドキュメントに以下の記載があります。

By default, Flutter only provides US English localizations. To add support for other languages, an application must specify additional MaterialApp (or CupertinoApp) properties, and include a package called flutter_localizations.
Internationalizing Flutter apps

デフォルトでは、Flutter は米国英語のローカライズのみを提供します。 他の言語のサポートを追加するには、アプリケーションで追加の MaterialApp (または CupertinoApp) プロパティを指定し、flutter_localizations というパッケージを含める必要があります。

ということなので、Flutterで英語以外の言語を使う場合は、MaterialAppに国際化の設定をしましょう

ちなみに、Flutterでの標準フォントはAndroidならRoboto、iOSならSan Francisco(SF Pro)で、どちらも欧文フォントです。つまり、Robotoだから変な漢字が表示されるというのは間違いです。
(Robotoしか使われていないのなら、変な漢字すら表示されません)

環境

  • Windows11 Home 21H2
  • Visual Studio Code 1.73
  • Flutter
PS D:\Flutter\locale_test> flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.3.6, on Microsoft Windows [Version 10.0.22000.1098], locale ja-JP)
[√] Android toolchain - develop for Android devices (Android SDK version 33.0.0-rc3)
[√] Chrome - develop for the web
[√] Visual Studio - develop for Windows (Visual Studio Community 2022 17.3.5)
[√] Android Studio (version 2021.3)
[√] VS Code (version 1.73.0)
[√] Connected device (4 available)
[√] HTTP Host Availability

• No issues found!
PS D:\Flutter\locale_test>

変な漢字になってしまう例

とりあえず、DataTableWidgetを使って、Androidエミュレータで日本語としては変な漢字になる単語を表示します。

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    const String title = 'Locale Demo';

    return MaterialApp(
      title: title,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: title),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  Widget build(BuildContext context) {
    const String bodyText = '写真\n納豆\n反対\n新聞紙\n直角\n化学\n編集';
    debugPrint('TextStyle: ${Theme.of(context).textTheme.bodyText2}');

    final List<String> listText = bodyText.split('\n');
    final listRow = List<DataRow>.generate(listText.length, (index) {
      return DataRow(cells: [
        DataCell(Text(
          listText[index],
          style: const TextStyle(fontSize: 40),
        )),
      ]);
    });

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: DataTable(
        dataRowHeight: 70,
        columns: [
          DataColumn(label: Text('${Localizations.localeOf(context)}')),
        ],
        rows: listRow,
      ),
    );
  }
}

端末の言語設定を「日本語」に設定したとしても、こんな感じで表示されると思います。

列名に現在のロケール名(en_US)を表示するようにしましたが、日本語として見ると、とても変な漢字が表示されてしまいます。

アプリ全体のロケール設定

アプリ全体のロケールを日本語に設定するにはflutter_localizationsというパッケージを使います。
このパッケージは公式ドキュメントにある国際化対応で使われているものです。

pubspec.yamlの編集

pubspec.yamlを以下のとおり修正します。

dependencies:
    flutter:
        sdk: flutter
    flutter_localizations: # 追加
        sdk: flutter       # 追加

pub getの実行

コマンドラインでflutter pub getを実行します。
(VSCodeだと保存すれば勝手にflutter pub getしてくれるはず)

ソースコードの編集

ソースコードを以下のとおり修正します。

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; // 追加

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

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

  @override
  Widget build(BuildContext context) {
    const String title = 'Locale Demo';

    return MaterialApp(
      localizationsDelegates: const [ // 追加
        GlobalMaterialLocalizations.delegate, // 追加
        GlobalWidgetsLocalizations.delegate, // 追加
        GlobalCupertinoLocalizations.delegate, // 追加
      ], // 追加
      supportedLocales: const [Locale('ja', 'JP')], // 追加
      title: title,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: title),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  Widget build(BuildContext context) {
    const String bodyText = '写真\n納豆\n反対\n新聞紙\n直角\n化学\n編集';
    debugPrint('TextStyle: ${Theme.of(context).textTheme.bodyText2}');

    final List<String> listText = bodyText.split('\n');
    final listRow = List<DataRow>.generate(listText.length, (index) {
      return DataRow(cells: [
        DataCell(Text(
          listText[index],
          style: const TextStyle(fontSize: 40),
        )),
      ]);
    });

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: DataTable(
        dataRowHeight: 70,
        columns: [
          DataColumn(label: Text('${Localizations.localeOf(context)}')),
        ],
        rows: listRow,
      ),
    );
  }
}

これだけで列名のロケールもja_JPに変わり、日本人が見慣れた漢字が表示されます。

ソースコードの説明

日本語ロケールの設定はflutter_localizations.dartのインポートと、MaterialAppで6行追加するだけです。

  • localizationsDelegatesオプション
    ローカライズするWidget群を指定します。
    とりあえず、GlobalMaterialLocalizations.delegateGlobalCupertinoLocalizations.delegateGlobalWidgetsLocalizations.delegateの3つを追加しましょう。

    MaterialAppを使ってるからGlobalCupertinoLocalizations.delegateはいらないだろうと思って消したらエラーになりました。

  • supportedLocalesオプション
    対応するロケールを指定します。
    ここでは日本語アプリなので、Locale('ja', 'JP')となります。

TextStyleの確認

VSCodeで日本語ロケール設定前のソースコードを実行すると、デバッグコンソールに以下の内容が出力されると思います。

I/flutter (11487): TextStyle: TextStyle(debugLabel: (englishLike bodyMedium 2014).merge(blackMountainView bodyMedium), inherit: false, color: Color(0xdd000000), family: Roboto, size: 14.0, weight: 400, baseline: alphabetic, decoration: TextDecoration.none)

これは、Scaffoldのbodyで使われているデフォルトのTextStyleであるbodyText2の内容を表示したものです。

ここで見てほしいのが、debugLabelにあるenglishLikeです。
TextStyleはMaterialAppがいい感じに決めてくれます。それはとても有難いのですが、デフォルトのロケール(en_US)だとenglishLikeという値になってしまいます。
englishLikeというのは英語・フランス語・ロシア語などラテン文字やキリル文字での設定です。

ScriptCategory言語
englishLike英語、フランス語、ロシア語 etc…
tallペルシア語、ヒンディー語、タイ語
dense中国語、日本語、韓国語

一方、日本語ロケール設定後のソースコードを実行すると、デバッグコンソールに以下の内容が表示されると思います。

I/flutter (12067): TextStyle: TextStyle(debugLabel: (dense bodyMedium 2014).merge(blackMountainView bodyMedium), inherit: false, color: Color(0xdd000000), family: Roboto, size: 15.0, weight: 400, baseline: ideographic, decoration: TextDecoration.none)

debugLabelenglishLikeだったところがdenseになっていることが分かります。
denseは日本語・中国語・韓国語の設定で、englishLikeよりフォントサイズが大きくなったり(14.0から15.0)、ベースラインが変わったり(alphabeticからideographic)しています。
ロケール設定をすることで、Materialデザインでの日本語表示の調整もされていることが分かります。

アプリ全体のロケールを日本語に変更した場合、TypographyのScriptCategoryはすべてdenseになります。このため、ロケール設定をする前と後ではアプリのデザインが変わってしまう可能性があります。

Textウィジェット個別でのロケール設定

ここまで説明してきた「アプリ全体のロケール設定」をしなくても、TextWidget個別でロケール設定をすることも可能です。
以下のとおり、TextWidgetのlocaleパラメータにLocale('ja', 'JP')を追加するだけです。

Text(
  listText[index],
  style: const TextStyle(fontSize: 40),
  locale: Locale('ja', 'JP'),
)

1つのアプリで日本語と中国語を表示させたい場合には、この方法で対応する必要があります。

ソースは以下のとおりです。
40行目~45行目にDataCellを1つ追加して、TextWidgetにlocaleパラメータを追加しました。そして、56行目に列名を追加しました。

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    const String title = 'Locale Demo';

    return MaterialApp(
      title: title,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: title),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  Widget build(BuildContext context) {
    const String bodyText = '写真\n納豆\n反対\n新聞紙\n直角\n化学\n編集';
    debugPrint('TextStyle: ${Theme.of(context).textTheme.bodyText2}');

    final List<String> listText = bodyText.split('\n');
    final listRow = List<DataRow>.generate(listText.length, (index) {
      return DataRow(cells: [
        DataCell(Text(
          listText[index],
          style: const TextStyle(fontSize: 40),
        )),
        DataCell(Text(
          listText[index],
          style: const TextStyle(fontSize: 40),
          locale: const Locale('ja', 'JP'),
        )),
      ]);
    });

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: DataTable(
        dataRowHeight: 70,
        columns: [
          DataColumn(label: Text('${Localizations.localeOf(context)}')),
          const DataColumn(label: Text('ja_JP')),
        ],
        rows: listRow,
      ),
    );
  }
}

コメント

タイトルとURLをコピーしました