// Internal jargon in release notes: words players do not use. The one list of them; `whatsNew.allowWords` in the // config keeps a word the owner wants (a product name that collides, a game term). Pure. export type JargonKind = 'vendor' | 'dev' | 'vcs' | 'path' | 'issue' | 'identifier'; export interface JargonFinding { word: string; kind: JargonKind; plain?: string } const DROP_NAME = 'drop the name and say what changed for the player'; /** SDK, service and tool names players never see. */ export const JARGON_VENDORS: readonly string[] = [ 'Umami', 'Sentry', 'Firebase', 'Crashlytics', 'Capacitor', 'Tauri', 'Electron', 'Jenkins', 'Forgejo', 'GitHub', 'GitLab', 'Gitea', 'Gradle', 'Xcode', 'CocoaPods', 'Webpack', 'Vite', 'Docker', 'Supabase', 'RevenueCat', 'Amplitude', 'Mixpanel', 'Keeper OS', 'TestFlight', 'Fastlane', ]; /** Developer words, with a plain alternative where one is obvious (undefined: just drop it). */ export const JARGON_DEV: Readonly> = { telemetry: 'drop it, or "anonymous usage stats"', analytics: 'drop it, or "anonymous usage stats"', refactor: '"faster" / "more stable", or drop it', refactored: '"faster" / "more stable", or drop it', refactoring: '"faster" / "more stable", or drop it', endpoint: '"server"', endpoints: '"servers"', payload: '"data"', api: '"connection" / "online features"', apis: '"online features"', ci: undefined, pr: undefined, prs: undefined, fingerprint: 'say what the player notices', ota: '"in-app update"', bump: '"update"', bumped: '"updated"', deps: '"under-the-hood updates"', dependency: '"under-the-hood update"', dependencies: '"under-the-hood updates"', sdk: 'say what changed for the player', sdks: 'say what changed for the player', backend: '"server"', frontend: undefined, config: '"settings"', regression: '"a bug"', lint: undefined, linter: undefined, typecheck: undefined, nullable: undefined, async: undefined, websocket: '"live connection"', json: undefined, schema: undefined, webhook: undefined, stacktrace: '"crash"', 'stack trace': '"crash"', env: undefined, changelog: undefined, codebase: undefined, repo: undefined, }; /** Version-control words ("reads like a commit log"). */ export const JARGON_VCS: Readonly> = { commit: undefined, commits: undefined, merge: undefined, merged: undefined, rebase: undefined, 'pull request': undefined, 'merge request': undefined, revert: '"undo"', reverted: '"undid"', squash: undefined, git: undefined, 'cherry-pick': undefined, }; /** Product names that look like code identifiers. */ const NOT_IDENTIFIERS = /^(iPhone|iPad|iPod|iOS|iCloud|iMessage|macOS|watchOS|tvOS|visionOS|eBook|eSIM|eSports|YouTube|iTunes|AirPods|AirPlay)s?$/; const esc = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const wordRe = (w: string): RegExp => new RegExp(`(? w.toLowerCase())); const hits: { at: number; f: JargonFinding }[] = []; const seen = new Set(); const add = (at: number, f: JargonFinding): void => { const k = f.word.toLowerCase(); if (seen.has(k) || allowed.has(k)) return; seen.add(k); hits.push({ at, f }); }; // links are fine (a support page); look at the text without them const t = text.replace(/https?:\/\/\S+/g, m => ' '.repeat(m.length)); const scan = (words: Iterable<[string, string | undefined]>, kind: JargonKind): void => { for (const [w, plain] of words) { const m = wordRe(w).exec(t); if (m) add(m.index, { word: kind === 'vendor' ? w : m[0], kind, plain }); } }; scan(JARGON_VENDORS.map(v => [v, DROP_NAME] as [string, string]), 'vendor'); scan(Object.entries(JARGON_VCS), 'vcs'); scan(Object.entries(JARGON_DEV), 'dev'); for (const m of t.matchAll(/(? a.at - b.at).map(h => h.f); } const LABEL: Record = { vendor: 'SDK / vendor name', dev: 'developer word', vcs: 'reads like a commit log', path: 'file path', issue: 'issue id', identifier: 'code identifier', }; /** One warning line for a finding. */ export function jargonMessage(f: JargonFinding): string { return `internal jargon (${LABEL[f.kind]}): "${f.word}"${f.plain ? `; instead: ${f.plain}` : '; drop it'}`; }