iPhoneのマイクだけでハウリングを起こしやすい帯域を見つけるアプリを作っています。
日本語だけで出していたのですが、市場を広げたくて英語に対応することにしました。画面に出る日本語を String(localized:) で包み、en.lproj/Localizable.strings に訳を入れる、という手順です。
包んだところは924か所、訳は764件になりました(2026-08-26 11時に数えた値です)。
訳を入れ終えて、シミュレータの言語を英語にして全画面を見て回ったところ、ある画面を開いた瞬間にアプリが落ちました。日本語では落ちません。
この記事は、そのとき何が起きていたのかと、同じことを二度やらないために書いた検査の記録です。
私が見た症状
私が英語で起動して周波数当てクイズの結果画面を開くと、アプリは1,318ミリ秒で SIGSEGV で終了しました。
私が日本語で起動し直すと、アプリは落ちませんでした。
xcrun simctl launch --console <UDID> <BUNDLE_ID> -AppleLanguages "(ja)"
言語を切り替えるだけで落ちたり落ちなかったりするので、コードではなく訳を疑いました。
原因
問題の1件です。
キー : 「%@」まで、あと%lld点
訳 : %lld points to "%@"
日本語は「文字列 → 数」の順です。英語にすると「数 → 文字列」の順が自然なので、そう訳しました。
String(format:) は、引数を出てきた順に取ります。 番号を付けない限り、1つ目の指定子が1つ目の引数、2つ目が2つ目の引数です。
このとき渡していたのは、順に「称号の文字列」と「点数の整数」でした。
訳の1つ目の指定子は %lld なので、文字列が入っているはずの場所を、整数として読みます。 ポインタの値をそのまま数として読むことになり、続く %@ は整数を文字列のアドレスとして読みにいきます。そこで落ちます。
直し方
指定子に番号を付けます。
"%2$lld points to \"%1$@\""
%1$ が1つ目の引数、%2$ が2つ目の引数です。これで、訳の側で語順を変えても、引数は元の順のまま取られます。
Appleの資料にも、訳で語順を入れ替えるときのやり方として書かれています。
One problem that often occurs during translation is that the translator may need to reorder parameters inside translated strings to account for differences in the source and target languages. If a string contains multiple arguments, the translator can insert special tags of the form n$ (where n specifies the position of the original argument) in between the formatting characters. These tags let the translator reorder the arguments that appear in the original string.
同じページに、そのままの例も載っています。
/* Message in alert dialog when something fails */
"%@ Error! %@ failed!" = "%2$@ blah blah, %1$@ blah!";
764件のうち、ずれていたのは1件だけ
これが一番怖いところでした。
目で見つけるのは無理です。 訳が数百件あって、そのうち指定子が2つ以上入っているものだけが危なく、しかも語順を変えたときだけ起きます。
私の場合、指定子が2つ以上入っているキーは41件ありました(2026-08-26 11時の実測)。そのうち語順を変えていたのは2件です。
そして、このバグが入ったビルドを、そのままApp Store Connectへアップロードしていました。 気づいたのは、アップロードしたあとに英語で全画面を見て回ったときです。作り直しました。
型が同じだと、機械では止まらない間違いもある
もう1件、同じ画面で見つけました。こちらは落ちません。
キー : %lld問中%lld問正解
訳 : %lld of %lld correct
日本語は「全問数 → 正解数」の順です。英語の of は「正解数 of 全問数」と読むのが自然なので、訳した文の意味の順が逆になっていました。
画面には「5 of 4 correct」と出ていました。4問中5問正解、という表示です。
どちらも %lld なので、指定子の並びを比べる検査は通ってしまいます。 型は合っているからです。
直し方は同じで、番号を付けます。
"%2$lld of %1$lld correct"
指定子が2つ以上入っているキーは、1件ずつ目で読むしかありませんでした。 41件を全部読んで、間違いはこの1件だけでした。
検査を書いた
キーと訳で指定子が食い違っていないかを数えるだけのものです。
import re, json, subprocess, sys
P = "<path>/en.lproj/Localizable.strings"
j = json.loads(subprocess.run(["plutil", "-convert", "json", "-o", "-", P],
capture_output=True, text=True).stdout)
SPEC = re.compile(r'%(\d+\$)?[-+ #0]*[\d*]*(?:\.\d+)?(?:hh|h|ll|l|q|L|z|t|j)?'
r'([@dDiuUxXoOfeEgGcCsSpaAn%])')
def specs(s):
return [(m.group(1) or '', m.group(2)) for m in SPEC.finditer(s) if m.group(2) != '%']
def normalize(lst):
# %2$lld のように番号が付いていれば、その番号の順に並べ直して型だけ返す
if any(n for n, _ in lst):
return [t for _, t in sorted((int(n.rstrip("$")), t) for n, t in lst if n)]
return [t for _, t in lst]
bad = [(k, v) for k, v in j.items() if normalize(specs(k)) != normalize(specs(v))]
print(f"キーの数 {len(j)} / 書式が食い違っている数 {len(bad)}")
sys.exit(1 if bad else 0)
plutil -convert json を通しているのは、.strings のエスケープを自分で解かなくて済むからです。
番号が付いている訳を「食い違い」と数えないように、normalize() で番号順に並べ直してから比べています。ここを入れないと、正しく直した訳が全部引っかかります。
この検査で止まるのは、指定子の型と数が食い違うものだけです。上の「5 of 4 correct」のように、型が同じで意味の順番だけが逆のものは通ります。そこは目で読むしかありません。
空振りしていた検査
書いた直後に走らせたら「英語の訳が無いので飛ばしました」と出ました。
パスを間違えていました。 Sources/ の下を見るつもりが、実際のファイルは Sources/en.lproj/ にありました。
「0件でした」と出ていたのではなく「見ていません」と出ていたのですが、通ったつもりで先へ進みかけました。
直したあと、わざと訳を壊して検査が止まることと、戻すと通ることを両方確かめました。0件という結果は、検査が動いた証拠と一緒でないと意味がないからです。
おわりに
落ちたのは、書式指定子のこの1件だけでした。
ただ、英語にしたときだけおかしくなる場所は、これとは別の原因でもいくつも見つかりました。判定のときに画面へ出す文字をそのまま使っていて、英語では一致しなくなる、というものです。同じ根っこで、症状が違うだけでした。そちらは別に書きます。
日本語のままでは一度も落ちません。シミュレータの言語を英語にして全画面を開いて回るまで、気づく機会がありませんでした。アップロードまで済ませたあとでした。
短くまとめると、こうなります。
- 訳で語順を変えるときは
%1$%2$の番号を必ず付ける - 付け忘れると、その画面を開いた瞬間に落ちる。日本語では落ちないので気づきにくい
- 指定子の型と数を比べる検査は機械で書ける
- ただし型が同じで意味の順番だけ逆のものは通る。指定子が2つ以上あるキーは目で読む
- 検査を書いたら、わざと壊して止まることを確かめる
英語対応そのものは、まだ途中です。
出典
-
String Resources(Apple Developer Documentation・Archive) …
n$のタグで訳の語順を入れ替える話の出典
数値は全て手元の実装と実測に当たって書いています。
無料でできないことは3つです。ステレオ録音は1回5分・2件まで。モニターチェックの結果は3件まで。耳を鍛えるクイズは1日2セットまで。買い切り1,200円のフル版にすると、5分を超える本番も1本で録れます。結果は何件でも残せます。クイズも1日何セットでもできます。
使ってみて役に立ったら、App Storeで評価をいただけると助かります。
個人で作っているので、一つでも大きな励みになります。