10月12日、日記アプリ Lanternly のストアページを39言語分アップロードし終えました。欄の数を数えると819になります。「App情報」の名前、サブタイトル、ポリシーへのリンク、iOS バージョンの6欄、macOS バージョンの5欄、そしてサブスクリプションのローカライズ(グループと3つのプラン)です。翻訳チームも ASO エージェンシーもいません。
これは、それをどう組み立て、どこでつまずいたかの記録です。制限の数字は2026年10月13日に App Store Connect のドキュメントと照らし合わせました。
最初に、嘘をついてはいけない点を決めた#
Lanternly の UI は英語とロシア語しかありません。スウェーデン語やトルコ語のストアページは、アプリもスウェーデン語やトルコ語だという期待を生みます。ダウンロードして英語の画面を見た人は、星1つを付けるか返金を求めるでしょう。
そこで英語とロシア語以外のすべての言語で、説明文の2行目に同じ意味の文を置きました。
Die App-Oberfläche ist auf Englisch und Russisch.
Appens gränssnitt är på engelska och ryska.(ドイツ語とスウェーデン語で「アプリの UI は英語とロシア語です」。)この行は最初の段落の直後、機能の一覧より前に置いています。もっと下にあったら誰も見つけないからです。
2つ目の判断は分量です。14言語には3,000〜4,000文字の完全な説明文を用意し、残りの25言語は1,300文字ほどの短いものにしました。アプリの要点、UI についての正直な1行、サブスクのブロック、法的リンクです。インドの11言語は、そもそもアップロードしませんでした。ヒンディー語やタミル語の翻訳の質を確かめる手段がありませんし、ストアページの下手な翻訳は英語の原文より悪いからです。
819欄のフォームではなく、ファイル1つ#
39言語を App Store Connect のフォーム上で直接編集するのは無理があります。英語でサブスクの言い回しを直したら、ほかの38か所のどこに同じ文があるかを思い出さなければなりません。
信頼できる唯一のソースは、ローカライズ → 欄、という形の JSON 1つに移しました。iOS と macOS の説明文は最後の段落だけが違う(Mac には「ほかに動く環境」の独自の一覧がある)ので、共通の本文と Mac 用の差し替えを持たせています。どんな修正もまずファイルに入れ、チェックを通してから、ストアに送ります。隣には名前、サブタイトル、キーワードを並べた TSV があり、目で見るのにもスクリプトでチェックするのにも便利です。
すべてがぶつかる制限#
| 欄 | 制限 | 設定する場所 |
|---|---|---|
| Name | 30文字 | App情報。iOS と macOS で共通 |
| Subtitle | 30文字 | 同上 |
| Keywords | ドキュメント上は100バイト | バージョン。プラットフォームごとに別 |
| Promotional Text | 170文字 | バージョン。審査なしで変更可 |
| Description | 4000文字 | バージョン |
| What's New | 4000文字 | バージョン |
キーワードについて、ドキュメントにはこう書かれています。「You can provide up to 100 bytes of content」。文字ではなくバイトです。UTF-8 ではキリル文字は1文字2バイト、日本語は3バイトです。私のロシア語のキーワードは93文字で、これは176バイトになりますが、App Store Connect は何も言わずに受け付けました。実際には文字数で数えられているわけですが、それがこの先も変わらないと当てにする気にはなれません。
いちばん窮屈だったのは、予想していた場所ではありませんでした。30文字の名前とサブタイトルはどの言語でも収まりました。ドイツ語の複合語は長いものの、数が少ないからです。引っかかったのは別のところです。
- ドイツ語のプロモーションテキストは170文字中ちょうど170文字。読点ひとつ足せません。
- オランダ語のプロモーションテキストは削る必要がありました。最初の版は収まりませんでした。
- フランス語の説明文は4000文字に収まりませんでした。助かったのは、ポリシーと規約へのリンクが本文と、下の必須ブロックの2回出てきていたことです。重複を消して3917文字になりました。
フォームが見ていないものをチェックするスクリプト#
App Store Connect がチェックするのは長さだけです。それ以外は黙って受け付けるので、ミスに気づけるとしても、表示回数が伸びないことからくらいです。そこでアップロードの前には毎回、TSV に小さなスクリプトをかけています。実際に使っているものを簡略にしたのがこれです。
#!/usr/bin/env python3
"""ローカライズごとに name/subtitle/keywords をチェック: python3 check_fields.py fields.tsv"""
import csv, re, sys
LIMITS = {"name": 30, "subtitle": 30, "keywords": 100}
BANNED = {"app", "free", "apple", "iphone"}
def words(s):
return re.findall(r"[\w’'-]+", s.lower())
bad = 0
for r in csv.DictReader(open(sys.argv[1], encoding="utf-8"), delimiter="\t"):
issues = [f"{f}: {len(r[f])}/{n}" for f, n in LIMITS.items() if len(r[f]) > n]
kw = r["keywords"]
if re.search(r",\s", kw):
issues.append("カンマの後にスペース")
kw_words = [w for t in kw.split(",") for w in words(t)]
dup = {w for w in kw_words if kw_words.count(w) > 1}
if dup:
issues.append(f"keywords 内の重複: {sorted(dup)}")
title = set(words(r["name"] + " " + r["subtitle"]))
if set(kw_words) & title:
issues.append(f"name/subtitle と重複: {sorted(set(kw_words) & title)}")
if (set(kw_words) | title) & BANNED:
issues.append(f"不要な語: {sorted((set(kw_words) | title) & BANNED)}")
bad += bool(issues)
print(("FAIL " if issues else "OK ") + r["locale"], *issues, sep="\n - ")
sys.exit(1 if bad else 0)入力は locale、name、subtitle、keywords の列を持つ TSV です。何を見つけるのか、なぜそうするのか。
- カンマの後のスペース。 100文字のうち1文字を無駄にするだけです。Apple はキーワードをカンマで区切るので、スペースは要りません。
- キーワード内での語の重複と、名前やサブタイトルとの重複。 名前とサブタイトルの語はすでに検索の対象なので、キーワードで繰り返すと同じ場所を2回使うことになります。
- 要らない語。
appやfreeは何も足しませんし、ほかのアプリ名や企業名はドキュメントではっきり禁止されています。実際のリストはもっと長く、プロダクトでそもそも使わない語、たとえば医療的なレッテルもそこに入れています。
説明文とプロモーションテキストの長さは別のスクリプトでチェックし、結果は JSON の隣のファイルに保存しています。1か月後に次の修正をするとき、どの言語に余裕がないかがすぐわかります。
どう翻訳したか#
翻訳の下書きは言語モデルで作りました。ネイティブスピーカーのチェックは受けていません。隠しても意味がありません。一人で開発している人間に出せるのは、まさにここまでです。そのぶん、自分にいくつかのルールを課しました。
フレーズではなく意味を訳す。 英語のプロモーションテキスト「Free to write as much as you like」は、ドイツ語では「Schreib, so viel du willst」(好きなだけ書いて)になりました。「無料」という語を入れると制限に収まらなかったはずですが、意味は残っています。
キーワードは人が入力するとおりに書く。 ロシア語では「дневник эмоций」(感情日記)や「дневник с паролем」(パスワード付き日記)で検索されるので、キーワードには эмоций と паролем が入っています。それぞれ「эмоции(感情)」の生格、「пароль(パスワード)」の造格という、ロシア語の格変化した形です。ドイツ語では複合語で検索されるので、dankbarkeitstagebuch(感謝日記)は1語のまま丸ごと入れています。ポーランド語の dziennik は学校の電子成績簿の意味で使われることが多いため、名前のメインの語は Pamiętnik にしました。
確かめる手段のない言語は短い説明文にする。 文章が短いほど、私が気づけない妙な言い回しが紛れ込む余地は減ります。
リンクもローカライズが必要#
ローカライズが必要なのはリンクも同じです。ローカライズごとに Privacy Policy URL、Support URL、Marketing URL があり、すべての説明文の末尾には法的リンクのブロックがあります。サイト上の文書は5言語なので、スペイン語のローカライズは /es/、日本語は /ja/、中国語は /zh-CN/、残りは /en/ に向けています。サポートは英語とロシア語です。EULA ブロックそのものと、それがサブスクでなぜ必須なのかは別の記事に書きました。
アップロードはスクリプトだけで#
819欄を手で入力すれば1日かかり、どこかで打ち間違えるのはほぼ確実です。私は自分の App Store Connect のセッションからスクリプトでアップロードしました。定期的にやるなら、正しい道は公式の App Store Connect API です。appInfoLocalizations、appStoreVersionLocalizations、subscriptionGroupLocalizations、subscriptionLocalizations のリソースがあり、上に挙げたものはすべてカバーされています。
途中で出くわしたことが2つあります。
- 最初の Mac バージョンには What's New の欄がない。 Lanternly の macOS 1.1 は最初のバージョンなので、この欄がそもそも存在せず、スクリプトはそれを飛ばす必要があります。
- 名前とサブタイトルは iOS と macOS で共通ですが、キーワードと説明文はプラットフォームごとに別です。私はそのままコピーし、説明文の最後の段落だけを変えています。
まだやれていないこと#
新しい言語のスクリーンショットは、今のところすべて英語です。UI がどのみち英語かロシア語なので正直ではありますが、理想的ではありません。37言語のテキストはネイティブスピーカーの誰も読んでいません。読者の中にスウェーデン語やトルコ語、ポーランド語を話す方がいて、Lanternly のページに間違いを見つけたら、メールをもらえるとうれしいです。
もし一からやり直すなら、最初の翻訳より前、初日からソースファイル1つとチェック用スクリプトを用意します。この作業のほかの部分は毎回同じことの繰り返しなので、スクリプトに任せられます。



