330 lines
13 KiB
Markdown
330 lines
13 KiB
Markdown
---
|
|
title: 'How-to Guides'
|
|
description: 'Step-by-step recipes for common contributor tasks: languages, themes, deployment, migrations.'
|
|
slug: 'how-to-guides'
|
|
weight: 2
|
|
date: 2026-06-16
|
|
---
|
|
|
|
## Add a New Language
|
|
|
|
> TrainUs already ships three languages today — English, French, and Danish (`da`) — not just the EN/FR pair the project website emphasizes. The steps below walk through adding one more, using Spanish (`es`) as the worked example.
|
|
|
|
Adding a language requires four touch points: a new locale file, wiring it into the i18n initializer, a selector option in Settings, and a decision-log entry.
|
|
|
|
1. `src/i18n/locales/<code>.json` — create the translation file
|
|
2. `src/i18n/index.js` — import and register the locale
|
|
3. `src/views/SettingsView.vue` — add the `<option>` in the language selector
|
|
4. `docs/decisions-log.md` — record the decision
|
|
|
|
### 1. Create the translation file
|
|
|
|
Copy `src/i18n/locales/en.json` to `src/i18n/locales/<code>.json`, where `<code>` is the two-letter BCP 47 language tag (e.g. `es`, `de`, `it`).
|
|
|
|
Translate every string value. Do **not** change any key name. Keep all interpolation placeholders (`{{date}}`, `{{title}}`, etc.) and HTML entities exactly as they appear in the source.
|
|
|
|
### 2. Register the locale in i18next
|
|
|
|
Open `src/i18n/index.js`. Add an import at the top alongside the existing ones:
|
|
|
|
```js
|
|
import es from './locales/es.json'
|
|
```
|
|
|
|
Then add the locale to the `resources` object inside `i18next.init`:
|
|
|
|
```js
|
|
resources: {
|
|
en: { translation: en },
|
|
fr: { translation: fr },
|
|
da: { translation: da },
|
|
es: { translation: es }, // ← new
|
|
},
|
|
```
|
|
|
|
### 3. Add the option in the Settings selector
|
|
|
|
Open `src/views/SettingsView.vue` and locate the `<select>` element with `id="settings-language"`. Add one `<option>` after the existing ones:
|
|
|
|
```html
|
|
<option value="es">Español</option>
|
|
```
|
|
|
|
Use the language's native name as the label (not the English name), so speakers can identify it without reading English.
|
|
|
|
### 4. Update the decision log
|
|
|
|
Add an entry to `docs/decisions-log.md` recording the addition and why.
|
|
|
|
### Checklist
|
|
|
|
- [ ] `src/i18n/locales/<code>.json` created and fully translated
|
|
- [ ] Import added in `src/i18n/index.js`
|
|
- [ ] `<option>` added in `src/views/SettingsView.vue`
|
|
- [ ] Language can be selected in Settings and the UI switches correctly
|
|
- [ ] `docs/decisions-log.md` updated
|
|
|
|
---
|
|
|
|
## Add a New Theme
|
|
|
|
A theme is a named set of CSS variable overrides scoped to a `[data-theme='<name>']` attribute. The app reads the active theme from the database and sets that attribute on `<html>` before the first render.
|
|
|
|
1. `src/styles/variables.css` — define the CSS variable block
|
|
2. `src/i18n/locales/{en,fr,da}.json` — add the display label, in every supported language
|
|
3. `src/views/SettingsView.vue` — add the `<option>` in the theme selector
|
|
4. Manual smoke-test in the browser
|
|
5. `docs/decisions-log.md` — record the decision
|
|
|
|
### 1. Define the CSS variables
|
|
|
|
Open `src/styles/variables.css` and append a new block at the end. Copy the `:root` block as a starting point and change only the values that differ from the default light theme.
|
|
|
|
```css
|
|
/* My theme */
|
|
[data-theme='<name>'] {
|
|
--bg-color1: ...;
|
|
--bg-color2: ...;
|
|
--front-color1: ...;
|
|
--front-color2: ...;
|
|
--navbar-bg-color: ...;
|
|
--navbar-front-color: ...;
|
|
--navbar-hover-bg-color: ...;
|
|
--navbar-hover-front-color: ...;
|
|
--actionbar-bg-color: ...;
|
|
--actionbar-font-color: ...;
|
|
--border-color: ...;
|
|
--input-bg: ...;
|
|
--input-border: ...;
|
|
--btn-primary-bg: ...;
|
|
--btn-primary-color: ...;
|
|
--btn-danger-bg: ...;
|
|
--btn-danger-color: ...;
|
|
--success-color: ...;
|
|
--warning-color: ...;
|
|
}
|
|
```
|
|
|
|
The selector value (e.g. `trainus`) becomes the theme's key everywhere else. See the variable reference at the bottom of this recipe for what each one affects.
|
|
|
|
### 2. Add the display label in every supported language
|
|
|
|
TrainUs ships three languages today — English, French, and Danish (see [Add a New Language](#add-a-new-language) above) — so the new theme's label needs an entry in **all three** locale files, not just English and French. Add `"themeMyName"` to the `"settings"` object, after `"themeDark"`, in each:
|
|
|
|
`src/i18n/locales/en.json`:
|
|
|
|
```json
|
|
"themeMyName": "My Theme Label"
|
|
```
|
|
|
|
`src/i18n/locales/fr.json`:
|
|
|
|
```json
|
|
"themeMyName": "Mon thème"
|
|
```
|
|
|
|
`src/i18n/locales/da.json`:
|
|
|
|
```json
|
|
"themeMyName": "Mit tema"
|
|
```
|
|
|
|
If a language ends up missing the key, i18next falls back to `fallbackLng: 'en'` rather than crashing — but it's still worth catching during the smoke-test below, since a stray English label in another language's UI is easy to miss otherwise.
|
|
|
|
### 3. Add the option in the settings selector
|
|
|
|
In `src/views/SettingsView.vue`, locate the theme `<select>` block (search for `settings-theme`). Add one `<option>` after the existing ones:
|
|
|
|
```html
|
|
<option value="<name>">{{ $t('settings.themeMyName') }}</option>
|
|
```
|
|
|
|
The `value` must match the selector name from step 1.
|
|
|
|
### 4. Smoke-test
|
|
|
|
1. `npm run dev`
|
|
2. Open Settings → Theme → select your new theme.
|
|
3. Verify: navbar, background, buttons, and inputs all render with the new palette.
|
|
4. Refresh the page to confirm the theme persists (loaded from the database on startup).
|
|
5. Switch through each language (English, French, Danish) and confirm the theme's label is translated, not falling back to English.
|
|
6. Export settings and re-import to confirm the theme key round-trips correctly.
|
|
|
|
### 5. Update the decision log
|
|
|
|
Add an entry to `docs/decisions-log.md` recording the addition and why.
|
|
|
|
### Variable reference
|
|
|
|
| Variable | Affects |
|
|
| ---------------------------- | ----------------------------------- |
|
|
| `--bg-color1` | Main page background |
|
|
| `--bg-color2` | Card / secondary surface background |
|
|
| `--front-color1` | Primary text |
|
|
| `--front-color2` | Secondary / muted text |
|
|
| `--navbar-bg-color` | Side navigation background |
|
|
| `--navbar-front-color` | Side navigation text & icons |
|
|
| `--navbar-hover-bg-color` | Nav item hover background |
|
|
| `--navbar-hover-front-color` | Nav item hover text |
|
|
| `--actionbar-bg-color` | Top action bar background |
|
|
| `--actionbar-font-color` | Top action bar text & icons |
|
|
| `--border-color` | Card and input borders |
|
|
| `--input-bg` | Input field background |
|
|
| `--input-border` | Input field border |
|
|
| `--btn-primary-bg` | Primary button background |
|
|
| `--btn-primary-color` | Primary button text |
|
|
| `--btn-danger-bg` | Danger / delete button background |
|
|
| `--btn-danger-color` | Danger button text |
|
|
| `--success-color` | Success messages and indicators |
|
|
| `--warning-color` | Warning messages and indicators |
|
|
|
|
### Checklist
|
|
|
|
- [ ] CSS variable block added in `src/styles/variables.css`
|
|
- [ ] Display label added in every supported language (English, French, Danish)
|
|
- [ ] `<option>` added in `src/views/SettingsView.vue`
|
|
- [ ] Theme selectable, persists on reload, and round-trips through export/import
|
|
- [ ] `docs/decisions-log.md` updated
|
|
|
|
---
|
|
|
|
## Add a Database Migration
|
|
|
|
> **Ask first.** Per the project rules, `SCHEMA_VERSION` is only bumped once a schema has actually been deployed to users — and only after confirming with the project owner. Adding a column directly to `SCHEMA_SQL` (in `src/db/schema.js`) is enough for in-place changes before a release; you don't need any of the steps below for that case. Reach for a real migration only when existing users already have data under the old schema.
|
|
|
|
A `SCHEMA_VERSION` bump always ships together with exactly one entry in the `RELEASES` registry (`src/db/releases.js`), which is the single source of truth both the startup migration and the Restore Database compatibility check read from.
|
|
|
|
1. Bump `SCHEMA_VERSION` in `src/db/schema.js`.
|
|
2. Write the SQL that brings a live database from the previous version to the new one.
|
|
3. Add the corresponding entry to `RELEASES` in `src/db/releases.js`.
|
|
4. Test the upgrade path against a database still on the old version.
|
|
5. Update the reference docs.
|
|
|
|
### 1. Bump the version
|
|
|
|
```js
|
|
// src/db/schema.js
|
|
export const SCHEMA_VERSION = 2 // was 1
|
|
```
|
|
|
|
### 2. Write the migration script
|
|
|
|
A migration script is plain SQL — `ALTER TABLE`, data backfills, anything needed to bring the previous schema in line with the new one. It runs inside the same transaction as the `schema_version` stamp, so partial failures roll back cleanly.
|
|
|
|
### 3. Register the release
|
|
|
|
```js
|
|
// src/db/releases.js
|
|
export const RELEASES = [
|
|
{ schemaVersion: 1, appVersion: '1.0.0', dbBreak: false, migrationScript: null },
|
|
{
|
|
schemaVersion: 2,
|
|
appVersion: '1.1.0',
|
|
dbBreak: true,
|
|
migrationScript: `ALTER TABLE exercise ADD COLUMN category TEXT;`,
|
|
},
|
|
]
|
|
```
|
|
|
|
- `dbBreak: false` with `migrationScript: null` is a valid no-op entry (only the `schema_version` row is stamped) — use it for a version bump that doesn't actually need to touch existing data.
|
|
- `dbBreak: true` requires a non-null `migrationScript`; `getMigrationScripts()` throws if one is missing for a breaking release in range.
|
|
|
|
### 4. Test the upgrade path
|
|
|
|
Two code paths read `RELEASES`, and both need a check:
|
|
|
|
- **Startup migration**: open the app with an existing database stamped at the old `schema_version`. The app should show the blocking "download a backup first" screen, then apply your script and continue with data intact.
|
|
- **Restore Database**: restore a `.sqlite3` backup taken at the old schema version into a build running the new one — `checkCompatibility()` should detect the gap and run the same script during the restore's adaptation step.
|
|
|
|
See [Reference]({{< relref "technical.en.md" >}}) for the full migration state machine and flowchart, and [Understanding the Architecture]({{< relref "understanding-the-architecture.en.md" >}}) for why the system is built this way (forced backup, transactional migrations, fail-safe on a newer-than-app database).
|
|
|
|
### 5. Update the docs
|
|
|
|
- The schema table and ER diagram in [Reference]({{< relref "technical.en.md" >}})
|
|
- `docs/decisions-log.md`, with the date, decision, and rationale
|
|
|
|
### Checklist
|
|
|
|
- [ ] Confirmed with the project owner that a real version bump (not an in-place change) is warranted
|
|
- [ ] `SCHEMA_VERSION` bumped
|
|
- [ ] `RELEASES` entry added with a correct `migrationScript` (or `null` for a no-op)
|
|
- [ ] Startup migration tested against an old-version database
|
|
- [ ] Restore Database adaptation tested against an old-version backup
|
|
- [ ] Reference docs and `docs/decisions-log.md` updated
|
|
|
|
---
|
|
|
|
## Deploy TrainUs
|
|
|
|
TrainUs is a static site: `npm run build` outputs everything needed to `dist/`, ready to copy to any web server or CDN. There's a single deployment — each new build replaces the previous one at the site root, with no per-version folders. The service worker handles client-side code updates, and the startup migration system (see [Reference]({{< relref "technical.en.md" >}})) handles any database schema changes, so the same OPFS data simply carries over.
|
|
|
|
```bash
|
|
npm run build # outputs static files to dist/
|
|
npm run preview # preview the production build locally
|
|
```
|
|
|
|
TrainUs uses the `opfs-sahpool` VFS (see [Reference]({{< relref "technical.en.md" >}})), which doesn't strictly require COOP/COEP headers. Setting them anyway is recommended as defense-in-depth, and they may become required if the VFS implementation ever changes — the dev server already sets them as a precaution:
|
|
|
|
```
|
|
Cross-Origin-Opener-Policy: same-origin
|
|
Cross-Origin-Embedder-Policy: require-corp
|
|
```
|
|
|
|
**Nginx**
|
|
|
|
```nginx
|
|
location / {
|
|
add_header Cross-Origin-Opener-Policy "same-origin";
|
|
add_header Cross-Origin-Embedder-Policy "require-corp";
|
|
}
|
|
```
|
|
|
|
**Apache**
|
|
|
|
```apache
|
|
<IfModule mod_headers.c>
|
|
Header always set Cross-Origin-Opener-Policy "same-origin"
|
|
Header always set Cross-Origin-Embedder-Policy "require-corp"
|
|
</IfModule>
|
|
```
|
|
|
|
**Caddy**
|
|
|
|
```caddy
|
|
header {
|
|
Cross-Origin-Opener-Policy "same-origin"
|
|
Cross-Origin-Embedder-Policy "require-corp"
|
|
}
|
|
```
|
|
|
|
**Netlify** (`public/_headers`)
|
|
|
|
```
|
|
/*
|
|
Cross-Origin-Opener-Policy: same-origin
|
|
Cross-Origin-Embedder-Policy: require-corp
|
|
```
|
|
|
|
**Vercel** (`vercel.json`)
|
|
|
|
```json
|
|
{
|
|
"headers": [
|
|
{
|
|
"source": "/(.*)",
|
|
"headers": [
|
|
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
|
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Checklist
|
|
|
|
- [ ] `npm run build` completed without errors
|
|
- [ ] `dist/` deployed to the site root, replacing the previous build
|
|
- [ ] COOP/COEP headers configured on the server (recommended, not strictly required for `opfs-sahpool`)
|
|
- [ ] App loads and OPFS initializes correctly
|