desktop: Prices, Store art, Growth and App info tabs, exports at the top right and in the command palette, studio and fastlane steps in the first-run guide, fix hints on errors; fixtures, screenshots, tests, UX audit in docs/desktop.md

This commit is contained in:
CRTkafa 2026-10-11 07:40:59 +08:00
parent 029ef2e2d3
commit 5d09111528
34 changed files with 2572 additions and 67 deletions

View file

@ -527,7 +527,7 @@ export const en = {
'wz.footer': 'You can open this guide again from Settings or the command palette.',
'wz.footerSafe': 'Nothing is submitted or released from here.',
'wz.welcomeTitle': 'Every app, both stores, one window',
'wz.welcomeLead': 'storebound desktop shows where each of your apps stands on the App Store and Google Play, and runs the storebound CLI for you. Five short steps get you there.',
'wz.welcomeLead': 'storebound desktop shows where each of your apps stands on the App Store and Google Play, and runs the storebound CLI for you. Six short steps get you there.',
'wz.p1t': 'One portfolio',
'wz.p1s': 'Releases, rollouts, catalog parity, health and reviews of every app.',
'wz.p2t': 'Safe by design',
@ -552,7 +552,7 @@ export const en = {
'wz.readLead': 'One read-only pass over App Store Connect and Google Play fills the portfolio: versions, review state, rollouts and catalog parity.',
'wz.readNow': 'Read the stores now',
'wz.readDone': '{count} apps read {when}',
'wz.needApp': 'Add an app folder first (step 2).',
'wz.needApp': 'Add an app folder first (step 3).',
'wz.skip': 'Skip for now',
'wz.back': 'Back',
'wz.next': 'Next',
@ -871,5 +871,355 @@ export const en = {
'reviews.tooLong': 'Longer than the store allows ({limit} characters)',
'reviews.sendNoteD3': 'Replies are public. Save, approve, then Send: each step shows a system dialog first.',
// ---- every CLI area in the window: prices, store art, growth, app info, exports, onboarding ----
'tab.prices': 'Prices',
'tab.art': 'Store art',
'tab.growth': 'Growth',
'tab.appinfo': 'App info',
'col.date': 'Date',
'common.yes': 'Yes',
'common.no': 'No',
'export.button': 'Export',
'export.hint': 'Save what this screen shows as a file (CSV or Markdown)',
'export.saved': 'Saved',
'export.failed': 'Export failed',
'export.readFirst': 'Read it first',
'export.personal': 'personal data',
'export.prices': 'Prices (CSV, price sheet format)',
'export.reviews': 'Reviews (CSV)',
'export.health': 'Health report (Markdown)',
'export.validate': 'Store readiness report (Markdown)',
'export.catalog': 'Catalog parity (Markdown)',
'export.listings': 'Listings (Markdown)',
'export.noListings': 'No listing files in this app folder',
'export.portfolio': 'Portfolio (CSV)',
'export.noApps': 'No apps yet',
'export.declarations': 'Store declarations (Markdown)',
'export.dataSafety': 'Data safety (Play CSV)',
'export.noDataSafety': 'The config has no Data safety answers',
'export.analytics': 'Analytics, daily (CSV)',
'palette.gExport': 'Export',
'palette.export': 'Export: {what}',
'palette.prices': '{name} · Prices and price sheet',
'palette.art': '{name} · Store art gallery',
'hint.keys': 'Map a store account to this app in App folders, or add the key in Studios & accounts.',
'hint.permission': 'The key works but lacks a permission: Studios & accounts > Test shows the fix.',
'hint.config': 'Set that value in storebound.config.ts (the Checks tab lists what is missing).',
'hint.renders': 'Render the screenshots first: Store art > Render.',
'hint.ffmpeg': 'Download ffmpeg on the Dependencies screen (one click, checked by hash).',
'hint.chrome': 'Download Chrome for Testing on the Dependencies screen.',
'hint.snapshot': 'Take a snapshot first: Growth > Take a snapshot.',
'hint.bucket': 'Set play.reportsBucket in the config (Play Console > Download reports > Statistics > Copy Cloud Storage URI).',
'hint.analyticsSetup': 'Start the App Store reports once: Growth > Start App Store reports (Admin key); data comes 1-2 days later.',
'hint.version': 'Create the version first: Listings > Push App Store version and text.',
'hint.fastlane': 'Pick the folder that contains fastlane/ (or ios/fastlane, android/fastlane).',
'hint.priceSheet': 'Write the sheet first: Prices > Write store/prices.csv, or pick a CSV.',
'hint.pricePoint': 'App Store prices must be one of Apple\'s price points: export with equalized prices to see them.',
'hint.network': 'Check the internet connection and try again.',
'hint.confirm': 'This step needs your confirmation in the dialog; run it again and press the button.',
'hint.git': 'The app folder must be a git repository with history (analytics compare reads git log).',
'prices.readTitle': 'Prices',
'prices.sheetTitle': 'Price sheet',
'prices.sheetSub': 'Every product x store x region in one CSV: export it, edit it, compare it, apply it',
'prices.exportCsv': 'Export prices CSV',
'prices.getSheet': 'Get the sheet',
'prices.readFirst': 'Read prices first',
'prices.writeProject': 'Write store/prices.csv',
'prices.writeHint': 'Writes the current prices into the app folder for `storebound prices import` (dry run first)',
'prices.compareHead': 'Compare and apply',
'prices.pickSheet': 'Compare a sheet…',
'prices.pickFailed': 'Could not open the sheet',
'prices.compareProject': 'Compare store/prices.csv',
'prices.showSheet': 'Show the sheet in the folder',
'prices.noSheetYet': 'No price sheet yet',
'prices.sheetNote': 'Reading changes nothing. Applying a sheet is your decision: storebound shows the dry run in a system dialog and writes only after you press its button. An App Store price increase cannot be undone once it starts.',
'prices.compareTitle': 'Compare the price sheet',
'prices.diffTitle': 'Sheet {file} against the stores',
'prices.diffSub': 'What would change on each store',
'prices.compareAgain': 'Compare again',
'prices.apply': 'Apply {count} changes',
'prices.apply_one': 'Apply 1 change',
'prices.applied': 'Prices written',
'prices.applyNote': 'Owner action: the stores apply new prices within hours, users see them then. Play regions without a price get Play\'s conversion of the US price.',
'prices.upToDate': 'Prices match the sheet',
'prices.upToDateText': 'Nothing to apply. Edit the sheet, then compare again.',
'prices.dChanged': '{count} changed',
'prices.dNew': '{count} new',
'prices.dUp': '{count} up',
'prices.dDown': '{count} down',
'prices.dProducts': 'Products: {list}',
'prices.colStarts': 'Starts',
'prices.colNow': 'Now',
'prices.colNew': 'New',
'prices.colKind': 'Set by',
'prices.now': 'now',
'prices.new': 'New',
'prices.more': '{count} more rows in the sheet',
'prices.emptyTitle': 'No in-app products in the config',
'prices.emptyText': 'Prices are read for the products of storebound.config.ts. Add them there (store-iap-setup skill), then create them on the stores from the Catalog tab.',
'prices.emptyHint': 'in storebound.config.ts',
'prices.notReadTitle': 'Prices not read yet',
'prices.notReadText': 'One read-only pass over App Store Connect and Google Play lists every product\'s price in every territory. It uses the accounts\' API quota.',
'prices.readNow': 'Read prices',
'prices.kProducts': 'Products',
'prices.kProductsSub': '{count} prices read',
'prices.kRegions': 'Territories',
'prices.kRegionsAuto': 'App Store equalized prices included',
'prices.kRegionsManual': 'App Store manual prices only',
'prices.kGaps': 'Price gaps',
'prices.kGapsSub': '≥ {pct} % apart in the same currency',
'prices.kMissing': 'Not on a store',
'prices.kMissingNone': 'Every product is on its stores',
'prices.overview': 'Products x key territories',
'prices.withAuto': 'App Store equalized prices',
'prices.auto': 'Equalized',
'prices.manual': 'Manual',
'prices.autoLegend': 'grey: App Store equalized from the base territory',
'prices.gapLegend': 'Δ: gap between the stores ≥ {pct} %',
'prices.perRegion': '{product} in every territory',
'prices.perRegionSub': '{count} territories',
'prices.searchRegion': 'Territory or currency',
'prices.otherCurrency': 'other currency',
'prices.scheduled': 'from {date}: {price}',
'prices.missingNote': 'Not on the store yet: {list}. Create them from the Catalog tab, then read again.',
'cat.openPrices': 'Open prices',
'cat.pricesHere': 'Prices per territory, the price sheet and applying it are on the Prices tab.',
'art.title': 'Store art',
'art.images': 'Images',
'art.imagesSub': '{count} languages',
'art.imagesSub_one': '1 language',
'art.videos': 'App previews',
'art.videosSub': 'App Store and promo videos',
'art.errors': 'Store errors',
'art.errorsSub': 'Size, alpha, format, count',
'art.warnings': 'Warnings',
'art.rendered': 'Scanned {when}',
'art.variant': 'Variant',
'art.base': 'Base',
'art.onlyIssues': 'Only issues',
'art.openFolder': 'Open folder',
'art.openPage': 'Open review page',
'art.openPageHint': 'Opens the gallery page (index.html) in your browser',
'art.writePage': 'Write review page',
'art.render': 'Render {lang}',
'art.notThere': 'Not there yet',
'art.count': '{count} files',
'art.count_one': '1 file',
'art.open': 'Open {name}',
'art.tooBig': 'too big to preview',
'art.prev': 'Previous',
'art.next': 'Next',
'art.emptyTitle': 'No renders yet',
'art.emptyText': 'Render the store screenshots from the frames and the copy file: press Render. It runs locally (ffmpeg and Chrome) and uploads nothing.',
'art.noIssues': 'No issues here',
'art.noIssuesText': 'Every file of this language passes the local store checks.',
'art.foot': 'Local files only, read {when}. Uploading is on the Listings tab.',
'gr.analytics': 'Store analytics',
'gr.analyticsSub': '{count} snapshots since {since}',
'gr.analyticsSubNone': 'Snapshots in .storebound/analytics, next to the listing changes in git',
'gr.setup': 'Start App Store reports',
'gr.setupDone': 'Report request created',
'gr.setupHint': 'Creates the ongoing App Store analytics report request once (Admin key). Changes nothing public.',
'gr.snapshot': 'Take a snapshot',
'gr.snapshotDone': 'Snapshot saved',
'gr.snapshotHint': 'Reads impressions, page views, downloads (App Store) and visitors, installs (Play) into .storebound/analytics',
'gr.noneTitle': 'No analytics snapshots yet',
'gr.noneText': 'A snapshot keeps 28 days of store traffic in the app folder (.storebound/analytics). With a few of them, this tab shows each listing change from git next to the conversion before and after it.',
'gr.noBucket': 'For Google Play, set play.reportsBucket in the config first.',
'gr.impressions': 'Impressions',
'gr.downloads': 'Downloads',
'gr.visitors': 'Listing visitors',
'gr.acquisitions': 'Acquisitions',
'gr.conversion': 'Conversion',
'gr.conversionSub': 'Installs per reach, whole window',
'gr.days': '{count} days',
'gr.days_one': '1 day',
'gr.daysN': '{count} days',
'gr.daysN_one': '1 day',
'gr.chartAria': 'Daily store traffic on {store} with listing changes',
'gr.changes': '{count} listing changes',
'gr.changes_one': '1 listing change',
'gr.oneDay': 'One day of data so far: the chart starts with the second day.',
'gr.change': 'Listing change (git)',
'gr.before': 'Before',
'gr.after': 'After',
'gr.reading': 'A change needs several days on both sides and enough traffic; seasonality, featuring and ads move these numbers too.',
'gr.ppo': 'Product Page Optimization',
'gr.ppoSub': 'App Store A/B tests of the screenshots',
'gr.ppoRead': 'Read tests',
'gr.ppoNotRead': 'Tests not read yet',
'gr.ppoNotReadText': 'Read-only: the tests, their state, traffic share and treatments. Results per treatment are only in App Store Connect.',
'gr.ppoNone': 'No tests yet',
'gr.ppoNoneText': 'Create a test from a screenshot variant below, upload its screenshots, then start it.',
'gr.ppoNoVariants': 'Add a screenshot variant to shots.variants in the config (store-screenshots skill) and render it; each variant becomes one treatment.',
'gr.results': 'Results in App Store Connect',
'gr.test': 'Test',
'gr.state': 'State',
'gr.traffic': 'Traffic',
'gr.dates': 'Running',
'gr.treatments': 'Treatments',
'gr.owner': 'Owner action',
'gr.running': 'running',
'gr.notStarted': 'not started',
'gr.treatment': '{name} ({count} locales)',
'gr.treatment_one': '{name} (1 locale)',
'gr.startBtn': 'Start',
'gr.stopBtn': 'Stop',
'gr.start': 'Start the test "{name}"',
'gr.stop': 'Stop the test "{name}"',
'gr.started': 'Test start requested',
'gr.stopped': 'Test stop requested',
'gr.create': 'New test',
'gr.variant': 'Variant {id}',
'gr.createBtn': 'Create test',
'gr.createTitle': 'Create a Product Page Optimization test',
'gr.created': 'Test created (not started)',
'gr.upload': 'Treatment screenshots',
'gr.uploadBtn': 'Upload screenshots',
'gr.uploadTitle': 'Upload the screenshots of variant {id}',
'gr.uploadInto': 'Into the editable test "{name}"',
'gr.uploadNoTest': 'Create a test first: uploads go into the editable test.',
'gr.ppoNote': 'Create and upload change nothing visitors see. Start and stop are your decision: each shows a system dialog that names the action; a stopped test cannot be restarted.',
'gr.ppoStoreOnly': 'Read-only for a store-only app: link an app folder to create and run tests.',
'gr.playExp': 'Store listing experiments',
'gr.playExpSub': 'Google Play: no API',
'gr.playExpText': 'Google Play has no API for store listing experiments: create and run them in Play Console > Grow > Store listing experiments, with the variant screenshots storebound renders.',
'gr.seeVariants': 'See the variant renders',
'ai.askTitle': '{count} questions only you can answer',
'ai.askTitle_one': '1 question only you can answer',
'ai.askSub': 'The config does not prove these answers. storebound never guesses a compliance answer: decide, then set it in the config.',
'ai.allAnswered': 'Every declaration this tab shows comes from the config. The answers are still yours: review them before each submission.',
'ai.aDataSafety': 'Data safety',
'ai.aAppPrivacy': 'App Privacy',
'ai.aAppInfo': 'App info',
'ai.aEncryption': 'Export compliance',
'ai.aReview': 'App Review',
'ai.askedHere': '{count} open questions about this form above',
'ai.askedHere_one': '1 open question about this form above',
'ai.dsTitle': 'Data safety',
'ai.dsSub': 'Google Play, from dataSafety in the config',
'ai.dsUpload': 'Upload Data safety',
'ai.dsUploadHint': 'Replaces the whole Play Data safety form (dry run and system dialog first)',
'ai.dsUploaded': 'Data safety uploaded',
'ai.dsNoneTitle': 'No Data safety answers',
'ai.dsNoneText': 'Add dataSafety to storebound.config.ts (store-declarations skill): what the app and every SDK collect, why, and whether it is shared.',
'ai.dsCollects': 'Collects or shares data',
'ai.dsEncrypted': 'Encrypted in transit',
'ai.dsDeletion': 'Users can ask for deletion',
'ai.dsAccounts': 'Account creation',
'ai.dsNever': 'Play cannot read this form back. Once you upload from here, this tab shows what changed since.',
'ai.dsSame': 'Same as the last upload ({when}).',
'ai.dsChanged': '{count} answers changed since the last upload ({when}): upload again to bring Play up to date.',
'ai.dsChanged_one': '1 answer changed since the last upload ({when}): upload again to bring Play up to date.',
'ai.show': 'Show',
'ai.hide': 'Hide',
'ai.colType': 'Data type',
'ai.colCollected': 'Collected for',
'ai.colShared': 'Shared for',
'ai.notShared': 'not shared',
'ai.optional': 'optional',
'ai.ephemeral': 'ephemeral',
'ai.dsFoot': 'The upload sends {rows} CSV rows (Play\'s import format) and replaces every earlier answer.',
'ai.apTitle': 'App Privacy',
'ai.apSub': 'App Store, suggested from the Data safety answers',
'ai.apOpen': 'Open App Privacy',
'ai.apNote': 'App Privacy has no API: enter these answers in App Store Connect > App Privacy. Apple also asks per type whether it is linked to the user and used for tracking.',
'ai.apNoneTitle': 'No data collected',
'ai.apNoneText': 'The Data safety answers list no data types: in App Store Connect answer "Data Not Collected", if that is true for every SDK.',
'ai.apUnknownTitle': 'Nothing to suggest yet',
'ai.apUnknownText': 'Answer the data collection once in dataSafety; the App Privacy answers follow from it.',
'ai.colAppleType': 'Data type',
'ai.colPurposes': 'Purposes',
'ai.colLinked': 'Linked to user',
'ai.colTracking': 'Tracking',
'ai.ask': 'Your answer',
'ai.askLikely': 'Check: shared',
'ai.noPlay': 'Not on Google Play',
'ai.noPlayText': 'The config has no play section.',
'ai.noApple': 'Not on the App Store',
'ai.noAppleText': 'The config has no apple section.',
'ai.infoTitle': 'App info',
'ai.infoSub': 'Categories, age rating, content rights',
'ai.infoPush': 'Push the App Store app info',
'ai.push': 'Push',
'ai.pushed': 'Pushed to App Store Connect',
'ai.category': 'Category',
'ai.age': 'Age rating',
'ai.ageAllNone': 'Every question "none": confirm',
'ai.rights': 'Content rights',
'ai.rightsOwn': 'No third-party content',
'ai.rightsThird': 'Third-party content, rights held',
'ai.price': 'Price',
'ai.free': 'Free, every territory',
'ai.paid': 'Paid (set in App Store Connect)',
'ai.notSet': 'Not set',
'ai.notAnswered': 'Not answered',
'ai.encTitle': 'Export compliance',
'ai.encSub': 'Encryption answer for every build',
'ai.encAnswer': 'Answer builds',
'ai.encPush': 'Answer export compliance for the builds',
'ai.encYes': 'Uses non-exempt encryption',
'ai.encNo': 'Only exempt encryption',
'ai.encYesText': 'Builds are linked to the approved encryption declaration in App Store Connect.',
'ai.encNoText': 'Builds without an answer get usesNonExemptEncryption = false. Better: ITSAppUsesNonExemptEncryption = NO in Info.plist.',
'ai.encUnset': 'apple.usesNonExemptEncryption is not set. Decide it with the export-compliance skill; storebound never guesses it.',
'ai.encUnsetHint': 'Answer apple.usesNonExemptEncryption in the config first',
'ai.reviewTitle': 'App Review details',
'ai.reviewSub': 'Contact, demo account, notes',
'ai.reviewPush': 'Push the App Review details',
'ai.signIn': 'Sign-in needed',
'ai.contact': 'Contact',
'ai.demo': 'Demo account',
'ai.demoNone': 'none needed',
'ai.missing': 'Missing',
'ai.notes': 'Notes for the reviewer',
'ai.notesBytes': '{n} / {max} bytes',
'ai.notesNone': 'No notes (apple.info.reviewNotes, reviewAccess.instructions).',
'ai.reviewSafe': 'Contact and demo account stay in the app folder\'s .storebound/ (gitignored); shown masked, never the password.',
'ai.foot': 'From the config and .storebound/, read {when}. Nothing here was sent anywhere.',
'ds.functionality': 'App functionality',
'ds.analytics': 'Analytics',
'ds.communications': 'Developer communications',
'ds.fraud': 'Fraud prevention, security',
'ds.advertising': 'Advertising or marketing',
'ds.personalization': 'Personalization',
'ds.account': 'Account management',
'folders.fastlane': 'Import from fastlane',
'folders.fastlaneHint': 'Pick a folder with a fastlane setup: storebound shows the import plan in a system dialog, then writes its files next to fastlane (never overwrites)',
'folders.imported': '{name} imported from fastlane',
'folders.importedDetail': 'Config, listing files and screenshot frames written. Next: replace the placeholders, then Checks.',
'folders.startShort': 'Start an app folder',
'folders.newNote': 'Both ask for a folder in a system dialog, show what they would write, and write only after you press Apply. Existing files are kept.',
'folders.open': 'Open',
'folders.openApp': 'Open this app',
'folders.reveal': 'Show in the file manager',
'wz.s0': 'Your studio',
'wz.studioTitle': 'Which studio are these apps from?',
'wz.studioLead': 'A studio groups store accounts and their apps (a company, a client, a side project). Pick one or create it; the keys of the next steps go into it. You can move things later.',
'wz.studioPick': 'Studio',
'wz.studioAccounts': '{count} store accounts',
'wz.studioAccounts_one': '1 store account',
'wz.studioPicked': 'Selected',
'wz.studioNoneHint': 'Keep accounts without a studio',
'wz.studioNew': 'Create your studio',
'wz.studioAnother': 'Or create another studio',
'wz.studioNewText': 'A name and a colour; a logo can come later in Studios & accounts.',
'wz.studioPlaceholder': 'For example: Example Games',
'wz.studioCreate': 'Create studio',
'wz.fastlaneTitle': 'Import from fastlane',
'wz.fastlaneText': 'The folder has a fastlane setup (deliver, supply). storebound turns it into its config and listing files and leaves fastlane untouched.',
'wz.fastlaneGo': 'Choose the folder and import',
'wz.fastlaneNote': 'You see the import plan and what it does not map before anything is written.',
'wz.keysInto': 'New keys go into {name}.',
'wz.keysNoStudio': 'New keys stay without a studio.',
'wz.change': 'Change',
} as const;