アプリエンジニアがChatGPTにarbファイル分割管理の為のシェルスクリプト書いてもらった
こんにちは、スペースマーケットでモバイルエンジニアをしている村田です。
スペースマーケット外の私が関わっているプロジェクトですが、Flutterアプリ多言語対応するためにSDK標準パッケージのflutter_localizationsを使用しています。
flutter_localizationsには文言リソース管理ファイル(arbファイル)を分割する機能が備わっていない為、サービスが大きくなるにつれて1ファイルが肥大し続けてしまいます。その問題を解消すべく愚直な方法ではありますが、arbファイルを分割して管理するためのシェルスクリプトを作成しました。
普段シェルスクリプト書く機会があまりなく腰が重かったのですが、ChatGPTの力に頼ることで楽に作成できたのでその感想も交えつつ共有いたします。
プロジェクト概要
簡単に本プロジェクトのflutter_localizationsの設定とディレクトリ構成について触れます
flutter_localizations
プロジェクトルートへ設置する設定ファイル l10n.yaml
についてです。
arb-dir: lib/l10n
template-arb-file: app_ja.arb
output-localization-file: app_localizations.dart
output-class: L10n
-
arb-dir
: 言語ファイル(XXX.arb
)を設置するディレクトリを指定 -
template-arb-file
: ベースとなる言語ファイルを指定
ディレクトリ構成
詳細は省きますがlib配下のディレクトリ構成についてです。
日本語と英語へ対応するため、l10n.yamlで指定したl10nディレクトリへ app_ja.arb
app_en.arb
ファイルを設置しています。presentationディレクトリ直下はドメイン毎のディレクトリ、その中へ画面毎にディレクトリを作成し、セットとなるWidgetファイル/ViewModelファイルを格納しています。
lib
├── l10n
│ ├── app_ja.arb
│ ├── app_en.arb
│ └── l10n.dart
├── presentation
│ ├── hoge
│ │ ├── hoge1
│ │ │ ├── widget
│ │ │ ├── hoge1.screen.dart
│ │ │ └── hoge1.viewModel.dart
│ │ └── hoge2
│ │ ├── widget
│ │ ├── hoge2.screen.dart
│ │ └── hoge2.viewModel.dart
│ ├── fuga
│ │ ├── fuga1
│ │ │ ├── widget
│ │ │ ├── fuga1.screen.dart
│ │ │ └── fuga1.viewModel.dart
│ │ └── fuga2
│ │ ├── widget
│ │ ├── fuga2.screen.dart
│ │ └── fuga2.viewModel.dart
│ ︙
│ └── root.screen.dart
︙
└── main.dart
理想の構成
arbファイルを分割し、以下のような構成にできればと考えました
- hoge1画面が不要になった際にその画面で参照している文言もディレクトリごと削除可能(いちいちapp_ja/app_enからhoge1画面に関する文言を探し、削除するという手間が省ける)
- 後から文言追加する際もapp_ja/app_enから該当画面の文言が記載されている位置を探し、ズレがないよう追記するストレスがなくなる
- common_l10nを作成することで画面問わず利用する文言であることを強調可能(app_ja/app_enで1元管理だとネーミングで判断する必要があるかなと思います)
lib
├── l10n
│ ├── app_ja.arb
│ ├── app_en.arb
│ └── l10n.dart
├── presentation
│ ├── hoge
│ │ ├── hoge1
+ │ │ ├── hoge1_l10n
+ │ │ │ ├── hoge1_ja.arb
+ │ │ │ └── hoge1_en.arb
│ │ │ ├── widget
│ │ │ ├── hoge1.screen.dart
│ │ │ └── hoge1.viewModel.dart
│ │ └── hoge2
+ │ ├── hoge2_l10n
+ │ │ ├── hoge2_ja.arb
+ │ │ └── hoge2_en.arb
│ │ ├── widget
│ │ ├── hoge2.screen.dart
│ │ └── hoge2.viewModel.dart
│ ├── fuga
│ │ ├── fuga1
+ │ │ ├── fuga1_l10n
+ │ │ │ ├── fuga1_ja.arb
+ │ │ │ └── fuga1_en.arb
│ │ │ ├── widget
│ │ │ ├── fuga1.screen.dart
│ │ │ └── fuga1.viewModel.dart
│ │ └── fuga2
+ │ ├── fuga2_l10n
+ │ │ ├── fuga2_ja.arb
+ │ │ └── fuga2_en.arb
│ │ ├── widget
│ │ ├── fuga2.screen.dart
│ │ └── fuga2.viewModel.dart
+ ├── common_l10n
+ │ ├── common_ja.arb
+ │ └── common_en.arb
│ ︙
│ └── root.screen.dart
︙
└── main.dart
シェルスクリプト構成
ざっくりですが以下の流れで実現させます
-
app_ja.arb
app_en.arb
を読み込み、ファイルを空にする -
./lib/presentation
配下から*_ja.arb
*_en.arb
にヒットするファイルを探索し、それぞれ配列に格納 - 配列をループで1ファイルずつ取得
- ファイルを1行ずつ読み込み、必要な行のみそれぞれ
app_ja.arb
app_en.arb
へ書き込み
シェルスクリプト作成
冒頭に記載した通り今回ChatGPTに全頼りでシェルスクリプトを作成しました。
全ては長過ぎて添付できない為、冒頭と要点のみ共有します。
まず最終的に仕上がったものを置いておきます👐
完成したスクリプト
function make_loalizations() {
local readonly app_ja_file=./lib/l10n/app_ja.arb
local readonly app_en_file=./lib/l10n/app_en.arb
# ファイルを空にする
: > $app_ja_file
: > $app_en_file
files_ja_path_array=`find ./lib/presentation -name *_ja.arb`
files_en_path_array=`find ./lib/presentation -name *_en.arb`
# JA
log_title "Make app_ja.arb"
make_app_localization $app_ja_file "${files_ja_path_array[@]}"
# EN
log_title "Make app_en.arb"
make_app_localization $app_en_file "${files_en_path_array[@]}"
}
function make_app_localization() {
local readonly app_arb_file=$1
local readonly files_path_array=$2
echo "{" >> $app_arb_file
for file in $files_path_array; do
log_green "💡 $file"
while IFS= read -r file_line || [[ -n "$file_line" ]]; do
# 先頭行/最終行を飛ばす
if [ "$file_line" == "{" ] || [ "$file_line" == "}" ] ; then
continue
fi
# 末尾が「"」または「 }」であれば「,」を追加
if [ ${file_line: -1} == '"' ] || [ "$file_line" == " }" ] ; then
printf '%s\n' "$file_line," >> $app_arb_file
else
printf '%s\n' "$file_line" >> $app_arb_file
fi
done < $file
done
# 最終行の末尾「,」を削除
sed -i '' '$s/,$//' $app_arb_file
echo "}" >> $app_arb_file
}
function log_title() {
local readonly message=$1
log_green "/-----------------------------/"
log_green "/ ⚡️ $message ⚡️"
log_green "/-----------------------------/"
}
function log_green() {
IFS=$'\n'
printf '\033[32m%s\033[m\n' $1
}
make_loalizations
冒頭
腰が低い...!
確認の仕方までフォローしてくれて抜かりない
あーーーーん!そうだreadonlyだー!
Web検索でもヒットするとは思いますが、余計な情報に触れることなくコードに反映された状態で答えが即座に得られるのは嬉しいですね
ほうほう..
...と、ラリーを繰り返しまずは荒く骨組みを完成させました
要点
細かい箇所は別途質問をして詰めていきました。
慣れている方からしたらなんてことない問題かと思いますが、例えば while read
でファイルを1行ずつ読み込む際に以下の考慮ができていませんでした。
- インデント(行頭の空白)が無視されてしまう
- 最終行が無視されてしまう
{
"test", "テスト",
"test2", "テスト2"
}
while read -r line; do
echo "$line"
done < "test.arb"
$ sh test.sh
{
"test", "テスト",
"test2", "テスト2"
インデント(行頭の空白)が無視されてしまう
言われるがまま IFS=
を追加することで解消されました💡
while IFS= read -r line; do
echo "$line"
done < "test.arb"
$ sh test.sh
{
"test", "テスト",
"test2", "テスト2"
最終行が無視されてしまう
こちらもその通りコード修正することで解消されました💡
while IFS= read -r line || [[ -n "$line" ]]; do
echo "$line"
done < "test.arb"
$ sh test.sh
{
"test", "テスト",
"test2", "テスト2"
}
などなど細かい不具合も切り出して質問することで解消でき、例外的な考慮はできていませんが無事完成させる事ができました!
感想
シェルスクリプトの作成不慣れな自分ですが、ChatGPT協力のもと希望通り動くものを完成させることができました(完成度はともかく)。
AIの進化にビビり倒していたので頼らないぞ...という気持ちでしたが以下のツイートをみて「確かに!」と思い今回がっつり利用してみました。
(因みにヒカルの碁大好きで、実家に全巻あります。三谷の姉ちゃん好きだったなあ...)
「君が今僕を支えて 僕が今君を支える だから迷いながらも共に生きていこうよ 未来へと」
(出典:アニメ「ヒカルの碁」OP曲「Get Over」)
まさにこれ。敵じゃなく、師であり相棒のような、でも少しビビりつつ。
そんな気持ちでChatGPT(AI)と共に生きていきます。
最後に
スペースマーケットでは一緒に働く仲間を募集中です!
詳しくは以下採用ページをご確認ください。
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion