1.2.3 · 開発基盤

スクリーンショットを多言語対応しよう

2026.07.12約5分

00Overview

01Story

Situation

1.2.2 までで、テキスト (アプリ本体・申請情報) の多言語化は済んだ。残るのは、スクリーンショットである。

Complication

本来なら、Mac 上でエミュレーターを起動し、言語を切り替えながら自動で撮影するのが理想だ。しかし 1.1.2 のとおり、手元に Mac はない。50 ロケール分のスクリーンショットを用意するのは、不可能に見えた。

Question

Mac なしで、大量の言語のスクリーンショットをどう用意するか?

02Solution

Criteria

  • Mac を持たずに成立すること
  • 大量の言語をカバーできること
  • Apple の厳密なサイズ要件を満たせること

Answer

一つの手法で全部やろうとせず、4 つの選択肢を「全部使う」ことにした。

  • UI をできるだけ言語に依存しない設計にする — 画像内の文字を減らせば、そのぶん翻訳も生成も要らなくなる。
  • 主要言語 (日本語・英語など) は実機で撮る — 言語を切り替えて、実機のスクリーンショットを撮影する。
  • その他の言語は Flow で一括変換する — 「この画像を○○語版にして」と指示し、まとめて生成する。
  • 英語版に説明を描き込み、他言語でも流用する — イラストや注釈を加えておけば、細部まで各言語で作り直さずに済む。

生成した画像は ZIP でまとめて Git にアップロードし、Claude で展開・配置したうえで、GitHub Actions から App Store Connect へアップロードする (分割・トリミング・リサイズを通し、ストアの API 経由で送る)。

Reason

組み合わせでしか、Mac なしの制約を越えられない。 全部を実機で撮れば言語数分の手間で潰れ、全部を AI 生成にすれば細部が崩れてサイズ要件も外れる。

どれか一つでは詰む。
けれど、組み合わせれば越えられる。

とりわけ Flow は、一度に大量の画像を生成して ZIP でまとめて落とせるうえ、生成を待つ間に次の生成を指示できるので、作業効率がとても高い。

Options

  • 全部を実機で撮影する — 忠実だが、言語数だけ撮影の手間がかかり、現実的でない。
  • 全部を AI で生成する — 速いが、細部が崩れやすく、翻訳も正確ではない。
  • Claude で画像置換してもらう — Claude の弱いところとして、画像生成エンジンを持っていない。酷いものが出てきたので諦めた。

03Result

Good

Mac なしで、多言語のスクリーンショットが用意できた。UI の言語非依存設計と英語版の説明流用のおかげで、生成すべき量そのものも抑えられた。地味に大量だった作業が、AI との組み合わせで回るようになった。Flow は、無料枠で結構画像生成ができる。それを ZIP で DL できる点も大きい。

Bad

コードを画像生成 (翻訳時) に入れていないので、精度が悪い。何度も画像生成 AI に修正をかけていくと、文字が画像なのでどんどん劣化していく。改善の余地は色々ありそう。

Follow-up

これで、アプリ本体・申請情報・スクリーンショットの 3 つすべてが多言語化でき、世界公開の準備が整った。狭いニッチでも、世界を母集団にすれば市場になる — その狙いを定める話は 3.1.2 STP に続く。

■ 再現できる方法

この構成を自分で再現したい人向けのメモ。読み物ではないので、指示文は数行だけ表示してスクロールにしてある。

まずは、仕組みを作成。下記を 1 回のコピペで貼り付けて実行 (プロンプト + Gemfile + workflow + Fastfile をまとめて渡す)。

App Store Connect のスクリーンショットを GitHub Actions から fastlane
(spaceship 直叩き)で自動アップロードする仕組みを作りたい。

前提:
・ASC の API キーは GitHub Secrets に APPLE_API_KEY_ID /
  APPLE_API_KEY_ISSUER_ID / APPLE_API_KEY_P8 の3つで登録済み
・アプリは SKU=<自分のSKU>、Bundle ID=<自分のBundleID> で ASC 登録済み

作ってほしいもの:
1. Gemfile … fastlane 2.228 系を固定
2. fastlane/Fastfile に upload_screenshots_direct レーンを spaceship 直叩きで実装
   ・apps/<SKU>/screenshots/<locale>/*.png を読む
   ・iPhone ファイルは APP_IPHONE_67、iPad ファイルは APP_IPAD_PRO_3GEN_129
     のスクショセットに入れ替えアップロード
   ・ローカルに画像が無いロケールはスキップ(= 別経路で上げ済みを消さない)
3. .github/workflows/ に screenshots 用の workflow_dispatch ワークフローを1つ。
   env に APP_SKU / APP_BUNDLE_ID と3つの Secret を渡す(timeout は長めに)
4. apps/<SKU>/screenshots/<locale>/ を50ロケール分(.gitkeep 付き)

さらに、apps/<SKU>/ に置いた ZIP を読んで、ファイル名の言語タグで
ロケールを判定し、iPhone は縦3分割・iPad は縦2分割 → 中央トリミング →
Apple 推奨サイズ(iPhone 1320×2868 / iPad 2752×2064)へリサイズして
apps/<SKU>/screenshots/<locale>/ に書き出す Python スクリプトも作って。
実行前に「どのファイルが→どのロケールに行くか」の対応表を出す作りにして。


source "https://rubygems.org"
gem "fastlane", "~> 2.228.0"


name: <SKU> — screenshots

on:
  workflow_dispatch:

jobs:
  upload:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
          bundler-cache: true

      - name: Run fastlane upload_screenshots_direct
        env:
          APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
          APPLE_API_KEY_ISSUER_ID: ${{ secrets.APPLE_API_KEY_ISSUER_ID }}
          APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
          APP_SKU: <SKU>
          APP_BUNDLE_ID: <BundleID>
          FASTLANE_SKIP_UPDATE_CHECK: "1"
        run: bundle exec fastlane ios upload_screenshots_direct


default_platform(:ios)

platform :ios do
  desc "Upload per-locale screenshots via spaceship (bypasses deliver's broken screenshot upload)"
  lane :upload_screenshots_direct do |options|
    sku       = options[:sku]       || ENV["APP_SKU"]
    bundle_id = options[:bundle_id] || ENV["APP_BUNDLE_ID"]
    UI.user_error!("sku is required")       if sku.nil? || sku.empty?
    UI.user_error!("bundle_id is required") if bundle_id.nil? || bundle_id.empty?

    screenshots_path = File.expand_path("../apps/#{sku}/screenshots", __dir__)
    UI.user_error!("screenshots not found: #{screenshots_path}") unless Dir.exist?(screenshots_path)

    require "spaceship"
    Spaceship::ConnectAPI.token = Spaceship::ConnectAPI::Token.create(
      key_id:    ENV.fetch("APPLE_API_KEY_ID"),
      issuer_id: ENV.fetch("APPLE_API_KEY_ISSUER_ID"),
      key:       ENV.fetch("APPLE_API_KEY_P8"),
    )

    app = Spaceship::ConnectAPI::App.find(bundle_id)
    UI.user_error!("app not found for bundle_id=#{bundle_id}") if app.nil?

    version = app.get_edit_app_store_version
    UI.user_error!("no edit version found on App Store Connect") if version.nil?
    UI.message("Writing screenshots to version #{version.version_string} (#{version.app_store_state})")

    # 出力ファイル名 -> ASC のスクショ表示タイプ。
    # "iPhone" ファイルは 1320x2868 (iPhone 6.9")、Apple が 6.7"/6.9" 共用にする枠へ。
    # "iPad" ファイルは 2752x2064、iPad Pro 12.9" 3rd gen 横向き。
    device_of = ->(filename) {
      case filename
      when /iPhone/i then "APP_IPHONE_67"
      when /iPad/i   then "APP_IPAD_PRO_3GEN_129"
      else nil
      end
    }

    summary_lines = ["## Direct screenshot upload to #{version.version_string}", ""]
    updated = 0
    version.get_app_store_version_localizations.sort_by(&:locale).each do |loc|
      locale_dir = File.join(screenshots_path, loc.locale)
      unless Dir.exist?(locale_dir)
        summary_lines << "- **#{loc.locale}**: skipped (no local directory)"
        next
      end

      pngs = Dir.children(locale_dir).select { |f| f.downcase.end_with?(".png") }.sort
      if pngs.empty?
        summary_lines << "- **#{loc.locale}**: skipped (no PNGs)"
        next
      end

      groups = pngs.group_by { |f| device_of.call(f) }.reject { |k, _| k.nil? }
      if groups.empty?
        summary_lines << "- **#{loc.locale}**: skipped (no iPhone/iPad files)"
        next
      end

      existing_sets = loc.get_app_screenshot_sets.group_by(&:screenshot_display_type)
      groups.each do |display_type, files|
        set = existing_sets[display_type]&.first
        if set
          # 既存スクショを消してから新しいものに入れ替える。
          set.app_screenshots.each do |shot|
            shot.delete!
          end
        else
          set = loc.create_app_screenshot_set(attributes: {
            screenshotDisplayType: display_type,
          })
        end

        files.each do |filename|
          path = File.join(locale_dir, filename)
          UI.message("  #{loc.locale}/#{filename} -> #{display_type}")
          set.upload_screenshot(path: path, wait_for_processing: true)
        end
      end
      summary_lines << "- **#{loc.locale}**: uploaded #{pngs.length} screenshot(s)"
      updated += 1
    end

    summary_lines << ""
    summary_lines << "Updated #{updated} locale(s)."
    report = summary_lines.join("\n")
    UI.message(report)

    step_summary = ENV["GITHUB_STEP_SUMMARY"]
    File.write(step_summary, report) if step_summary && !step_summary.empty?
  end
end

次に、各言語のスクショ画像を詰めた ZIP を apps/<アプリ名>/ に置いてから、下記をコピペして依頼。

apps/<アプリ名>/ に、各言語のスクショ画像を詰めた ZIP を置いた。
これを App Store 用に処理して。

・ファイル名の言語タグ(ja_01.png / en-US_… / 韓国語.png など)でロケール判定
・iPhone は縦3分割・iPad は縦2分割、中央トリミングで Apple 推奨サイズにリサイズ
  (iPhone 6.9" = 1320×2868 / iPad 13" = 2752×2064)
・en-US → en-AU / en-CA / en-GB のような家族コピーも展開
・「その他言語」画像は未指定ロケール全部に自動展開

実行前に「どのファイルが → どのロケールに行くか」の対応表を出して確認させて。
OK を出したら apps/<アプリ名>/screenshots/<locale>/ に生成して。

<アプリ名> のスクショをアップする前にチェックして。
screenshots フォルダを全ロケール分ならべて、各ロケールの枚数と
ファイル名を一覧で出して。触ってほしくないロケールを消さないか確認したい。

よく分からなかったら「何してるの?」と聞けば教えてくれるし、うまくいったら「スキルにして」と頼んだらスキルにしてくれる。スキル化するまで /clear や新しいセッションで再開しないこと。記憶が無くなるので、Claude の記憶 (同じセッション内) でスキル化させると、作業が安定する。

無理にスクショを多言語にする必要はない。デフォルト言語の画像が共通で使われるので、ゲームなど分かりやすいものであれば、無理して複数作らなくても、イラストで分かればリリースはできる。