upload --pre-check/--evidence, shots copy translate, game-center --locale-map, play games API-disabled hint

This commit is contained in:
CRTkafa 2026-10-24 12:10:23 +08:00
parent 29a9683ab7
commit 8ffa27d4f8
24 changed files with 598 additions and 27 deletions

View file

@ -65,6 +65,8 @@ export interface GamesConfig {
gameCenter?: boolean;
/** Play Games Services: the games project id (the digits of games_APP_ID / com.google.android.gms.games.APP_ID). */
playGames?: { appId?: string };
/** Game Center only: config language id (or its apple locale) -> the locale the live Game Center uses, e.g. { en: 'en-GB' } when the app's English is en-GB there but the language says en-US. `storebound apple game-center --locale-map en=en-GB` overrides. */
localeMap?: Record<string, string>;
leaderboards?: GameLeaderboard[];
achievements?: GameAchievement[];
}
@ -78,6 +80,10 @@ export function validateGames(g: GamesConfig | undefined, languages: Language[]
if (g.gameCenter !== undefined && typeof g.gameCenter !== 'boolean') e.push('games.gameCenter: true | false');
if (g.playGames !== undefined && (typeof g.playGames !== 'object' || (g.playGames.appId !== undefined && !/^\d{6,20}$/.test(String(g.playGames.appId)))))
e.push('games.playGames.appId: the digits of the Play Games Services project id (games_APP_ID)');
if (g.localeMap !== undefined) {
if (!g.localeMap || typeof g.localeMap !== 'object' || Array.isArray(g.localeMap)) e.push('games.localeMap: an object { en: "en-GB" } (config language id or locale -> Game Center locale)');
else for (const [k, v] of Object.entries(g.localeMap)) if (typeof v !== 'string' || !/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(v)) e.push(`games.localeMap.${k}: "${String(v)}" is not a locale like en-GB`);
}
const langs = new Set(languages.map(l => l.id));
const seen = new Set<string>();
const checkId = (kind: string, id: unknown) => {
@ -133,7 +139,41 @@ export function changedFields(have: Record<string, unknown> | undefined, want: R
return Object.keys(want).filter(k => JSON.stringify(have?.[k] ?? null) !== JSON.stringify(want[k] ?? null));
}
const appleLocale = (languages: Language[], id: string): string | undefined => languages.find(l => l.id === id)?.apple;
const appleLocale = (languages: Language[], id: string, map: Record<string, string> = {}): string | undefined => {
const apple = languages.find(l => l.id === id)?.apple;
return map[id] ?? (apple ? map[apple] : undefined) ?? apple;
};
/** The effective Game Center locale map: games.localeMap, then --locale-map en=en-GB,tr=tr (flag entries win). */
export function gcLocaleMap(g: GamesConfig | undefined, flag: string[] = []): Record<string, string> {
const out: Record<string, string> = { ...(g?.localeMap ?? {}) };
for (const item of flag.flatMap(x => x.split(',')).map(x => x.trim()).filter(Boolean)) {
const m = /^([A-Za-z0-9_-]+)=([A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*)$/.exec(item);
if (!m) throw new Error(`--locale-map "${item}": use <config language id or locale>=<Game Center locale>, e.g. en=en-GB (several: en=en-GB,pt=pt-PT)`);
out[m[1]!] = m[2]!;
}
return out;
}
/** A suggestion per config language whose Game Center locale is not on the live Game Center while exactly one other
* variant of the same language is (en-US configured, only en-GB live): the map entry that stops the spurious creates. */
export function suggestLocaleMap(languages: Language[], liveLocales: string[], map: Record<string, string> = {}): { id: string; from: string; to: string }[] {
const live = [...new Set(liveLocales)];
if (!live.length) return [];
const out: { id: string; from: string; to: string }[] = [];
for (const l of languages) {
const from = appleLocale(languages, l.id, map);
if (!from || live.includes(from) || map[l.id] || map[l.apple ?? '']) continue;
const base = from.split('-')[0]!.toLowerCase();
const same = live.filter(x => x.split('-')[0]!.toLowerCase() === base);
if (same.length === 1) out.push({ id: l.id, from, to: same[0]! });
}
return out;
}
/** The live locales of a Game Center read (every leaderboard and achievement localization). */
export const liveGcLocales = (have: { leaderboards: GcExisting[]; achievements: GcExisting[] }): string[] =>
[...have.leaderboards, ...have.achievements].flatMap(x => x.localizations.map(l => String(l.attributes.locale ?? '')).filter(Boolean));
export function gcLeaderboardAttrs(l: GameLeaderboard): Record<string, unknown> {
return {
@ -151,7 +191,7 @@ export interface GcExisting { id: string; attributes: Record<string, unknown>; l
/** Game Center plan: matched by vendorIdentifier; create what is missing, PATCH what differs, keep the rest. Archived
* items on the store are left alone (an archived vendor id cannot be reused; the plan names it). */
export function planGameCenter(g: GamesConfig, languages: Language[], have: { leaderboards: GcExisting[]; achievements: GcExisting[] }): ItemPlan[] {
export function planGameCenter(g: GamesConfig, languages: Language[], have: { leaderboards: GcExisting[]; achievements: GcExisting[] }, localeMap: Record<string, string> = {}): ItemPlan[] {
const out: ItemPlan[] = [];
const one = (kind: ItemPlan['kind'], id: string, attrs: Record<string, unknown>, locs: Record<string, Record<string, unknown>>, list: GcExisting[]) => {
const cur = list.find(x => x.attributes.vendorIdentifier === id);
@ -166,7 +206,7 @@ export function planGameCenter(g: GamesConfig, languages: Language[], have: { le
for (const l of (g.leaderboards ?? []).filter(x => onStore(x, 'apple'))) {
const locs: Record<string, Record<string, unknown>> = {};
for (const [k, v] of Object.entries(l.locales)) {
const loc = appleLocale(languages, k);
const loc = appleLocale(languages, k, localeMap);
if (loc) locs[loc] = { name: v.name, ...(v.suffix ? { formatterSuffix: v.suffix } : {}) };
}
one('leaderboard', l.id, gcLeaderboardAttrs(l), locs, have.leaderboards);
@ -174,7 +214,7 @@ export function planGameCenter(g: GamesConfig, languages: Language[], have: { le
for (const a of (g.achievements ?? []).filter(x => onStore(x, 'apple'))) {
const locs: Record<string, Record<string, unknown>> = {};
for (const [k, v] of Object.entries(a.locales)) {
const loc = appleLocale(languages, k);
const loc = appleLocale(languages, k, localeMap);
if (loc) locs[loc] = { name: v.name, beforeEarnedDescription: v.description, afterEarnedDescription: v.earned ?? v.description };
}
one('achievement', a.id, gcAchievementAttrs(a), locs, have.achievements);