Skip to content

Commit 0764485

Browse files
committed
Add bilingual interface and English documentation
1 parent 4a3ae35 commit 0764485

14 files changed

Lines changed: 449 additions & 177 deletions

File tree

‎README.md‎

Lines changed: 72 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,118 @@
11
# CPCJS
22

3-
CPCJS est le port navigateur de [Caprice32](https://github.com/ColinPitrat/caprice32).
4-
Le cœur C++ original est compilé en WebAssembly et JavaScript avec Emscripten;
5-
SDL2 relie l’écran, le son, le clavier et les manettes aux API du navigateur.
3+
**[Play CPCJS online](https://smichea.github.io/cpcjs/)**
64

7-
Formats acceptés: DSK, SNA, CDT, VOC, CPR, IPF, RAW et ZIP.
5+
CPCJS is a browser port of [Caprice32](https://github.com/ColinPitrat/caprice32).
6+
The original C++ core is compiled to WebAssembly and JavaScript with Emscripten;
7+
SDL2 connects video, audio, keyboard, and controller support to browser APIs.
88

9-
## Prérequis
9+
Supported formats: DSK, SNA, CDT, VOC, CPR, IPF, RAW, and ZIP.
1010

11-
- Emscripten (`em++` dans le `PATH`)
12-
- Node.js 18 ou supérieur
13-
- les sources Caprice32 dans `../caprice32`, ou leur chemin dans
11+
## Requirements
12+
13+
- Emscripten (`em++` available in `PATH`)
14+
- Node.js 18 or later
15+
- the Caprice32 sources in `../caprice32`, or their location provided through
1416
`CAPRICE32_DIR`
1517

16-
Sur Ubuntu/Debian, Emscripten peut être installé avec:
18+
On Ubuntu or Debian, Emscripten can be installed with:
1719

1820
```sh
1921
sudo apt install emscripten
2022
```
2123

22-
## Construire et jouer
24+
## Build and run
2325

2426
```sh
2527
npm run build
2628
npm run serve
2729
```
2830

29-
Ouvrir ensuite <http://localhost:8080>, choisir une image de jeu, puis cliquer
30-
dans l’écran pour donner le focus au clavier. Le son démarre après cette action
31-
utilisateur, conformément aux règles des navigateurs.
31+
Open <http://localhost:8080>, select a game image, and click the screen to give
32+
the emulator keyboard focus. Audio starts after this user interaction, as
33+
required by browser autoplay policies.
3234

33-
Le build produit `public/emulator/caprice32.js`, `caprice32.wasm` et le paquet de
34-
données préchargé. Ces artefacts ne sont pas versionnés. Le navigateur doit être
35-
servi via HTTP; ouvrir directement `index.html` avec `file://` ne fonctionne pas.
35+
The build creates `public/emulator/caprice32.js`, `caprice32.wasm`, and the
36+
preloaded data package. These artifacts are not committed. The application must
37+
be served over HTTP; opening `index.html` directly through `file://` does not
38+
work.
3639

37-
## Contrôles
40+
## Controls
3841

39-
- clavier CPC: clavier physique
40-
- joystick: flèches, `Z` et `X`
41-
- menu Caprice32: `F1`
42-
- plein écran: bouton de l’interface ou `Ctrl` + `Entrée`
42+
- CPC keyboard: physical keyboard
43+
- joystick: arrow keys, `Z`, and `X`
44+
- Caprice32 menu: `F1`
45+
- fullscreen: interface button or `Ctrl` + `Enter`
4346

44-
Pour changer de disque après le démarrage, utilisez le menu Caprice32 (`F1`).
45-
Pour démarrer directement avec une autre image, rechargez la page.
47+
To change disks after startup, use the Caprice32 menu (`F1`). To start directly
48+
with another image, reload the page.
4649

47-
## Jeux locaux et catalogue hébergé
50+
## Local games and hosted catalog
4851

49-
Le bouton **Choisir un jeu** propose deux sources:
52+
The **Choose a game** button offers two sources:
5053

51-
- une image CPC présente sur l'appareil de l'utilisateur;
52-
- un jeu du catalogue hébergé dans `public/games/`.
54+
- a CPC image stored on the user's device;
55+
- a game from the catalog hosted in `public/games/`.
5356

54-
Les jeux hébergés suivent la convention `nom-du-jeu.<format>` en minuscules,
55-
avec un identifiant stable, une empreinte SHA-256 et un lien vers l'autorisation
56-
de redistribution dans `public/games/catalog.json`. Le navigateur contrôle
57-
l'empreinte du fichier avant de le lancer.
57+
Hosted games use lowercase `game-name.<format>` filenames, a stable identifier,
58+
a SHA-256 digest, and a link to the redistribution permission in
59+
`public/games/catalog.json`. The browser verifies the file digest before
60+
launching it.
5861

59-
Le catalogue contient actuellement cinq jeux Design Design Software distribués
60-
avec l'autorisation de Simon Brattel et les sept jeux CPC du Vortex Emulation
61-
Package. Ce dernier reste hébergé sous la forme de son archive originale; le
62-
navigateur en extrait uniquement le jeu sélectionné en mémoire. Un autre jeu ne
63-
doit être ajouté que si son titulaire autorise explicitement la
64-
**redistribution**. Une mention « abandonware », un téléchargement gratuit ou
65-
une page permettant de jouer en ligne ne suffisent pas. La provenance, les
66-
conditions et le schéma sont documentés dans `public/games/README.md`.
62+
The catalog currently contains five Design Design Software games distributed
63+
with permission from Simon Brattel and the seven CPC games from the Vortex
64+
Emulation Package. The Vortex package remains hosted as its unmodified original
65+
archive; the browser extracts only the selected game in memory. Another game
66+
must not be added unless its rights holder explicitly permits
67+
**redistribution**. An “abandonware” label, a free download, or a page that
68+
allows online play is not sufficient. Provenance, terms, and the catalog schema
69+
are documented in `public/games/README.md`.
6770

6871
## Tests
6972

70-
Installer Chromium pour Playwright une première fois, puis lancer la suite:
73+
Install Chromium for Playwright once, then run the suite:
7174

7275
```sh
7376
npm install
7477
npx playwright install chromium
7578
npm test
7679
```
7780

78-
`npm test` reconstruit le module WebAssembly, puis valide dans un vrai Chromium
79-
headless les deux parcours suivants:
81+
`npm test` rebuilds the WebAssembly module and validates these flows in a real
82+
headless Chromium instance:
8083

81-
- démarrage du CPC 6128 sans image, affichage vidéo et progression des frames;
82-
- chargement de l’image disque de test, saisie de `RUN"HELLO` et exécution du
83-
programme BASIC jusqu’à la modification effective de l’écran et la sortie
84-
exacte `Hello, World !` sur le port imprimante émulé.
84+
- booting the CPC 6128 without an image, with active video and frame progress;
85+
- loading the test disk image, entering `RUN"HELLO`, and running the BASIC
86+
program until the screen changes and the emulated printer port outputs the
87+
exact text `Hello, World !`;
88+
- downloading and starting a standalone game from the hosted catalog;
89+
- extracting and starting a Vortex game in memory from the original archive;
90+
- selecting the interface language from the browser locale and persisting a
91+
manual language change.
8592

86-
La fixture est issue des tests intégrés GPL-2.0 de Caprice32. Ces scénarios
87-
valident le pipeline navigateur de bout en bout; ils ne signifient pas encore
88-
que l’ensemble du catalogue de jeux CPC est compatible.
93+
The fixture comes from Caprice32's GPL-2.0 integration tests. These scenarios
94+
validate the browser pipeline end to end; they do not imply compatibility with
95+
the entire CPC software catalog.
8996

90-
## Déployer manuellement sur GitHub Pages
97+
## Manual deployment to GitHub Pages
9198

92-
Une seule fois, ouvrir **Settings → Pages → Build and deployment** dans le
93-
dépôt GitHub et choisir **GitHub Actions** comme source.
99+
For the initial setup, open **Settings → Pages → Build and deployment** in the
100+
GitHub repository and select **GitHub Actions** as the source.
94101

95-
Pour chaque déploiement:
102+
For each deployment:
96103

97-
1. ouvrir l’onglet **Actions** du dépôt;
98-
2. choisir le workflow **Deploy to GitHub Pages**;
99-
3. cliquer sur **Run workflow**, sélectionner la branche à publier, puis
100-
confirmer avec **Run workflow**.
104+
1. open the repository's **Actions** tab;
105+
2. select the **Deploy to GitHub Pages** workflow;
106+
3. click **Run workflow**, select the branch to publish, and confirm with
107+
**Run workflow**.
101108

102-
Le workflow reconstruit Caprice32 en WebAssembly, publie le contenu de
103-
`public/` et affiche l’URL du site dans le job `deploy`. Il est uniquement
104-
déclenché à la demande et ne déploie rien automatiquement lors d’un push.
109+
The workflow rebuilds Caprice32 as WebAssembly, publishes the contents of
110+
`public/`, and reports the site URL in the `deploy` job. It only runs when
111+
manually triggered and does not deploy automatically after a push.
105112

106-
## Licence
113+
## License
107114

108-
Caprice32 et ce port sont distribués sous GPL-2.0. Les ROM incluses dans le
109-
dépôt Caprice32 sont empaquetées au moment du build. Les jeux tiers de
110-
`public/games/` conservent leur copyright et ne sont pas couverts par la GPL;
111-
leurs conditions sont documentées dans ce répertoire.
115+
Caprice32 and this port are distributed under GPL-2.0. The ROMs included in the
116+
Caprice32 repository are packaged during the build. Third-party games in
117+
`public/games/` retain their copyright and are not covered by the GPL; their
118+
terms are documented in that directory.

‎playwright.config.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default defineConfig({
99
use: {
1010
...devices['Desktop Chrome'],
1111
baseURL: 'http://127.0.0.1:4173',
12+
locale: 'en-US',
1213
trace: 'retain-on-failure',
1314
screenshot: 'only-on-failure'
1415
},

‎public/app.css‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ h1 { margin: 0; font-size: clamp(38px, 7vw, 68px); line-height: .9; letter-spaci
1616
h1 span { color: #ffcc45; }
1717
.repo-link { padding: 8px 12px; border: 1px solid #314361; border-radius: 999px; color: #c7d6ed; font-size: 13px; text-decoration: none; transition: .15s ease; }
1818
.repo-link:hover { border-color: #7fd5ff; color: #fff; transform: translateY(-1px); }
19+
#language-select { width: auto; padding: 7px 10px; border-radius: 999px; font-size: 13px; }
1920
.status { margin: 0; padding: 8px 12px; border: 1px solid #314361; border-radius: 999px; color: #aebbd0; font-size: 13px; }
2021
.status[data-state="ready"] { color: #88efbd; border-color: #246044; }
2122
.status[data-state="running"] { color: #7fd5ff; border-color: #245b75; }

‎public/app.js‎

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { safeName } from './filename.js';
22
import { sha256Hex, validateCatalog } from './catalog.js';
33
import { extractZipMember } from './archive.js';
4+
import { initI18n, t } from './i18n.js';
45

56
const canvas = document.querySelector('#canvas');
67
const status = document.querySelector('#status');
@@ -15,9 +16,14 @@ const startEmpty = document.querySelector('#start-empty');
1516
const fullscreen = document.querySelector('#fullscreen');
1617
const screenMessage = document.querySelector('#screen-message');
1718
const logs = document.querySelector('#logs');
19+
const languageSelect = document.querySelector('#language-select');
20+
21+
initI18n(languageSelect);
1822

1923
let started = false;
2024
let hostedGames = [];
25+
let statusDescriptor = { key: 'status.loading', state: '', values: {} };
26+
let catalogDescriptor = { key: 'catalog.loading', values: {} };
2127
let runtimeReady;
2228
const ready = new Promise((resolve) => { runtimeReady = resolve; });
2329

@@ -35,28 +41,47 @@ function log(message) {
3541
logs.scrollTop = logs.scrollHeight;
3642
}
3743

38-
function setStatus(message, state = '') {
39-
status.textContent = message;
44+
function setStatus(key, state = '', values = {}) {
45+
statusDescriptor = { key, state, values };
46+
status.textContent = t(key, values);
4047
status.dataset.state = state;
4148
}
4249

50+
function setRawStatus(message) {
51+
statusDescriptor = null;
52+
status.textContent = message;
53+
}
54+
55+
function setCatalogMessage(key, values = {}) {
56+
catalogDescriptor = { key, values };
57+
catalogMessage.textContent = t(key, values);
58+
}
59+
60+
window.addEventListener('cpcjs:languagechange', () => {
61+
if (statusDescriptor) {
62+
const { key, state, values } = statusDescriptor;
63+
setStatus(key, state, values);
64+
}
65+
if (catalogDescriptor) setCatalogMessage(catalogDescriptor.key, catalogDescriptor.values);
66+
});
67+
4368
window.Module = {
4469
noInitialRun: true,
4570
canvas,
4671
locateFile: (path) => `emulator/${path}`,
4772
print: log,
48-
printErr: (message) => log(`[erreur] ${message}`),
49-
setStatus: (message) => message && setStatus(message),
73+
printErr: (message) => log(`[${t('log.errorPrefix')}] ${message}`),
74+
setStatus: (message) => message && setRawStatus(message),
5075
onRuntimeInitialized() {
5176
window.cpcjsState.runtimeReady = true;
52-
setStatus('Prêt — choisissez un jeu', 'ready');
77+
setStatus('status.ready', 'ready');
5378
runtimeReady();
5479
}
5580
};
5681

5782
async function launch(file, source = 'local') {
5883
if (started) {
59-
setStatus('Rechargez la page pour changer de jeu', 'warning');
84+
setStatus('status.reload', 'warning');
6085
return;
6186
}
6287

@@ -68,7 +93,7 @@ async function launch(file, source = 'local') {
6893
Module.FS.writeFile(path, new Uint8Array(await file.arrayBuffer()));
6994
args.push(path);
7095
window.cpcjsState.gamePath = path;
71-
log(`Image chargée: ${file.name} (${file.size} octets)`);
96+
log(t('log.imageLoaded', { name: file.name, size: file.size }));
7297
}
7398

7499
started = true;
@@ -81,7 +106,7 @@ async function launch(file, source = 'local') {
81106
startEmpty.disabled = true;
82107
fullscreen.disabled = false;
83108
screenMessage.hidden = true;
84-
setStatus(file ? `En jeu — ${file.name}` : 'CPC 6128 démarré', 'running');
109+
setStatus(file ? 'status.playing' : 'status.started', 'running', file ? { name: file.name } : {});
85110
canvas.focus();
86111
Module.callMain(args);
87112
}
@@ -93,7 +118,7 @@ async function loadCatalog() {
93118
hostedGames = validateCatalog(await response.json());
94119

95120
if (!hostedGames.length) {
96-
catalogMessage.textContent = 'Aucun jeu redistribuable n’est encore référencé.';
121+
setCatalogMessage('catalog.empty');
97122
return;
98123
}
99124

@@ -103,11 +128,11 @@ async function loadCatalog() {
103128
option.textContent = `${game.title}${game.year ? ` (${game.year})` : ''}`;
104129
hostedGame.append(option);
105130
}
106-
catalogMessage.textContent = 'Ces fichiers sont publiés avec l’autorisation indiquée dans le catalogue.';
131+
setCatalogMessage('catalog.available');
107132
hostedControls.hidden = false;
108133
} catch (error) {
109134
console.error(error);
110-
catalogMessage.textContent = 'Le catalogue hébergé est indisponible.';
135+
setCatalogMessage('catalog.unavailable');
111136
}
112137
}
113138

@@ -116,17 +141,17 @@ async function downloadHostedGame() {
116141
if (!game) return;
117142

118143
launchHosted.disabled = true;
119-
setStatus(`Téléchargement — ${game.title}`, 'loading');
144+
setStatus('status.downloading', 'loading', { title: game.title });
120145
try {
121146
const response = await fetch(game.file);
122-
if (!response.ok) throw new Error(`Téléchargement impossible (HTTP ${response.status})`);
147+
if (!response.ok) throw new Error(t('error.download', { status: response.status }));
123148
let buffer = await response.arrayBuffer();
124149
if (game.archive && await sha256Hex(buffer) !== game.archive.sha256) {
125-
throw new Error(`Empreinte invalide pour l’archive de ${game.title}`);
150+
throw new Error(t('error.archiveDigest', { title: game.title }));
126151
}
127152
if (game.archive) buffer = await extractZipMember(buffer, game.archive.member);
128153
if (await sha256Hex(buffer) !== game.sha256) {
129-
throw new Error(`Empreinte invalide pour ${game.title}`);
154+
throw new Error(t('error.digest', { title: game.title }));
130155
}
131156
gamePicker.close();
132157
await launch(new File([buffer], `${game.slug}.${game.format}`), 'hosted');
@@ -151,16 +176,19 @@ canvas.addEventListener('click', () => canvas.focus());
151176

152177
function fatal(error) {
153178
console.error(error);
154-
window.cpcjsState.error = error.message || String(error);
155-
log(error.stack || error.message || String(error));
156-
setStatus('Échec du démarrage — consultez le journal', 'error');
179+
const message = error.i18nKey
180+
? t(error.i18nKey, error.i18nValues)
181+
: error.message || String(error);
182+
window.cpcjsState.error = message;
183+
log(error.stack ? error.stack.replace(error.message, message) : message);
184+
setStatus('status.failed', 'error');
157185
}
158186

159187
const emulatorScript = document.createElement('script');
160188
emulatorScript.src = 'emulator/caprice32.js';
161189
emulatorScript.async = true;
162190
emulatorScript.onerror = () => fatal(new Error(
163-
'Build WebAssembly absent. Exécutez npm run build, puis relancez le serveur.'
191+
t('error.buildMissing')
164192
));
165193
document.body.append(emulatorScript);
166194
loadCatalog();

0 commit comments

Comments
 (0)