Widgetbook+SlangでLocaleを変更



## はじめに

仕事でFlutterにローカリゼーションとWidgetbookの両方を導入することになりました。
それぞれは問題なく導入できて、よっしゃー！と思っていたのですがWidgetbookのAddonに存在するLocaleを変更しても言語が変更されませんでした。調べたところ、Widgetのデフォルトの`LocalizationAddon`はFlutterネイティブでLocaleを変更する方式であればうまく動くようですが、今回は[slang][slang]を使ってるので動いてないっぽい。じゃあ直すか！ということで色々調べ、問題を解決したので共有です。

## 対象者

- Flutterで開発をしている人
- アプリの多言語化に[slang][slang]を使用している人
- [widgetbook][widgetbook]を導入してページやWidgetをカタログ化したい人

## 前提条件

- ここでは[widgetbook][widgetbook]や[slang][slang]の説明はしません
- ローカルに[slang][slang]と[widgetbook][widgetbook]の両方がすでに入ってる
- [slang][slang]で多言語化を行っている
- [widgetbook][widgetbook]の設定は以下

```dart
// widgetbook.dart

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;

// Import the generated directories variable
import 'widgetbook.directories.g.dart';

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

@widgetbook.App()
class WidgetbookApp extends StatelessWidget {
  const WidgetbookApp({super.key});

  @override
  Widget build(BuildContext context) {
    return Widgetbook.material(
      // Use the generated directories variable
      appBuilder: (context, child) {
        return ProviderScope(child: child);
      },
      directories: directories,
      addons: <WidgetbookAddon>[
        MaterialThemeAddon(themes: [
          WidgetbookTheme(name: "Light", data: ThemeData.light()),
          WidgetbookTheme(name: "Dark", data: ThemeData.dark()),
        ]),
        DeviceFrameAddon(devices: [
          Devices.ios.iPhoneSE,
          Devices.ios.iPhone13,
          Devices.ios.iPhone13Mini,
          Devices.ios.iPhone13ProMax,
        ]),
        TextScaleAddon(
          scales: <double>[1.0, 2.0],
        ),
        LocalizationAddon(
          locales: <Locale>[
            const Locale('en', 'US'),
          ],
          localizationsDelegates: <LocalizationsDelegate<dynamic>>[
            DefaultWidgetsLocalizations.delegate,
            DefaultMaterialLocalizations.delegate,
          ],
        ),
      ],
      integrations: const [],
    );
  }
}
```

いくつかカスタマイズしてますが、基本的にはデフォルトに近いです。

では本題に入ります。

## Localization入ってない？

そう、上記のコードを見るとそもそもLocaleのコードが入ってるんです。
しかしこれは**動きません**。
上記のローカリゼーションは[widgetbook][widgetbook]が用意しているAddonで、これはFlutterのネイティブのローカリゼーション機能を使用しています。[slang][slang]を使ってる場合、このネイティブのローカリゼーションは行っていないので機能しません。(少なくとも自分は機能させられなかった)

## じゃあどうしたら良いのか？

そこで色々試行錯誤し、「自分でAddon作れば良いんじゃね？」という結論になりました。
[wdigetbook][widgetbook]のAddonは自分でカスタムのものが作れるので、これと[slang][slang]が用意してる手動で言語を切り替える機能を使って、独自の`LocalizationAddon`を作成します。

```dart
class CustomLocaleAddon extends WidgetbookAddon<String> {
  String initialCustomLocale = "ja";

  CustomLocaleAddon()
      : super(
          name: "Locale",
        );

  @override
  Widget buildUseCase(BuildContext context, Widget child, String setting) {
    LocaleSettings.setLocaleRaw(setting);
    return child;
  }

  @override
  List<Field<String>> get fields {
    return [
      ListField<String>(
          name: "locale",
          initialValue: initialCustomLocale,
          values: [
            "ja",
            "en",
          ])
    ];
  }

  @override
  String valueFromQueryGroup(Map<String, String> group) {
    return valueOf<String>("locale", group)!;
  }
}
```

基本的なコードは[widgetbook][widgetbook]の公式にある[カスタムAddonの項目](https://docs.widgetbook.io/addons/custom-addon)を参考にしています。

`fields`がAddonで選択できる内容を管理しています。
これが選択された時、`valueFromQueryGroup`が実行されるのでここで値の変更を行います。
上記の例ではそのまま文字列を返せばいいので特別な処理は入れません。

これが実行されたあと、`buildUseCase`が実行され、`valueFromQueryGroup`のreturn値が`buildUseCase`の`setting`引数に渡されます。
つまり、この時点でユーザーが選択した言語文字列が`settings`に入ってくるということです。
あとは、[slang][slang]の`LocaleSettings.setLocaleRaw(言語文字列)`を使って、[slang][slang]の選択されている言語を変更します。

そしたら、これを[widgetbook][widgetbook]のAddonに追加します。

```dart
// widgetbook.dart

import 'package:comolu_app/i18n/i18n.g.dart';
import 'package:comolu_app/services/firebase.dart';
import 'package:comolu_app/services/get_it.dart';
import 'package:comolu_app/utils/theme.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;

// Import the generated directories variable
import 'widgetbook.directories.g.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  LocaleSettings.useDeviceLocale();

  // NOTE: 各種コンフィグの設定
  await setupFirebase();
  await setupGetIt();

  runApp(const WidgetbookApp());
}

@widgetbook.App()
class WidgetbookApp extends StatelessWidget {
  const WidgetbookApp({super.key});

  @override
  Widget build(BuildContext context) {
    return Widgetbook.material(
      // Use the generated directories variable
      appBuilder: (context, child) {
        return ProviderScope(child: child);
      },
      directories: directories,
      addons: <WidgetbookAddon>[
        MaterialThemeAddon(themes: [
          WidgetbookTheme(name: "Light", data: defaultTheme),
          WidgetbookTheme(name: "Dark", data: defaultTheme),
        ]),
        DeviceFrameAddon(devices: [
          Devices.ios.iPhoneSE,
          Devices.ios.iPhone13,
          Devices.ios.iPhone13Mini,
          Devices.ios.iPhone13ProMax,
        ]),
        TextScaleAddon(
          scales: <double>[1.0, 2.0],
        ),
        CustomLocaleAddon(), <- 追加
      ],
      integrations: const [],
    );
  }
}
```

これで、[widgetbook][widgetbook]+[slang][slang]で言語の変更できます。

![slang_switch.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147238/1e8a4ccc-c0b8-9827-5ee8-61fb77a98e92.gif)

## あとがき

Storybookに慣れてるので最初は設定などに手こずりましたが、やり方がわかってくるとやっぱりページやWidgetのカタログ化は便利ですね。

## Reference

[widgetbook]: https://docs.widgetbook.io
[slang]: https://pub.dev/packages/slang