Files
FamilyMealPlanner/fastlane/screenshot_localize/generate.py
T
alexandrev-tibco 619d5f921d 1.2.0: localized App Store screenshots (es/de/fr/it/pt-BR) + 1.2.0 release notes
- Caption-localized the 5 iPhone 6.7" + 5 iPad 12.9" screenshots into the 5
  non-English locales, reusing the en-US framed captures (in-frame UI stays EN).
  Tooling in fastlane/screenshot_localize/ (Pillow: repaints the pastel caption
  band, re-renders headline/subtitle in SF Pro; auto-fits + wraps per language).
- Rewrote release_notes 1.2.0 in all 6 languages: since 1.1.5 never shipped
  publicly (last public = 1.1.4), the notes consolidate 1.1.5 + 1.2.0 features.
- Fastfile: new `publish` lane — uploads metadata + screenshots for an existing
  TestFlight build (skip_binary_upload, no submit) so offer codes can be set up
  before submission.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkanrydYtrme8wipTzWssG
2026-07-09 10:42:44 +02:00

143 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""Localize App Store caption bands. Repaints top band + redraws localized text."""
import json, os
from PIL import Image, ImageDraw, ImageFont
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "..", "en-US")
FONT = "/System/Library/Fonts/SFNS.ttf"
LANGS = ["es-ES", "de-DE", "fr-FR", "it", "pt-BR"]
FILES = [ # (index, filename, device)
(i, dev) for i in range(5)
for dev in ("IPHONE_67", "IPAD_PRO_3GEN_129")
]
with open(os.path.join(HERE, "translations.json")) as f:
T = json.load(f)
def font(size, weight):
ft = ImageFont.truetype(FONT, size)
try:
ft.set_variation_by_name(weight)
except Exception:
pass
return ft
def detect_phone_top(im):
w, h = im.size
px = im.load()
def dark(c): return c[0] < 70 and c[1] < 70 and c[2] < 70
for y in range(int(h * 0.10), int(h * 0.6), 2):
run = mx = 0
for x in range(0, w, 4):
run = run + 4 if dark(px[x, y]) else 0
if run > mx:
mx = run
if mx > w * 0.35:
return y
return int(h * 0.20)
def wrap(draw, text, ft, max_w, max_lines):
words = text.split()
lines, cur = [], ""
for wd in words:
trial = (cur + " " + wd).strip()
if draw.textlength(trial, font=ft) <= max_w or not cur:
cur = trial
else:
lines.append(cur)
cur = wd
if cur:
lines.append(cur)
return lines[:max_lines] if len(lines) <= max_lines else None
def fit_headline(draw, text, max_w, start, floor, weight, max_lines=3):
size = start
while size >= floor:
ft = font(size, weight)
lines = wrap(draw, text, ft, max_w, max_lines)
if lines and all(draw.textlength(l, font=ft) <= max_w for l in lines):
return ft, lines, size
size -= 2
ft = font(floor, weight)
lines = wrap(draw, text, ft, max_w, max_lines) or [text]
return ft, lines, floor
def block_height(draw, lines, ft, lh):
asc, desc = ft.getmetrics()
return int((asc + desc) * lh * len(lines))
def render(base_path, out_path, headline, subtitle, is_ipad):
im = Image.open(base_path).convert("RGB")
w, h = im.size
bg = im.getpixel((10, 10))
phone_top = detect_phone_top(im)
d = ImageDraw.Draw(im)
# erase band
d.rectangle([0, 0, w, phone_top], fill=bg)
max_w = int(w * 0.88)
if is_ipad:
h_start, h_floor, s_size = 96, 60, 66
else:
h_start, h_floor, s_size = 72, 44, 50
lh = 1.12
hf, hlines, hpx = fit_headline(d, headline, max_w, h_start, h_floor, "Bold")
sf = font(s_size, "Regular")
slines = wrap(d, subtitle, sf, max_w, 3) or [subtitle]
hb = block_height(d, hlines, hf, lh)
sb = block_height(d, slines, sf, lh)
gap = int(s_size * 0.6)
total = hb + gap + sb
# vertically center within [0, phone_top) with slight upward bias
bottom_gap = int(phone_top * 0.12)
avail = phone_top - bottom_gap
y = max(int(phone_top * 0.06), (avail - total) // 2)
def draw_lines(lines, ft):
nonlocal y
asc, desc = ft.getmetrics()
step = int((asc + desc) * lh)
for ln in lines:
tw = d.textlength(ln, font=ft)
d.text(((w - tw) / 2, y), ln, font=ft, fill=(0, 0, 0))
y += step
draw_lines(hlines, hf)
y += gap
draw_lines(slines, sf)
im.save(out_path)
ok = y <= phone_top
return hpx, len(hlines), len(slines), ok, y, phone_top
rows = []
for lang in LANGS:
outdir = os.path.join(HERE, "..", lang)
os.makedirs(outdir, exist_ok=True)
for i, dev in FILES:
fn = f"{i}_APP_{dev}_{i}.png"
base = os.path.join(SRC, fn)
out = os.path.join(outdir, fn)
tr = T[lang][i]
hpx, hl, sl, ok, ybot, pt = render(base, out, tr["h"], tr["s"], "IPAD" in dev)
rows.append((lang, i, "ipad" if "IPAD" in dev else "iphone", hpx, hl, sl, ok, ybot, pt))
print(f"{'lang':7} {'i':2} {'dev':6} {'hpx':4} {'hL':3} {'sL':3} {'ok':3} {'ybot':5} {'ptop':5}")
bad = 0
for r in rows:
print(f"{r[0]:7} {r[1]:<2} {r[2]:6} {r[3]:<4} {r[4]:<3} {r[5]:<3} {str(r[6]):5} {r[7]:<5} {r[8]:<5}")
if not r[6]:
bad += 1
print(f"\nTotal images: {len(rows)} | text-overlaps-phone failures: {bad}")