デザイナーから受け取ったPSDを開いた瞬間、手が止まりました。1440×13333px、220MB。Android版の壁紙アプリ「綺麗な壁紙」のストア掲載スクリーンショットで、日本語で組まれたキャッチコピーが5枚のアートボードに散らばっています。これを少なくとも10言語、できれば15言語に展開したい。けれど翌朝にはストアの差し替え作業を始めたい、という状況でした。
手作業でPhotoshopのテキストレイヤーを1枚ずつ翻訳して差し替えると、5枚 × 15言語で75画面分。コピー&ペーストの取り違えが必ず起きます。そこでCoworkに、このPSDそのものを渡して機械的に処理させることにしました。
やってみると、翻訳よりもフォントと字形のほうがずっと手強い、という当たり前の現実にぶつかります。この記事は、その一晩の作業ログです。
なぜ「スクショのローカライズ」だけは作品制作と切り離せるのか
最初に自分の中で整理したのは、ここでAIに任せてよいのかという線引きでした。個人開発を続けながら現代美術の作品制作も行っていますが、作品本体には一貫してAIを使っていません。手を動かして作ること自体に意味がある、という感覚があるからです。
一方で、作品やアプリを「世に出す」作業は、制作そのものとは別物です。ストアのスクリーンショットは、壁紙という商品の中身ではなく、それを店頭に並べるための値札やポップに近い。だからスクリーンショットの版組みとローカライズは、私の中では迷いなくAIに任せられるデザイナー業務に分類されます。
作品の制作とプロモーションを意識して分けておくと、こういう判断が一瞬で済みます。逆にこの線引きが曖昧なままだと、「どこまで自動化してよいのか」を毎回悩むことになり、手が止まります。
アップロードしたPSDの中身を、まず数える
ローカライズの前に、PSDが何でできているのかを正確に把握します。Coworkの作業環境にはPython版のpsd-toolsが入るので、開いて構造をダンプさせました。
なお、この環境の pip は PEP 668 で保護されているため、そのままでは externally-managed-environment で止まります。この点はCowork の Bash で pip install が externally-managed-environment エラーで止まるとき に詳しくまとめていますので、先に読んでおくと詰まりません。
from psd_tools import PSDImage
psd = PSDImage.open( "screenshot.psd" )
print ( f "canvas: { psd.width } x { psd.height } " )
for layer in psd.descendants():
kind = layer.kind # 'type'(テキスト) / 'pixel' / 'group' など
if kind == "type" :
print ( f "[text] { layer.name !r } @ { layer.bbox } -> { layer.text !r } " )
elif kind == "group" :
print ( f "[group] { layer.name !r } children= { len ( list (layer)) } " )
出力で分かったのは、5枚のアートボードがそれぞれグループになっていて、1枚あたりおよそ40個のレイヤー(うちテキストは見出し・説明の2系統)で構成されていること。
ここで重要なのは、ワークスペースに前からあったPSDとアップロード版で解像度が違った点でした。正本はアップロードされた1440×13333px・220MBのほうなので、抽出も描画もこのファイルから行うようスクリプトを固定しました。受け取ったファイルが複数あるときは、どれが正本かを最初に1回だけ確定させておくと、後の作り直しを防げます。
クラウド同期フォルダを介してPSDを受け渡す場合、bashからはファイルが見えないのに Finder には見える、という現象も起こります。これはCowork の bash が『ファイルがありません』と言うのに Finder には見える理由 で扱いました。220MBのファイルを扱う前に、実体化を済ませておくと安全です。
テキストレイヤーの「名前」に翻訳文そのものを入れる
ローカライズというと、テキストレイヤーの中身(text)を翻訳文に書き換えるのが素直な発想です。実際それもやるのですが、私はそれに加えて、各テキストレイヤーのレイヤー名 を翻訳文そのものに設定しました。
理由は、Photoshopのレイヤーパネルを開いたときに「どの言語のどの文か」が一目で分かるようにするためです。レイヤー名が headline_1 のままだと、字形が表示されない言語では中身が読めません。名前に翻訳文を入れておけば、たとえタイ語のように環境にグリフが無くてラスタライズできない言語でも、パネル上で内容を確認し、コピーして使えます。
手作業のブラッシュアップ前提のPSDを返すなら、この一手間が後工程をいちばん助けます。
言語ごとにグループを作り、グループ名は th_ไทย ko_한국어 のように「言語コード + その言語自身での言語名」にしました。英語名(Korean)ではなく現地語名にしておくと、各言語の担当に渡したときに迷いがありません。
フォントと字形の壁 — タイ語と韓国語でつまずく
ここからが本番です。テキストを書き換えても、その字形を描けるフォントが環境に無ければ、ラスタライズした瞬間に豆腐(□)になります。作業環境はネットワークが制限されていてNotoフォントを取りに行けなかったため、入手済みのフォントでどこまでカバーできるかを先に確認しました。
スクリプト / 言語
試したフォント
結果
取った判断
ラテン文字(英・独・仏・西・伊・トルコ語 ほか)
DejaVu Sans
全グリフ描画可
そのままラスタ配置
キリル文字(ロシア語)
DejaVu Sans
全グリフ描画可
そのままラスタ配置
アラビア文字(アラビア語・40レイヤー)
DejaVu Sans
描画可(連結形は要目視)
ラスタ配置し、最終確認は人間が実施
中国語(簡体・35レイヤー)
Droid Sans Fallback
描画可
そのままラスタ配置
韓国語(ハングル)
Droid Sans Fallback
一部グリフ欠落
ハングルが出る代替へ切替、残りは名前のみ
タイ語
環境に該当フォントなし
描画不可(豆腐)
ラスタ配置せず、レイヤー名とExcelで受け渡し
結論として、タイ語はPSDにラスタ配置せず、レイヤー名(=翻訳文)とExcel表での受け渡しに留める 判断をしました。最終的に、ラテン系15言語とアラビア語・中国語は描画できましたが、タイ語・韓国語の一部は字形不足で「名前のみ」として残しています。
フォントは後で本番用(Noto / IPAex系)に差し替える前提だったので、ここで完璧を目指さず「差し替え地点を明示して渡す」ほうが現実的でした。公式の体裁を整えるのは、グリフが揃った環境でやればよいのです。
入れ子グループが保存・再オープンで壊れないかを、小さく潰す
5枚のアートボード × 言語別グループという入れ子構造に、新しいピクセルレイヤー(ラスタ化した訳文)を追加していきます。psd-toolsで複雑な階層を編集すると、保存して開き直したときに構造が壊れることがあります。220MBの本番ファイルでそれが起きると、保存だけで数分かかるため発見が遅れます。
そこで本番の前に、小さなPSDで「入れ子グループ + サブグループにピクセルレイヤーを足して保存 → 再オープン」が無事かを検証しました。
from psd_tools import PSDImage
def verify_roundtrip (path: str , expected_boards: int ) -> None :
"""保存済みPSDを開き直し、構造が保たれているかを機械的に確かめる。"""
psd = PSDImage.open(path)
boards = [l for l in psd if l.kind == "group" ]
assert len (boards) == expected_boards, f "artboards: { len (boards) } != { expected_boards } "
added = [l for l in psd.descendants() if l.kind == "pixel" and l.name.startswith( "t_" )]
assert added, "追加したピクセルレイヤーが再オープン後に消えています"
for l in added:
assert l.bbox != ( 0 , 0 , 0 , 0 ), f " { l.name } の座標が失われています"
print ( f "ok: boards= { len (boards) } added_layers= { len (added) } " )
verify_roundtrip( "mini_test.psd" , expected_boards = 5 )
assert を書いておくと、目視で「たぶん大丈夫」と流す余地がなくなります。アートボード5枚が保持され、追加したレイヤーの種別(pixel)も座標も正常に読めることを確認してから、本番ビルドに進みました。重い処理ほど、先にミニチュアで一往復させておく価値があります。
psd-tools が非ASCIIレイヤー名で落ちる(mac_roman問題)
描画自体は通ったのに、保存でエラーが出ました。原因は、psd-toolsがレイヤー名を旧来のmac_roman形式で書き出そうとするため、한국어 や Русский のような非ASCII文字を含む名前でエンコードに失敗していたことです。
レイヤー名に翻訳文や現地語名を入れる方針と真っ向からぶつかる罠でした。
PSDのレイヤー名は、実は二重構造になっています。ひとつはレコード本体にあるレガシーなPascal文字列(ASCII前提、mac_romanで書かれる)。もうひとつは追加情報ブロックの luni(Unicode Layer Name)で、こちらはUTF-16BEで正しく多言語を保持できます。Photoshopは読み込み時に luni を優先します。
つまり、レガシー側にはASCIIの安全な代替名だけを置き、luni に本当の名前を書けば、保存も表示も両立します。
from psd_tools.psd.tagged_blocks import TaggedBlocks, TaggedBlock
from psd_tools.constants import Tag
import re
def safe_ascii (name: str , index: int ) -> str :
"""レガシー領域用。非ASCIIを落とし、空になったら連番で埋める。"""
ascii_only = re.sub( r " [ ^ \x20 - \x7E ] " , "" , name).strip()
return ascii_only or f "layer_ { index } "
def set_layer_name (record, name: str , index: int ) -> None :
"""レガシー名はASCII、実体は luni(UTF-16BE) に書き込む。"""
record.name = safe_ascii(name, index) # mac_roman で必ず通る値だけを渡す
blocks = record.tagged_blocks or TaggedBlocks()
blocks[Tag. UNICODE_LAYER_NAME ] = TaggedBlock(
key = Tag. UNICODE_LAYER_NAME ,
data = name, # 'ko_한국어' などをそのまま
)
record.tagged_blocks = blocks
for i, (record, translated) in enumerate ( zip (layer_records, translations)):
set_layer_name(record, translated, i)
この分離を入れてから、保存は一度も落ちていません。「名前に多言語を入れる」設計を選ぶなら、この保存経路だけは最初に確認しておくべきでした。3回作り直したうちの2回は、この一点が原因です。
翻訳がはみ出す — フォントサイズの自動縮小を入れる
日本語のキャッチコピーは短く、同じ意味を英語やドイツ語で書くと確実に長くなります。キャンバス幅を超えたレイヤーをそのまま描画すると、文字が切れるか、隣の要素に重なります。
そこで、幅に収まるまでフォントサイズを段階的に落とす処理を入れました。下限は元の50%です。それ以下に落とすと、スクリーンショットとして読めなくなるからです。
from PIL import ImageFont
MIN_SCALE = 0.50
STEP = 0.95
def fit_font (text: str , font_path: str , base_size: int , max_width: int ):
"""max_width に収まる最大のフォントサイズを返す。収まらなければ None。"""
size = base_size
floor = int (base_size * MIN_SCALE )
while size >= floor:
font = ImageFont.truetype(font_path, size)
width = font.getbbox(text)[ 2 ]
if width <= max_width:
return font, size, size < base_size # (font, 実サイズ, 縮小したか)
size = int (size * STEP )
return None , floor, True # 下限でも収まらない → 人間に渡す
戻り値の3つ目で「縮小したか」を返しているのが要点です。縮小が発生した箇所だけをログに出し、後から目視で確認できるようにしました。
全31箇所の編集のうち、縮小対象になったのは言語によって2〜11箇所。単語が長い英語・ドイツ語ほど縮小箇所が増えます。下限50%でも収まらないケースは今回ゼロでしたが、収まらなければ None を返して処理を止め、翻訳文そのものを短くする判断を人間に戻す設計にしています。自動化が判断を奪わないよう、止まる場所を先に決めておくのが安全です。
数字の見せ方も言語をまたいで整理しました。小さなバッジは「100K+」、大きな見出しは「100,000+」と意図的に書き分けています。全16言語の対訳は、横並びで確認できるようにExcelへまとめました。表形式の集計やレポート生成をCowork側でやる手順は、Cowork × Python データ分析自動化 の流れがそのまま使えます。
作業の流れを5ステップに畳む
今回の一晩を振り返ると、再現可能な手順は次の5つに畳めます。次にスクショを差し替えるときは、この順番をそのままなぞれば迷いません。
正本の確定 : 受け取ったPSDが複数あるなら、解像度と容量で正本を1つに決める(今回は1440×13333px・220MB)
構造のダンプ : psd-toolsでアートボード数とテキストレイヤー数を数える(5枚 × 約40レイヤー)
フォントの可用性チェック : 描画予定の言語の字形が環境にあるかを先に確認する
小さなPSDで保存検証 : 入れ子グループへのレイヤー追加が再オープンで壊れないかをミニチュアで一往復させる
本番ビルドと書き出し : レイヤー名に訳文を入れて描画し、ストア別サイズへ書き出す
字形が問題なく出たのは15言語中13言語、約87%でした。残る韓国語・タイ語の2言語(約13%)は字形不足で名前のみとして残し、フォントが揃った環境での仕上げに回しています。
割合で見ておくと、どこまで自動化で到達し、どこから手作業が必要かの線引きが明確になります。私自身は、この「どこで止めて人間に渡すか」を最初に決めておくことを強く推奨します。完璧を目指して全言語を一気通貫で処理しようとすると、たいてい字形の壁で止まるからです。
Play Console と App Store Connect 用に書き出す
ブラッシュアップ後のPSDは、最終的に各ストアの推奨サイズへリサイズして書き出します。Google Play Console と App Store Connect では要求される縦横比やピクセル数が違うため、1枚のマスターから複数サイズを生成する処理を別途用意しておくと、次のバージョンのスクショ差し替えがそのまま使い回せます。
今回はマスターPSDを正本として保管し、書き出しは言語×サイズの掛け算で自動化する方針にしました。書き出した画像をストア側へ反映する工程は、Claude in Chrome で App Store Connect を自動化する で扱った手順に接続できます。
こうした「店頭に並べる作業」こそ、仕組み化の効果がいちばん大きいところです。アプリの中身は手で作る。それを世界中の人に届けるための版組みは、AIに任せて時間を生む。この線引きが、個人で多言語のストア運用を回す現実的な落としどころだと感じています。
その後の進捗 — 編集可能PSDという落としどころ
この記事を書いたあとも作業は続き、いくつか前進がありました。
最初の課題だった字形の問題は、PSDに直接ラスタライズして埋め込むのではなく、編集可能なテキストレイヤーを持つPSD(localized_editable.psd)として書き出す 方針に切り替えることで筋を通しました。各言語のグループをそのまま原本のスクショPSDにドラッグし、日本語レイヤーを非表示にして、フォントを差し替えてから書き出す、という流れです。グリフが出るかどうかは、最後にフォントを当てる工程の問題に閉じ込められます。
そのフォント差し替えのために、言語別のおすすめフォント早見表(FONT_GUIDE.md)を用意しました。デザインのBold・Medium・Lightの各ウェイトを、Noto Sans / Noto Sans KR・SC・TC・Thai・Arabic に、日本語はIPAexゴシックに対応づけています。多言語で統一感を出すなら、Notoファミリーで揃えるのが安全だと感じました。
最終的に全16言語を精査し、誤訳はなく、アプリ名もASOの正式名称に準拠していることを確認しています(ロシア語でカテゴリ名と本文の表記が軽く揺れる程度の差はありましたが、意味の誤りではありません)。
ここまで来て改めて感じるのは、限られた環境で完璧なラスタを作ろうとせず、「編集可能な状態 + フォント差し替えの指示書」という形で渡すほうが、結局いちばん速くて確実だということです。自動化の出口を「完成品」ではなく「人が仕上げられる状態」に置く。それだけで、詰まる場所がひとつ減ります。
次にやるなら、まずは手元の1枚のPSDをCoworkに渡して、テキストレイヤーの一覧をダンプさせてみてください。何枚のアートボードに、何個の文言が載っているのか。その数を見た瞬間に、手作業でやるべきでない理由がはっきりします。
私自身まだ手探りの部分が多いのですが、同じようにストア素材の多言語化で止まっている方の助けになれば嬉しいです。お読みいただきありがとうございました。