ストアのスクリーンショットを16言語ぶん作る、と言葉にすると一行ですが、実際にやると「翻訳文をデザインに流し込んだ瞬間にレイアウトが崩れる」という地味な壁に何度もぶつかります。ドイツ語は日本語の1.8倍に伸びてボタンからはみ出し、アラビア語は右から左に流れて全部反転し、韓国語とタイ語に至っては手元のフォントに字形すら入っていない。私は2014年から個人でiOS/Androidの壁紙アプリを作っていて、累計5,000万ダウンロードまで育ったいまも、こうした「最後の見た目を整える工程」が一番時間を食います。
先日、Android版「綺麗な壁紙」(Beautiful Wallpapers — Google Play )のストアスクショを、英語・スペイン語・フランス語・ドイツ語など16言語に作り直しました。このとき、Photoshopで16言語×5枚を手で打ち直すのをやめて、PSDのテキストレイヤーをPythonで言語別に差し替える やり方に切り替えたら、作業がだいぶ楽になりました。この記事は、そのときの実装メモです。きれいな理論ではなく、実際に動かしたコードと、つまずいた箇所、そして言語別にどのフォントを当てたかの早見表を残します。
なぜ「画像に焼く」のではなく「テキストのまま差し替える」のか
最初に方針の話をさせてください。多言語スクショの自動化というと、各言語の文字を画像として描いて背景に重ねる(オーバーレイPNGを合成する)方法がまず思いつきます。実際それも作りました。ただ、この方法には弱点があります。フォントを後から変えられない のです。
オーバーレイ方式だと、文字は生成した時点でピクセルに固定されます。あとで「やっぱり韓国語の見出しをもう少し太く」と思っても、スクリプトを回し直すしかありません。デザインの微調整は最後まで手元のPhotoshopでやりたい、というのが個人開発の現実です。
そこで本命にしたのが、テキストレイヤーをラスタライズせずに、文字列だけ各言語へ差し替える 方法でした。原本PSDの日本語タイプレイヤーを複製し、中の文字列を翻訳に入れ替えた「編集可能なテキスト版PSD」を生成します。各言語グループを原本にドラッグすれば元の位置にそのまま乗り、フォントもサイズもPhotoshop側で自由に触れる。この一点が、運用してみて一番効きました。
補足として、作品そのもの(壁紙画像)にこうした自動化は一切使っていません。自動化しているのはあくまでストア掲載という運用・プロモーション側の工程です。この線引きは自分の中で固定しています。
元データの構造を把握する
対象の原本PSDは、1440 × 13333px に5枚のアートボード が縦に並んだ構成でした。1枚あたり 1440 × 2560px(9:16)です。テキストレイヤーは全部で約40枚。タイトル、サブタイトル、特徴の箇条書き、数字バッジといった要素が、アートボードごとに分かれて配置されています。
最初にやるのは、このテキストレイヤーを機械的に拾えるようにすることです。psd-toolsを使うと、グループを再帰的に降りていって kind == 'type' のレイヤーだけ抜き出せます。
from psd_tools import PSDImage
def find_types (layers):
"""グループを再帰的にたどり、テキストレイヤーだけを列挙する"""
for layer in layers:
if layer.kind == 'type' :
yield layer
if layer.is_group():
yield from find_types(layer)
src = PSDImage.open( "screenshot.psd" )
for t in find_types(src):
# layer.text に表示文字列、layer.bbox に座標が入っている
print ( repr (t.text), t.bbox)
layer.text で表示中の文字列、layer.bbox で (left, top, right, bottom) の座標が取れます。この top の値がどのアートボードの範囲に入るかを見て、「この文は5枚目のスクショの見出しだ」と機械的に振り分けられます。原本の日本語文字列をキーにした翻訳辞書(TR[日本語] = {"en": "...", "de": "...", ...})を用意しておけば、レイヤーと翻訳が layer.text で素直に突き合わせられます。
テキストレイヤーの中身を書き換える(ラスタライズしない核心部分)
ここが一番のポイントです。Photoshopのテキストレイヤーは、表示文字列を EngineData という独自構造の中に持っています。単に layer.text を代入しても反映されません。EngineData の中の Text を書き換え、さらにスタイル実行配列(StyleRun / ParagraphRun)の長さを新しい文字数に合わせて作り直す 必要があります。ここを揃えないと、Photoshopで開いたときに文字が化けたり、途中で色やサイズが切り替わったりします。
from psd_tools.constants import Tag
from psd_tools.psd.engine_data import Integer
def set_text (layer, newtext):
"""テキストレイヤーの文字列を、ラスタライズせずに差し替える"""
tt = layer._record.tagged_blocks.get_data(Tag. TYPE_TOOL_OBJECT_SETTING )
eng = tt.text_data[ b 'EngineData' ].value
ed = eng.get( 'EngineDict' )
editor = ed.get( 'Editor' )
txt = editor.get( 'Text' )
# 改行は \n ではなく \r。元の末尾改行は維持する
orig = txt.value
trail = " \r " if orig.endswith( " \r " ) else ""
full = newtext.replace( " \n " , " \r " ) + trail
txt.value = full
# StyleRun / ParagraphRun を「1スタイル × 全文字数」に簡約して再構成する
for run in ( 'StyleRun' , 'ParagraphRun' ):
r = ed.get(run)
if r is None :
continue
ra = r.get( 'RunArray' )
rla = r.get( 'RunLengthArray' )
if ra is None or rla is None :
continue
ra._items = [ra._items[ 0 ]] # 先頭スタイルだけ残す
rla._items = [Integer( len (full))] # その長さ = 全文字数
return full, ed
複数スタイルが混在したレイヤー(例えば「10万+」の数字と単位でフォントが違う、といったもの)は、ここで単一スタイルに簡約されます。割り切りですが、ストアスクショではこの方が後の調整が楽でした。気になる箇所だけPhotoshopで手直しすればよい、という分担です。
もう一つ大事なのが、生成先を「原本のコピー」ではなく空のPSDにする ことです。原本にはスマートオブジェクトや調整レイヤーが含まれていて、それを再シリアライズした版はPhotoshopが「互換性がない」と言って開けませんでした。空のドキュメントを新規に作り、そこへ複製したテキストレイヤーだけを積んでいくと、安定して開けます。
W, H = src.width, src.height
doc = PSDImage.new( 'RGB' , (W, H)) # 原本ではなく空ドキュメントから組む
# ここに言語グループ → アートボード別サブグループ → テキストレイヤー を append していく
はみ出しを自動で縮小する
ドイツ語やフランス語は日本語より文字数が伸びます。放っておくとキャンバス幅を超えて切れてしまうので、翻訳がはみ出すレイヤーだけフォントサイズを自動で縮める 処理を入れました。フォントサイズは EngineData の FontSize、レイヤーの縦横スケールは TypeTool の transform 行列から取れます。実測の文字幅が使える幅を超えていたら、収まる比率までサイズを掛け直します。下限は元の50%までとして、それ以上は潰さない方針にしました。
CANVAS_W = 1440
MARGIN = 28
MIN_SCALE = 0.5
def fit_fontsize (font_size, text_width, avail):
"""はみ出しているぶんだけ縮小率を返す(最小50%)"""
if text_width <= avail:
return 1.0 # 収まっているので等倍
return max ( MIN_SCALE , avail / text_width)
実際のコードでは、文字揃え(中央/右/左)と、アラビア語のような右→左(RTL)レイアウトで「使える幅」の計算式が変わります。中央揃えなら中心からの左右の余白で決まり、RTLなら右端からマージンを引いた値になります。ここを言語ごとに分けておかないと、せっかく縮小しても変な位置で切れます。50%まで縮めても収まらない長文は、レイヤー名に翻訳全文を残しておき、Photoshopで二行に割るなど手で対処しました。「自動で8割、手で2割」くらいの配分が、品質と手間のバランスとして自分にはちょうどよかったです。
レイヤー名に翻訳文を入れておく小ワザ
地味ですが効いたのが、各テキストレイヤーの名前を翻訳文そのものにする ことです。Photoshopのレイヤーパネルを開いた瞬間に、どのレイヤーがどの言語のどの文なのかが一目で分かります。
def set_name (layer, pretty):
# ASCII化した安全名を内部に、表示名にはUnicodeの翻訳全文を入れる
layer._record.name = pretty.encode( 'ascii' , 'replace' ).decode()[: 200 ] or 'layer'
try :
layer._record.tagged_blocks.set_data(Tag. UNICODE_LAYER_NAME , pretty)
except Exception :
pass
16言語×5枚×8レイヤーともなると、レイヤーパネルは数百行になります。名前が「レイヤー1のコピー2」のままだと地獄ですが、翻訳文が入っていれば検索もできて、手直しの効率がまるで違いました。
言語別フォント早見表 — ここが本記事の実用パート
自動化で形は作れますが、最後の見栄えはフォント選びで決まります 。原本の日本語は小塚ゴシックなどで組んでありますが、ラテン文字はそのまま綺麗に出ても、キリル・ハングル・タイ・アラビアは字形が入っていないので必ず差し替えが要ります。
私は最終的に、全言語をNoto ファミリーで統一しました。無料で、全スクリプト(文字体系)をカバーし、ウェイトも揃うので多言語の統一感を出しやすい。これが一番安全です。実際に当てたフォントを、元デザインの役割と対応づけて残します。
役割ごとの置き換え方針:
見出し(タイトル/特徴) : 元は太いゴシック(Bold 700相当)。Noto Sans の Bold に。
小見出し : 元はMedium 500相当。Noto Sans の Medium に。
本文 : Regular 400相当。Noto Sans の Regular に。
補足本文 : Light 300相当。Noto Sans の Light に。
タグライン(明朝・セリフ系) : Noto Serif の Medium に。
ブランド英字(Beautiful 4K / Collection / Quality など) : 各言語共通で英字のまま据え置き。凝るなら Cinzel など細いタイトリング体。
数字バッジ(100K+ など・筆記体) : 据え置き。変えるなら Allura など筆記体。
言語別に、本文・見出しへ当てるフォント:
英語・スペイン語・フランス語・ドイツ語・イタリア語・ポルトガル語(BR)・カタルーニャ語・ポーランド語 : Noto Sans(ラテンなので原本のゴシックのままでも可)。
ロシア語(ru) : Noto Sans(キリル対応。要差し替え)。
トルコ語(tr) : Noto Sans(İ / ı の点あり・なしに対応済み)。
韓国語(ko) : Noto Sans KR(= Noto Sans CJK KR。要差し替え)。
簡体中国語(zh-Hans) : Noto Sans SC。
繁体中国語(zh-Hant) : Noto Sans TC。
タイ語(th) : Noto Sans Thai(要差し替え。行間をやや広めに)。
アラビア語(ar) : Noto Sans Arabic または Noto Kufi Arabic(要差し替え・右揃え)。
日本語(原本) : 小塚ゴシック/Noto Sans JP/IPAexゴシック。
ダウンロードして手元に置いておくべき実ファイルは、最低限この顔ぶれです。NotoSans-Regular.ttf、NotoSansJP、NotoSansKR、NotoSansSC、NotoSansThai、NotoSansArabic、そしてセリフのタグライン用に NotoSerifJP。日本語に味を足したいときの選択肢として ipaexg.ttf(IPAexゴシック)も入れています。ウェイト違いを別ファイルで持つより、可変フォント(VariableFont)版を1本入れておくとファイル管理が楽でした。
作業のコツとしては、同じグループ内のレイヤーをまとめて選択して一括でフォントを変えること、翻訳が長くて収まらないときはサイズを少し下げるか字間(トラッキング)を詰めること。アラビア語は右揃え、タイ語は行送りをやや広めにすると、それだけでぐっと締まります。
スクショ作成の前段として、デザインのベースをどう組むかは Figma でアプリのストアスクリーンショットを作成する方法 に、翻訳テキストそのものの設計や16言語のキーワード戦略は 12年・16言語で磨いたグローバルASO戦略 にまとめてあります。本記事はその「組んだデザインを多言語へ機械的に展開する」工程に特化した内容です。
書き出しサイズ — Google PlayとApp Storeで要件が違う
手でブラッシュアップしたあとは、ストアの要件に合わせて書き出します。ここを間違えると審査で弾かれるので、実際に使った数字を残します。
Google Play(電話):
推奨は 1080 × 1920px(9:16) 。各辺320〜3840px、縦横比は16:9〜9:16の範囲。
形式は JPEG または 24bit PNG(アルファ不可) 、各8MB以下、1リスティングあたり2〜8枚。
原本の 1440 × 2560 はそのまま9:16なので要件内ですが、1080 × 1920へ縮小 して書き出すのが軽くて確実でした。
App Store Connect(iPhone):
6.9インチ : 1320 × 2868px(1290×2796 / 1260×2736 も可)。
6.5インチ : 1242 × 2688px(1284×2778 も可)。
形式は PNG または JPEG・RGB・アルファ不可。寸法は厳密一致で、1pxでもズレると却下 されます。
注意点として、iPhoneは19.5:9(約0.4615)で、9:16(0.5625)より縦長です。単純リサイズでは比率が合わないので、上下に余白を足してカンバスを拡張 するか、iOS用に別カンバスで組み直す必要があります。私はiOS版は専用素材を別に持って、そちらで再レイアウトしています。
premium: true の余談ですが、Androidの密度別ドローアブルやnodpiの扱いで詰まったときの話は Android の密度別ドローアブルとnodpiのハマりどころ に分けて書いています。スクショとは別レイヤーの話ですが、ストア提出まわりで同じように細かい仕様に殺される系統の話です。
通しで何が起きるか(処理のまとめ)
スクリプトを一本走らせると、こういう流れになります。
原本PSDからテキストレイヤーを全部拾い、top座標でアートボードへ振り分ける。
各言語について、原本のコピーを開き、テキストレイヤーの文字列を翻訳へ差し替える(ラスタライズしない)。
翻訳がはみ出すレイヤーは、収まる比率(最小50%)まで自動縮小する。
レイヤー名に翻訳全文を入れ、言語グループ→アートボード別サブグループへ整理する。
空ドキュメントへ全言語ぶんを積み、編集可能なテキスト版PSDとして保存する。
開き直して、レイヤー数と各言語のサンプル文字列を検証ログに出す。
最後の検証は省略しないでください。私は「保存はできたがPhotoshopで開いたらJPのまま」という事故を一度やっています。原因は、テキストエンジンが古いキャッシュを描画していただけで、レイヤーを一度選択し直すと再描画されて直りました。書き出し後に開き直して文字列をassertする一手間 を入れておくと、こういう「保存はされているのに見た目が古い」系のトラップに早く気づけます。
ここまでやって、ようやく16言語ぶんのスクショを「毎回ゼロから打ち直す」状態から抜け出せました。次に同じアプリのデザインを更新しても、翻訳辞書とフォント指定を直すだけで作り直せます。個人開発で世界中のユーザーに届けたいと思ったとき、言語の壁はこういう地道な工程の積み重ねで一枚ずつ薄くしていくものなんだ、と改めて感じています。1997年にインターネットで初めて国境を越えてつながったときの感覚を、いまは自分のアプリでなぞっているのかもしれません。
同じように多言語のストア素材を量産しようとしている方の参考になれば嬉しいです。まずは自分のアプリの原本PSDで、テキストレイヤーが EngineData として拾えるかどうかを確認するところから始めてみてください。