creative assets: <renders>/creative/header.* / search.* fill every language without its own file (art without words needs no copy per locale; one upload, placed per locale)

This commit is contained in:
CRTkafa 2026-10-19 05:47:27 +07:00
parent c03291e7f7
commit 3542b897c8
5 changed files with 21 additions and 8 deletions

View file

@ -2,8 +2,9 @@
// 27+): the product page header (21:9) and the search results asset (3:2), image or video, per locale, with one
// universal 16:9 image (5244 x 2950) that can fill either slot. Apple's Creative assets specifications:
// https://developer.apple.com/help/app-store-connect/reference/app-information/creative-assets-specifications/
// Files: <renders>/<language id>/creative/{header,search}.{png,jpg,jpeg,mov,m4v,mp4}; universal fallback
// <renders>/<language id>/creative/universal.png or <renders>/creative/universal.png (every locale).
// Files: <renders>/<language id>/creative/{header,search}.{png,jpg,jpeg,mov,m4v,mp4}, else the same names in
// <renders>/creative/ for every locale; universal fallback <renders>/<language id>/creative/universal.png or
// <renders>/creative/universal.png (every locale).
// API: App Asset Library (placement types PRODUCT_PAGE_HEADER_ASSET and APP_STORE_SEARCH_RESULTS_ASSET); see
// cli/apple/creative-assets.ts. Play has no such slot: the feature graphic (1024 x 500) and the promo video are its
// closest equivalents.
@ -29,16 +30,20 @@ export interface CreativeFile { placement: Placement; file: string; kind: 'image
const EXT = /\.(png|jpe?g|mov|m4v|mp4)$/i;
const kindOf = (f: string): 'image' | 'video' => (CREATIVE_RULES.video.ext.test(f) ? 'video' : 'image');
/** The file for each placement of one language: own header / search file first, else the universal image. */
/** The file for each placement of one language: its own header / search file first, then a shared one for every
* language (<renders>/creative/header.png: art without words needs no copy per locale), then the universal image. */
export function creativeFiles(renders: string, lang: string, list: (d: string) => string[] = d => (existsSync(d) ? readdirSync(d) : [])): CreativeFile[] {
const dir = join(renders, lang, 'creative');
const dir = join(renders, lang, 'creative'), shared = join(renders, 'creative');
const own = list(dir).filter(f => EXT.test(f)).sort();
const common = list(shared).filter(f => EXT.test(f)).sort();
const uni = own.find(f => /^universal\.png$/i.test(f)) ? join(dir, own.find(f => /^universal\.png$/i.test(f))!)
: list(join(renders, 'creative')).some(f => f === 'universal.png') ? join(renders, 'creative', 'universal.png') : undefined;
: common.includes('universal.png') ? join(shared, 'universal.png') : undefined;
const out: CreativeFile[] = [];
for (const p of ['header', 'search'] as Placement[]) {
const f = own.find(x => x.toLowerCase().startsWith(`${p}.`));
const g = common.find(x => x.toLowerCase().startsWith(`${p}.`));
if (f) out.push({ placement: p, file: join(dir, f), kind: kindOf(f), universal: false });
else if (g) out.push({ placement: p, file: join(shared, g), kind: kindOf(g), universal: false });
else if (uni) out.push({ placement: p, file: uni, kind: 'image', universal: true });
}
return out;