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:
parent
3e4015b686
commit
edabbec874
32 changed files with 613 additions and 53 deletions
|
|
@ -17,6 +17,10 @@
|
|||
// the translation. Existing translated files are kept unless --force.
|
||||
// storebound whats-new check [--dir store/whats-new] [--json] limits per store (Play 500, App Store 4000), untranslated files
|
||||
// storebound whats-new apply [--dir store/whats-new] [--dry] [--json] checked files -> both listing files
|
||||
// Per store: a line tagged `[apple]` or `[play]` (also `• [play] ...`, or a `[play]` line heading a block up to the next
|
||||
// blank line) goes only into that store's notes. check / apply refuse notes that name the other side's platform
|
||||
// (Android / Google Play in App Store notes: Guideline 2.3.10; iOS / App Store in Play notes) unless
|
||||
// listing.allowPlatformMentions allows the line; the draft leaves such untagged lines out of the other store (pitfall 77).
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { Language } from './lib/config';
|
||||
|
|
@ -25,10 +29,11 @@ import type { Ctx } from './lib/ctx';
|
|||
import { done, log, printJson, warn } from './lib/log';
|
||||
import { textTable } from './lib/table';
|
||||
import {
|
||||
applyLanguageNotes, checkLanguageNotes, checkNotes, cutNotes, findNotesSource, formatNotes, type LangCheck, NOTES_LIMITS, type NoteItem, type NotesFile, noteItems, notesFileText, parseNotesFile,
|
||||
applyLanguageNotes, checkLanguageNotes, checkNotes, cutNotes, draftForStore, findNotesSource, formatNotes, itemsForStore, languageNoteMentions, type LangCheck, NOTES_LIMITS, type NoteItem, type NotesFile, noteItems, notesFileText, parseNotesFile,
|
||||
resolveLanguages, setAppleWhatsNew, setPlayWhatsNew,
|
||||
} from './lib/whats-new';
|
||||
import * as rt from './lib/rt';
|
||||
import { hitLine } from './lib/cross-store';
|
||||
|
||||
const TAG = 'whats-new';
|
||||
const storeCode = (l: Language): string => l.apple ?? playCodes(l)[0] ?? l.id;
|
||||
|
|
@ -52,14 +57,16 @@ function printChecks(checks: LangCheck[]): void {
|
|||
for (const c of checks) for (const w of c.warnings) warn(TAG, `${c.lang}: ${w}`);
|
||||
}
|
||||
|
||||
async function languagesFlow(ctx: Ctx, draft: { play: string; apple: string }): Promise<void> {
|
||||
async function languagesFlow(ctx: Ctx, draft: { play: string; apple: string; shared: string }): Promise<void> {
|
||||
const { config, resolve } = ctx.loaded;
|
||||
const a = ctx.args;
|
||||
const dir = resolve(a.get('dir') ?? 'store/whats-new');
|
||||
const langs = resolveLanguages(config.languages, a.list('languages'));
|
||||
const [src, ...rest] = langs;
|
||||
// the source text: the shorter Play draft when the source language is on Play (it has to fit 500)
|
||||
const text = playCodes(src!).length ? draft.play : draft.apple;
|
||||
// the source text: the shorter Play draft when the source language is on Play (it has to fit 500); with [apple] /
|
||||
// [play] tags the shared text keeps them (apply splits it per store)
|
||||
const tagged = draft.shared !== draft.play || draft.shared !== draft.apple;
|
||||
const text = tagged && /\[(apple|play|ios|android)\]/i.test(draft.shared) ? draft.shared : playCodes(src!).length ? draft.play : draft.apple;
|
||||
const files: { l: Language; f: NotesFile; write: boolean; note: string }[] = [{ l: src!, f: { lang: src!.id, source: src!.id, status: 'source', text }, write: true, note: 'draft (source)' }];
|
||||
const hook = a.get('translate');
|
||||
for (const l of rest) {
|
||||
|
|
@ -97,6 +104,10 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const files = readDir(dir, config.languages);
|
||||
if (!files.length) throw new Error(`${dir}: no <language id>.md files`);
|
||||
const checks = checkAll(files, config.whatsNew?.allowWords ?? []);
|
||||
// other platforms named in a store's notes: a blocker for check and apply (pitfall 77)
|
||||
const hits = files.flatMap(({ l, f }) => languageNoteMentions(l, f.text, config.listing?.allowPlatformMentions));
|
||||
for (const h of hits) { const c = checks.find(x => x.lang === files.find(y => playCodes(y.l).includes(h.locale) || y.l.apple === h.locale)?.l.id); c?.problems.push(`${h.store === 'apple' ? 'App Store' : 'Google Play'} line ${h.line}: "${h.term}" (${h.store === 'apple' ? '2.3.10' : 'Play: Metadata'})`); }
|
||||
for (const h of hits) warn(TAG, `refused: ${hitLine(h)}`);
|
||||
const bad = checks.filter(c => c.problems.some(p => !p.startsWith('first line')));
|
||||
if (a._[0] === 'check' || bad.length) {
|
||||
if (a.has('json')) printJson({ dir, checks, ok: !bad.length });
|
||||
|
|
@ -124,21 +135,30 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const version = a.get('release') ?? (() => { try { return (JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { version?: string }).version; } catch { return undefined; } })();
|
||||
const source = a.has('from-git') ? undefined : findNotesSource({ version, source: config.whatsNew?.source, read: rel => { const p = resolve(rel); return existsSync(p) ? readFileSync(p, 'utf8') : undefined; } });
|
||||
const hook = a.get('hook');
|
||||
let play: string, apple: string, subjects: string[] = [], items: NoteItem[] = [];
|
||||
let play: string, apple: string, shared: string, subjects: string[] = [], items: NoteItem[] = [];
|
||||
if (source) {
|
||||
log(TAG, `notes from ${source.path} (${source.kind === 'changelog' ? 'CHANGELOG section' : source.kind === 'release-doc' ? 'release doc' : 'whatsNew.source'}), not the git log`);
|
||||
const body = hook ? `${hook}\n${source.text}` : source.text;
|
||||
play = cutNotes(body, NOTES_LIMITS.play);
|
||||
apple = cutNotes(body, NOTES_LIMITS.apple);
|
||||
const dp = draftForStore(body, 'play', config.listing?.allowPlatformMentions), da = draftForStore(body, 'apple', config.listing?.allowPlatformMentions);
|
||||
for (const [s, d] of [['Google Play', dp], ['App Store', da]] as const) for (const l of d.dropped) warn(TAG, `left out of the ${s} draft (names another platform; tag it [${s === 'App Store' ? 'play' : 'apple'}] in ${source.path}): ${l}`);
|
||||
shared = cutNotes(body, NOTES_LIMITS.apple);
|
||||
play = cutNotes(dp.text, NOTES_LIMITS.play);
|
||||
apple = cutNotes(da.text, NOTES_LIMITS.apple);
|
||||
} else {
|
||||
subjects = git(['log', range, '--no-merges', '--format=%s'], root).split('\n').filter(Boolean);
|
||||
items = noteItems(subjects);
|
||||
log(TAG, `${subjects.length} commits since ${since || 'the first commit'} -> ${items.length} items, drafted from commit messages (no docs/RELEASE-<version>.md, CHANGELOG section or whatsNew.source): rewrite them in player words`);
|
||||
play = formatNotes(items, { hook, limit: NOTES_LIMITS.play });
|
||||
apple = formatNotes(items, { hook, limit: NOTES_LIMITS.apple });
|
||||
const pick = (store: 'play' | 'apple') => {
|
||||
const d = draftForStore(formatNotes(itemsForStore(items, store), { hook, limit: NOTES_LIMITS[store] }), store, config.listing?.allowPlatformMentions);
|
||||
for (const l of d.dropped) warn(TAG, `left out of the ${store === 'apple' ? 'App Store' : 'Google Play'} draft (names another platform): ${l}`);
|
||||
return d.text;
|
||||
};
|
||||
shared = formatNotes(items, { hook, limit: NOTES_LIMITS.apple });
|
||||
play = pick('play');
|
||||
apple = pick('apple');
|
||||
}
|
||||
for (const [store, text] of [['play', play], ['apple', apple]] as const) for (const p of checkNotes(text, store, allow)) warn(TAG, `${store}: ${p}`);
|
||||
if (a.has('languages')) return languagesFlow(ctx, { play, apple });
|
||||
if (a.has('languages')) return languagesFlow(ctx, { play, apple, shared });
|
||||
if (a.has('json')) printJson({ since: since || null, source: source ? { kind: source.kind, path: source.path } : null, commits: subjects.length, items, play, apple, limits: NOTES_LIMITS });
|
||||
else console.log(`--- draft (Play, ${[...play].length}/500) ---\n${play}\n---`);
|
||||
if (a.has('dry')) return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue