cross-store text: other platforms named in App Store text (2.3.10: Android, Google Play, ...) or Play text (Metadata: iOS, App Store, Game Center, ...) are refused by every text write (apple / play listing, play release / upload notes, listing set, whats-new, storebound edit, desktop editor), blockers in check / validate, scan other-platforms / play-other-platforms; listing.allowPlatformMentions for the owner's functional mentions; whats-new splits [apple] / [play] lines per store; storebound api --yes without a TTY is refused (exit 1, stderr) instead of a silent dry run; pitfalls 77, 78

This commit is contained in:
CRTkafa 2026-10-20 02:57:25 +07:00
parent 3e4015b686
commit edabbec874
32 changed files with 613 additions and 53 deletions

View file

@ -4,6 +4,10 @@
// Pure, no Node imports: the desktop window runs it on every keystroke, `storebound edit` before it writes.
import { auditAppleKeywords, auditPlayListing } from './aso';
import { APPLE_LIMITS, type AppleCopy, len, PLAY_LIMITS } from './copy';
import { fieldsMentions, GUIDELINE, type PlatformAllow, type PlatformHit } from './cross-store';
const mentionIssue = (h: PlatformHit): FieldIssue => ({ level: 'error', field: h.field,
msg: `line ${h.line}: "${h.term}": other platforms or stores in ${h.store === 'apple' ? 'App Store' : 'Google Play'} text get it rejected (${GUIDELINE[h.store]}); ${h.fix}` });
export type IssueLevel = 'error' | 'warn' | 'info';
export interface FieldIssue { level: IssueLevel; field: string; msg: string }
@ -12,8 +16,6 @@ export interface FieldIssue { level: IssueLevel; field: string; msg: string }
export const COMPETITORS = ['instagram', 'tiktok', 'whatsapp', 'facebook', 'snapchat', 'youtube', 'netflix', 'spotify', 'telegram', 'messenger', 'twitter', 'pinterest', 'uber', 'airbnb',
'amazon', 'chatgpt', 'openai', 'minecraft', 'roblox', 'fortnite', 'pubg', 'candy crush', 'clash of clans', 'pokemon', 'pokémon', 'among us', 'subway surfers', 'temple run',
'angry birds', 'tetris', 'wordle', 'duolingo', 'zoom', 'gmail', 'google maps', 'waze', 'shazam', 'tinder', 'notion', 'evernote', 'photoshop', 'canva', 'capcut', 'iphone', 'ipad', 'apple'];
/** Other mobile platforms or stores in App Store text (2.3.10). */
export const OTHER_PLATFORMS = /\b(Android|Google Play|Play Store|Galaxy Store|AppGallery|Huawei|Windows Phone|BlackBerry|Amazon Appstore)\b/;
// Developer markers count only in capitals ("TODO:", "FIXME"): "todo" is a real word and keyword (a to-do app).
export const PLACEHOLDER_MARKERS = /\bTODO\b|\bTBD\b|\bFIXME\b|\bXXX\b/;
export const PLACEHOLDER_TEXT = /lorem ipsum|\bplaceholder\b|your app (name|description) here|coming soon|First paragraph:|Short hook under the name|One or two sentences you can change|İsmin altındaki kısa kanca|Yeni sürüm göndermeden istediğin zaman|\bExample (Game|App|Pro|Studio)\b/i;
@ -31,7 +33,7 @@ export const APPLE_REQUIRED = ['subtitle', 'promotionalText', 'keywords', 'descr
* (apple.name, else app.name). Errors stop a save (the store would refuse it, or the listing file would no longer
* parse); warnings and hints do not.
*/
export function appleLocaleIssues(locale: string, v: AppleCopy, appName: string): FieldIssue[] {
export function appleLocaleIssues(locale: string, v: AppleCopy, appName: string, allow: readonly PlatformAllow[] = []): FieldIssue[] {
const out: FieldIssue[] = [];
for (const f of APPLE_REQUIRED) if (!(v[f] ?? '').trim()) out.push({ level: 'error', field: f, msg: `${locale}: ${f} is required` });
for (const [f, lim] of Object.entries(APPLE_LIMITS) as [keyof typeof APPLE_LIMITS, number][]) {
@ -63,19 +65,19 @@ export function appleLocaleIssues(locale: string, v: AppleCopy, appName: string)
for (const f of ['name', 'subtitle', 'promotionalText', 'keywords', 'description', 'whatsNew'] as const) {
const t = f === 'name' ? (v.name ?? '') : v[f] ?? '';
if (!t) continue;
const o = OTHER_PLATFORMS.exec(t);
if (o) out.push({ level: 'warn', field: f, msg: `"${o[0]}": other platforms or stores in App Store text get it rejected (2.3.10)` });
if (PLACEHOLDER_MARKERS.test(t) || PLACEHOLDER_TEXT.test(t)) out.push({ level: 'warn', field: f, msg: 'placeholder text (template or TODO) is still in it' });
const b = f === 'whatsNew' ? null : BETA_WORDS.exec(t);
if (b && !/\bdemo account\b|\bdemo (video|mode)\b/i.test(t)) out.push({ level: 'warn', field: f, msg: `"${b[0]}": demos, betas and trials belong on TestFlight (2.2)` });
}
// 2.3.10: other platforms or stores (Android, Google Play, ...) stop the save (pitfall 77)
out.push(...fieldsMentions('apple', locale, { ...v, name: v.name ?? '' }, allow).map(mentionIssue));
return out;
}
export interface PlayValues { title?: string; short?: string; full?: string; whatsNew?: string }
/** Issues of one Google Play language (title falls back to app.name). */
export function playLangIssues(lang: string, v: PlayValues, appName: string): FieldIssue[] {
export function playLangIssues(lang: string, v: PlayValues, appName: string, allow: readonly PlatformAllow[] = []): FieldIssue[] {
const out: FieldIssue[] = [];
for (const f of PLAY_REQUIRED) if (!(v[f] ?? '').trim()) out.push({ level: 'error', field: f, msg: `${lang}: the ${f} description is required` });
const title = (v.title ?? '').trim() || appName;
@ -93,6 +95,8 @@ export function playLangIssues(lang: string, v: PlayValues, appName: string): Fi
const t = v[f] ?? '';
if (t && (PLACEHOLDER_MARKERS.test(t) || PLACEHOLDER_TEXT.test(t))) out.push({ level: 'warn', field: f, msg: 'placeholder text (template or TODO) is still in it' });
}
// Play Metadata policy: iOS, App Store, ... in Play text stop the save (pitfall 77)
out.push(...fieldsMentions('play', lang, { ...v }, allow).map(mentionIssue));
return out;
}