RORK LABEN
PLAY — Google Play の target API level 36 要件が昨日8月31日に発効しました。今日以降、新規アプリと既存アプリの更新は Android 16 対応が必須ですVISIBILITY — API 35 のままのアプリは掲載こそ続きますが、新しい Android 版のユーザーには表示されなくなります。エラーが出ないまま新規インストールだけが減る点に注意が要りますEXTENSION — 間に合わなかった場合は、Play Console から2026年11月1日までの延長申請が出せます。恒久対応の計画とセットで進めるのが実務的ですAPPLE — Apple 側は9月9日にイベント、iOS 27 の正式リリースは9月14日と報じられています。生成したアプリの iOS 27 実機確認はリリース週の前に済ませておきたいところですEXPO — Expo が expo-paste-input を公開しました(8月28日)。React Native の TextInput に画像・GIF・ステッカーの貼り付けを追加するネイティブモジュールですEAS — EAS Observe が8月20日に GA になりました。クラッシュや性能の観測を、ビルドや配信と同じ EAS 上で持てるようになっていますPLAY — Google Play の target API level 36 要件が昨日8月31日に発効しました。今日以降、新規アプリと既存アプリの更新は Android 16 対応が必須ですVISIBILITY — API 35 のままのアプリは掲載こそ続きますが、新しい Android 版のユーザーには表示されなくなります。エラーが出ないまま新規インストールだけが減る点に注意が要りますEXTENSION — 間に合わなかった場合は、Play Console から2026年11月1日までの延長申請が出せます。恒久対応の計画とセットで進めるのが実務的ですAPPLE — Apple 側は9月9日にイベント、iOS 27 の正式リリースは9月14日と報じられています。生成したアプリの iOS 27 実機確認はリリース週の前に済ませておきたいところですEXPO — Expo が expo-paste-input を公開しました(8月28日)。React Native の TextInput に画像・GIF・ステッカーの貼り付けを追加するネイティブモジュールですEAS — EAS Observe が8月20日に GA になりました。クラッシュや性能の観測を、ビルドや配信と同じ EAS 上で持てるようになっています
記事一覧/ビジネス
ビジネス/2026-04-02上級

16言語のASOキーワードを機械で検査する — App Store Connect の160文字を取りこぼさない運用

壁紙アプリを多言語で配信する中で、キーワード枠の使い残しと二重登録が積み上がっていました。name 30・subtitle 30・keywords 100 という予算をロケールごとに機械検査する Node スクリプトを作り、実行結果とともに、英語の直訳が通用しなかった経緯と季節枠の入れ替え方をまとめます。

ASO27グローバル展開3App Store Connect12キーワード設計多言語運用

プレミアム記事

ある年の春、壁紙アプリの日本語ロケールのキーワード欄を久しぶりに開いて、手が止まりました。100文字の枠に対して、使っていたのは29文字。しかもその中に、アプリ名にすでに入っている語が二つ紛れ込んでいました。

数年にわたって少しずつ書き換えてきた結果です。誰かが間違えたわけではなく、ロケールが増えるにつれて全体を見渡す手段がなくなっただけでした。16言語ぶんの欄を目視で確かめるという運用が、そもそも成り立っていなかったのです。

その日から、キーワード欄は「考えて書くもの」ではなく「機械で守るもの」だと考えを変えました。ここでは、その検査スクリプトの中身と、それ以前に積み上がっていた判断の履歴を、順に書いていきます。

App Store のキーワードは「160文字の予算」である

前提から確認します。iOS の検索対象になるテキスト欄は、思っているより狭いです。

上限検索の索引対象
App 名(name)30文字対象
サブタイトル(subtitle)30文字対象
キーワード(keywords)100文字対象
説明文(description)4,000文字iOS では対象外

合計160文字。ここに載らなかった語は、説明文にいくら書いても検索の入り口にはなりません。iOS の説明文にキーワードを詰め込む作業に意味がないのは、この一点に尽きます。Google Play は説明文の語も拾う設計ですので、同じ原稿を両ストアに流用すると、片方だけが空振りします。

そして重要なのが、name と subtitle の語は別枠で索引されているという点です。つまりキーワード欄に同じ語を書き直すと、100文字の予算をそのぶん捨てることになります。

もうひとつ、カンマの直後に空白を入れると、その空白が1文字として数えられます。7個のキーワードを空白付きで区切れば、それだけで6文字が消えます。6文字あれば、日本語なら短いキーワードが1つ入る量です。

この3点は仕様として安定しているぶん、機械で検査するのに向いています。

160文字の予算を検査するスクリプトを書く

ロケールごとのメタデータを JSON にまとめ、上の規則に照らして検査する Node スクリプトを用意しました。依存パッケージはありません。

#!/usr/bin/env node
// aso-keyword-lint.mjs — App Store Connect のローカライズ済みメタデータを
// 「name 30 / subtitle 30 / keywords 100」の予算に対して機械検査する。
import { readFileSync } from 'node:fs';
 
const LIMITS = { name: 30, subtitle: 30, keywords: 100 };
 
// Apple の文字数カウントに合わせ、サロゲートペアを1文字として数える
const len = (s) => [...(s ?? '')].length;
 
// 比較用の正規化。全角/半角・大文字小文字・前後空白を吸収する
const norm = (s) => (s ?? '').normalize('NFKC').toLowerCase().trim();
 
// name / subtitle 側の「すでに索引されている語」を取り出す
const wordsOf = (s) =>
  norm(s)
    .split(/[\s,、・/|+\-–—::()()]+/u)
    .filter((w) => w.length > 1);
 
function lintLocale(locale, meta) {
  const errors = [];
  const warnings = [];
 
  for (const field of ['name', 'subtitle', 'keywords']) {
    const n = len(meta[field]);
    if (n > LIMITS[field]) {
      errors.push(`${field}: ${n} 文字(上限 ${LIMITS[field]}/${n - LIMITS[field]} 文字超過)`);
    }
  }
 
  const raw = meta.keywords ?? '';
 
  // 1) カンマ直後の空白は1文字ずつ課金される
  const spaceWaste = (raw.match(/,\s/g) ?? []).length;
  if (spaceWaste > 0) {
    errors.push(`keywords: カンマ直後の空白 ${spaceWaste} 個(${spaceWaste} 文字を無駄に消費)`);
  }
 
  // 2) 空トークン・末尾カンマ
  const tokens = raw.split(',').map((t) => t.trim());
  const empties = tokens.filter((t) => t === '').length;
  if (empties > 0) errors.push(`keywords: 空のトークン ${empties} 個(連続カンマ/末尾カンマ)`);
 
  const live = tokens.filter((t) => t !== '');
 
  // 3) keywords 内での重複
  const seen = new Map();
  for (const t of live) {
    const k = norm(t);
    seen.set(k, (seen.get(k) ?? 0) + 1);
  }
  const dupes = [...seen.entries()].filter(([, c]) => c > 1).map(([k]) => k);
  if (dupes.length) errors.push(`keywords: 重複トークン ${dupes.join(' / ')}`);
 
  // 4) name / subtitle と重複した語(別枠で索引されるため予算の二重払いになる)
  const indexed = new Set([...wordsOf(meta.name), ...wordsOf(meta.subtitle)]);
  const redundant = live.filter((t) => indexed.has(norm(t)));
  if (redundant.length) {
    const saved = redundant.reduce((a, t) => a + len(t) + 1, 0);
    errors.push(`keywords: name/subtitle と重複 ${redundant.join(' / ')}(回収可能 約${saved} 文字)`);
  }
 
  // 5) 予算の使い残し
  const used = len(raw);
  const left = LIMITS.keywords - used;
  if (left >= 15) {
    warnings.push(`keywords: ${left} 文字が未使用(予算の ${Math.round((left / LIMITS.keywords) * 100)}%)`);
  }
 
  const note = `索引対象 ${len(meta.name)}+${len(meta.subtitle)}+${used} = ${len(meta.name) + len(meta.subtitle) + used} / 160 文字`;
  return { locale, errors, warnings, note };
}
 
const path = process.argv[2];
if (!path) {
  console.error('usage: node aso-keyword-lint.mjs <metadata.json>');
  process.exit(2);
}
 
const data = JSON.parse(readFileSync(path, 'utf8'));
let failed = 0;
 
for (const [locale, meta] of Object.entries(data)) {
  const r = lintLocale(locale, meta);
  const mark = r.errors.length ? 'NG' : r.warnings.length ? '--' : 'OK';
  console.log(`[${mark}] ${locale}  ${r.note}`);
  for (const e of r.errors) console.log(`     ERROR  ${e}`);
  for (const w of r.warnings) console.log(`     WARN   ${w}`);
  if (r.errors.length) failed += 1;
}
 
console.log(`\n${Object.keys(data).length} ロケール検査 / ${failed} ロケールでエラー`);
process.exit(failed > 0 ? 1 : 0);

入力はロケールをキーにした素直な JSON です。App Store Connect の画面からコピーしても、あとで触れる API から吐き出しても、同じ形に落とせます。

{
  "en-US": {
    "name": "Beautiful HD Wallpapers",
    "subtitle": "4K backgrounds for your phone",
    "keywords": "aesthetic, live wallpaper, wallpapers, lock screen, 4k, nature, minimal"
  },
  "ja": {
    "name": "壁紙アプリ HD",
    "subtitle": "高画質の背景画像を毎日お届け",
    "keywords": "壁紙,無料,おしゃれ,ロック画面,和風,自然,高画質,壁紙"
  },
  "zh-Hans": {
    "name": "高清壁纸",
    "subtitle": "每日更新的手机背景图片",
    "keywords": "免费壁纸,动态壁纸,锁屏,无广告,风景"
  },
  "ar": {
    "name": "خلفيات جميلة",
    "subtitle": "خلفيات عالية الدقة لهاتفك",
    "keywords": "خلفيات,مجاني,شاشة القفل,طبيعة,"
  }
}

このサンプルは、私が実際にやってしまった間違いをそのまま4種類詰め込んであります。英語には空白区切りと二重登録、日本語には同一トークンの重複、アラビア語には末尾カンマ。中国語だけが素直な状態です。

Node v22.23.2 で実行した出力が次のものです。

$ node aso-keyword-lint.mjs metadata.sample.json
[NG] en-US  索引対象 23+29+71 = 123 / 160 文字
     ERROR  keywords: カンマ直後の空白 6 個(6 文字を無駄に消費)
     ERROR  keywords: name/subtitle と重複 wallpapers / 4k(回収可能 約14 文字)
     WARN   keywords: 29 文字が未使用(予算の 29%)
[NG] ja  索引対象 8+14+29 = 51 / 160 文字
     ERROR  keywords: 重複トークン 壁紙
     WARN   keywords: 71 文字が未使用(予算の 71%)
[--] zh-Hans  索引対象 4+11+19 = 34 / 160 文字
     WARN   keywords: 81 文字が未使用(予算の 81%)
[NG] ar  索引対象 12+25+30 = 67 / 160 文字
     ERROR  keywords: 空のトークン 1 個(連続カンマ/末尾カンマ)
     ERROR  keywords: name/subtitle と重複 خلفيات(回収可能 約7 文字)
     WARN   keywords: 70 文字が未使用(予算の 70%)
 
4 ロケール検査 / 3 ロケールでエラー
$ echo $?
1

英語ロケールでは、空白6文字と二重登録14文字で、合わせて20文字ぶんが取り戻せる計算になります。100文字の枠に対して20文字ですから、無視できる量ではありません。

終了コードが 1 になりますので、メタデータを更新する CI ジョブの手前に置けば、そのまま門番として使えます。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
name 30 / subtitle 30 / keywords 100 の予算をロケール横断で検査し、カンマ後の空白・重複トークン・name との二重登録を CI で落とせる Node スクリプト一式
日本語や中国語で最初の実装が重複を取りこぼした理由と、CJK に包含判定を足して直すまでの過程
英語キーワードの直訳が通用しなかった局面の記録と、季節・イベント枠を通年枠と分けて入れ替える運用の組み方
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

関連記事

ビジネス2026-03-28
地域を1つずつ開ける — Rork アプリの多地域展開とASOを、順番で設計する
Rork で作ったアプリを一度に全世界へ出すのではなく、1地域ずつ確かめながら広げるための設計。地域ブロックの切り方、その言語で実際に打たれる語を拾うキーワード設計、購買力に合わせた価格、各地域の規制対応までを順番で整理します。
ビジネス2026-07-19
アプリを手放す日の実務 — App Store の「Appを転送」で引き継がれるもの・作り直すもの
App Store Connect の「Appを転送」でアプリを売却・移管する際の条件と実務を整理します。評価やサブスク購読者など引き継がれるもの、TestFlight や APNs キーなど作り直すもの、ユーザーの再ログインが発生する箇所まで公式情報に基づいてまとめました。
ビジネス2026-06-15
複数アプリのストア情報を App Store Connect API でコード管理する — 手作業の更新を月一の仕組みに変える
Rork で量産したアプリが増えると、ストアの説明文や価格を手作業で直す時間が無視できなくなります。App Store Connect API を使ってメタデータをコードとして管理し、十数本のアプリへ一括反映する運用設計を、認証の落とし穴も含めて実装視点でまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →