From 23f4c43da85cb3e8ccab1a7b71e5f022361d2d42 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Wed, 19 Aug 2026 11:57:28 +0200 Subject: [PATCH] Put the store where people are, not at the bottom of the catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client was reachable from one grey link under the whole catalog listing and from nowhere on the home page — which is a strange place for the thing that actually gets our games onto somebody's machine. **Catalog**: the Stores link moves into the hero, as a button in the shape the Code and HowTo heroes already use (subtitle, breathing room, one white button). Somebody who has come to look at games is one step from wanting to run one, and that step should not be below the fold. The build matrix stays at the bottom, which is where somebody who has already read the catalog goes looking for it. **Home**: a section for the client under the featured game — the same dark band as the YouTube and Spotify blocks below it, so the page keeps one rhythm rather than growing a fifth nav card and breaking the four-column grid. Screenshot, three things it does, download and a link to the full page. The screenshot leads on a wide screen and follows on a narrow one: it is what makes the section legible at a glance, but it is not what somebody reads first on a phone. `btn-hero-purple` joins teal and indigo, and retro mode covers it — that block lists the button classes by name, so a new one is invisible there until added. Two things found on the way: - the stores page said the client "needs Python 3". It has not since the engine moved into the app; the line now says nothing has to be installed first, which is the actual selling point. - there was no test holding the two locale bundles together. A missing translation is not an error at runtime — vue-i18n renders the key itself, so `home.store.title` appears on screen looking like unfinished work, and only after switching language. The new spec names the missing key; I checked it fails by deleting one. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/i18n/__tests__/locales.test.ts | 41 ++++++ apps/frontend/src/i18n/locales/en.ts | 15 ++- apps/frontend/src/i18n/locales/hu.ts | 15 ++- .../src/page/catalog/CatalogIndexPage.vue | 17 +-- apps/frontend/src/page/home/HomePage.vue | 124 ++++++++++++++++++ apps/frontend/src/styles/global.css | 4 + apps/frontend/src/styles/retro.css | 1 + 7 files changed, 207 insertions(+), 10 deletions(-) create mode 100644 apps/frontend/src/i18n/__tests__/locales.test.ts diff --git a/apps/frontend/src/i18n/__tests__/locales.test.ts b/apps/frontend/src/i18n/__tests__/locales.test.ts new file mode 100644 index 0000000..7362be8 --- /dev/null +++ b/apps/frontend/src/i18n/__tests__/locales.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest' +import en from '../locales/en' +import hu from '../locales/hu' + +/** + * The two bundles have to carry the same keys. + * + * A missing translation is not an error at runtime — vue-i18n falls back and the page + * renders the key itself, so `home.store.title` appears on screen looking like a bug + * somebody forgot to finish. That is exactly the kind of thing a person only notices + * after switching language, which is to say: after it has shipped. + */ +function paths (value: unknown, prefix = ''): string[] { + if (typeof value !== 'object' || value === null) return [prefix] + return Object.entries(value as Record) + .flatMap(([key, nested]) => paths(nested, prefix === '' ? key : `${prefix}.${key}`)) +} + +describe('locale bundles', () => { + const english = paths(en).sort() + const hungarian = paths(hu).sort() + + it('has something to compare', () => { + expect(english.length).toBeGreaterThan(50) + }) + + it('translates every English key into Hungarian', () => { + expect(hungarian.filter((key) => !english.includes(key))).toEqual([]) + expect(english.filter((key) => !hungarian.includes(key))).toEqual([]) + }) + + it('leaves no empty string behind', () => { + const empty = [...paths(en), ...paths(hu)].filter((key) => { + const read = (bundle: unknown) => + key.split('.').reduce((acc, part) => + (acc as Record | undefined)?.[part], bundle) + return read(en) === '' || read(hu) === '' + }) + expect(empty).toEqual([]) + }) +}) diff --git a/apps/frontend/src/i18n/locales/en.ts b/apps/frontend/src/i18n/locales/en.ts index 618d22f..d602eb2 100644 --- a/apps/frontend/src/i18n/locales/en.ts +++ b/apps/frontend/src/i18n/locales/en.ts @@ -60,6 +60,18 @@ export default { spotify: { openInSpotify: 'Open in Spotify ↗', }, + store: { + label: 'Play on your own machine', + allStores: 'All stores', + title: 'WarpEngine Client', + desc: 'Our catalog as a grid of cards on your desktop: one click installs a game into your application menu, one plays it. It sets itself up on first run.', + point1: 'Nothing to install first — no interpreter, no admin rights', + point2: 'Installed games become normal menu entries, so they start without the app running', + point3: 'Browser builds too, for titles with no download for your machine', + download: 'Download', + more: 'How it works', + platforms: 'Linux, macOS and Windows', + }, }, contact: { title: 'Contact Us', @@ -109,6 +121,7 @@ export default { readMore: 'Read more', }, catalog: { + getTheStore: 'Get the store', title: 'Our Games', subtitle: 'Discover games made for TIC-80 and other platforms, with versions playable directly in your browser!', filter: { @@ -206,7 +219,7 @@ export default { clientDesc: 'The app: the catalog as a grid of cards, one click to install a game into your application menu, one to play it. It sets the store up itself on first run, and on Windows it is the way in.', clientDownload: 'Download', - clientPlatforms: 'Linux, macOS and Windows · needs Python 3', + clientPlatforms: 'Linux, macOS and Windows · nothing to install first', clientShotAlt: 'The WarpEngine Client window: the store picker and category filters on the left, and the catalog as a grid of game cards with install and play buttons.', docs: 'Documentation', diff --git a/apps/frontend/src/i18n/locales/hu.ts b/apps/frontend/src/i18n/locales/hu.ts index 97d5ecc..8e547cf 100644 --- a/apps/frontend/src/i18n/locales/hu.ts +++ b/apps/frontend/src/i18n/locales/hu.ts @@ -60,6 +60,18 @@ export default { spotify: { openInSpotify: 'Megnyitás Spotify-ban ↗', }, + store: { + label: 'Játssz a saját gépeden', + allStores: 'Összes bolt', + title: 'WarpEngine Client', + desc: 'A katalógusunk kártyákban, az asztalodon: egy kattintás a telepítés az alkalmazásmenübe, egy az indítás. Első futáskor magát is beállítja.', + point1: 'Semmit nem kell előtte feltenni — se értelmező, se rendszergazdai jog', + point2: 'A feltelepített játék rendes menüelem lesz, tehát az app nélkül is indul', + point3: 'Böngészős buildek is, ha egy címhez nincs letöltés a gépedre', + download: 'Letöltés', + more: 'Hogyan működik', + platforms: 'Linux, macOS és Windows', + }, }, contact: { title: 'Kapcsolat', @@ -109,6 +121,7 @@ export default { readMore: 'Tovább olvasom', }, catalog: { + getTheStore: 'Töltsd le a boltot', title: 'Játékaink', subtitle: 'Fedezd fel a TIC-80-ra és más platformokra készített játékainkat, amelyeknek közvetlenül a böngésződben is kipróbálhatsz verzióit!', filter: { @@ -206,7 +219,7 @@ export default { clientDesc: 'Az alkalmazás: a katalógus kártyákban, egy kattintás a telepítés az alkalmazásmenübe, egy az indítás. Első indításkor magát a store-t is beállítja, Windowson pedig ez az út.', clientDownload: 'Letöltés', - clientPlatforms: 'Linux, macOS és Windows · Python 3 kell hozzá', + clientPlatforms: 'Linux, macOS és Windows · semmit nem kell előtte feltenni', clientShotAlt: 'A WarpEngine Client ablaka: balra a store-választó és a kategóriaszűrők, jobbra a katalógus játékkártyákban, telepítés és indítás gombokkal.', docs: 'Dokumentáció', diff --git a/apps/frontend/src/page/catalog/CatalogIndexPage.vue b/apps/frontend/src/page/catalog/CatalogIndexPage.vue index aaf31e8..a524f86 100644 --- a/apps/frontend/src/page/catalog/CatalogIndexPage.vue +++ b/apps/frontend/src/page/catalog/CatalogIndexPage.vue @@ -2,7 +2,13 @@

{{ t('catalog.title') }}

-

{{ t('catalog.subtitle') }}

+ +

{{ t('catalog.subtitle') }}

+ + {{ t('catalog.getTheStore') }} +
@@ -62,14 +68,12 @@ + @@ -171,7 +175,4 @@ onMounted(() => store.fetch()) .builds-link { @apply text-sm text-gray-400 hover:text-purple-600 font-medium transition-colors; } -.builds-link-sep { - @apply text-gray-200 text-sm; -} diff --git a/apps/frontend/src/page/home/HomePage.vue b/apps/frontend/src/page/home/HomePage.vue index 29f76d4..c5b7774 100644 --- a/apps/frontend/src/page/home/HomePage.vue +++ b/apps/frontend/src/page/home/HomePage.vue @@ -66,6 +66,58 @@ + +
+
+
+ +
+
+
+
+
+ {{ t('home.store.label') }} +
+ {{ t('home.store.allStores') }} +
+ +
+
+ +
+ +
+

{{ t('home.store.title') }}

+

{{ t('home.store.desc') }}

+
    +
  • {{ t('home.store.point1') }}
  • +
  • {{ t('home.store.point2') }}
  • +
  • {{ t('home.store.point3') }}
  • +
+ +
+ + {{ t('home.store.download') }} + + + {{ t('home.store.more') }} + +
+ +

+ + {{ t('home.store.platforms') }} +

+
+
+
+
+