Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .weblate
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# wlc (Weblate command-line client) configuration.
# https://docs.weblate.org/en/latest/wlc.html
#
# This only points the CLI at the server and the default component. The actual
# component configuration (file mask, monolingual base language, file format)
# lives in the Weblate project UI — see TRANSLATING.md.
#
# NEVER commit an API token here. Store it in ~/.config/weblate under [keys].

[weblate]
# Free Hosted Weblate (libre projects). Self-hosters: set your own URL.
url = https://hosted.weblate.org/api/

# Default project/component the CLI operates on. Adjust to your project slug.
[component "lyftr/web"]
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ git rebase origin/main

---

## Translations

The web UI is internationalised with react-i18next; all strings live in
`web/src/locales/`. Adding or improving a language is just editing JSON — no
code change. The easiest path is Weblate, which opens the PR for you.

See [TRANSLATING.md](TRANSLATING.md) for the full guide.

---

## Commit Messages

Follow conventional commits:
Expand Down
149 changes: 149 additions & 0 deletions TRANSLATING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Translating Lyftr

Lyftr's web UI is internationalised with [react-i18next](https://react.i18next.com/).
All UI strings live in **monolingual JSON** files under `web/src/locales/`, with
English (`en.json`) as the source language.

You do **not** need to be a developer to translate. The recommended path is
[Weblate](#translating-with-weblate-recommended), a web UI that opens the pull
request for you.

---

## How translations load

Every `web/src/locales/<code>.json` file is auto-discovered at build time
(`import.meta.glob` in `web/src/i18n.ts`). Adding a language is therefore just
**committing one JSON file** — no code change, no registration step. The new
language appears automatically in **Settings → Appearance**, labelled by its own
name (e.g. `Deutsch`, `Français`).

`<code>` is a [BCP-47](https://en.wikipedia.org/wiki/IETF_language_tag) code:
`de`, `fr`, `es`, `pt-BR`, `zh`, … Region-specific browser locales fall back to
the base language (`de-AT` → `de`), and anything untranslated falls back to
English.

---

## Translating with Weblate (recommended)

1. Open the Lyftr project on Weblate and pick (or **+ Add**) your language.
2. Translate strings in the web editor. Weblate shows the English source beside
each entry and flags anything that breaks a [placeholder](#keys-and-placeholders).
3. Your changes are committed to the translation file and pushed back to GitHub
as a pull request automatically — no git knowledge required.

That's it. A maintainer reviews and merges the PR, and the language ships on the
next build.

---

## Translating manually (via a pull request)

For developers, or one-off contributions without a Weblate account:

1. Copy the source file to your language code:
```bash
cp web/src/locales/en.json web/src/locales/de.json
```
2. Translate the **values** in `de.json`. Leave the keys and every
`{{placeholder}}` exactly as-is (see below).
3. Build to confirm it loads and the language shows up in the switcher:
```bash
cd web && npm run build
```
4. Commit and open a PR against `main` (branch name: `feature/i18n-<code>`).

No edit to `i18n.ts` or any other file is required — the JSON file is enough.

---

## Keys and placeholders

- **Never translate or rename keys.** Only translate the string on the right of
the colon.
```jsonc
// en.json // de.json
"heading": "Welcome back" "heading": "Willkommen zurück"
```
- **Keep `{{placeholders}}` verbatim.** They are filled in at runtime. Their
position in the sentence may move, but the token must survive:
```jsonc
"current": "Current: {{url}}" "current": "Aktuell: {{url}}"
"synced": "Synced {{count}} exercises" "synced": "{{count}} Übungen synchronisiert"
```
- **Plurals** use i18next's `_one` / `_other` suffixes on the same key, driven by
`{{count}}`. Languages with more plural forms (e.g. Polish, Arabic) get extra
suffixes — Weblate generates the right set for the target language
automatically.
```jsonc
"count_one": "{{count}} exercise",
"count_other": "{{count}} exercises"
```
- Keep punctuation/spacing that belongs to the sentence; adapt it to your
language's norms (French, for instance, uses `:` differently).

---

## Maintainer: connecting Weblate

Configure one component in the Weblate project UI:

| Setting | Value |
|---|---|
| File format | **i18next JSON v4** |
| File mask | `web/src/locales/*.json` |
| Monolingual base language file | `web/src/locales/en.json` |
| Source language | English |
| New translations | Use the base file as template |

The `github` VCS backend ("GitHub pull request") makes Weblate push
translations back as pull requests instead of committing to `main` directly.

### Recommended: free Hosted Weblate (the wger approach)

Lyftr is MIT-licensed and developed in the open, so it qualifies for
[Hosted Weblate's free plan for libre projects](https://hosted.weblate.org/hosting/) —
the same setup [wger](https://hosted.weblate.org/engage/wger/) and most
open-source apps use. No server, no Docker.

1. Sign in at [hosted.weblate.org](https://hosted.weblate.org/) and **add a
project**, pointing it at `github.com/Cawlumm/lyftr`.
2. Add a component with the values in the table above and `github` as the VCS
backend; authorize Weblate's GitHub App when prompted so it can open PRs.
3. Request libre hosting — a Weblate maintainer approves it (one-time).

Translators then work at `hosted.weblate.org/projects/lyftr/` and their changes
land as pull requests against `main`.

### Alternative: self-host

To run your own instance (e.g. to keep the repo private), use the official
Docker stack plus [`scripts/weblate-provision.sh`](scripts/weblate-provision.sh),
which creates the project + component via the API:

```bash
git clone https://github.com/WeblateOrg/docker.git weblate-docker
# in ./environment set WEBLATE_SITE_DOMAIN, *_ADMIN_*, WEBLATE_GITHUB_USERNAME+TOKEN, then:
docker compose up -d
WEBLATE_URL=https://weblate.example.com WEBLATE_TOKEN=<token> scripts/weblate-provision.sh
```

### The `.weblate` CLI config

The repo-root `.weblate` file points the
[`wlc`](https://docs.weblate.org/en/latest/wlc.html) CLI at the server and
default component. It defaults to Hosted Weblate; change the `url` only if you
self-host. **Never commit an API token**; `wlc` reads it from `~/.config/weblate`.

---

## Testing a language locally

```bash
cd web && npm run dev
```

Open the app, go to **Settings → Appearance**, and switch the language. The
choice is saved to `localStorage` (`lyftr_lang`) and `<html lang>` updates so
screen readers and spellcheck follow along.
91 changes: 91 additions & 0 deletions scripts/weblate-provision.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
#
# Provision the Lyftr translation project on a self-hosted Weblate instance.
#
# This talks to the Weblate REST API to create one project ("Lyftr") and one
# component ("Web") wired to this repo's locale files. Run it ONCE, after your
# self-hosted Weblate is up and you have an admin API token.
#
# Settings → API access → generate a token
#
# Usage:
# WEBLATE_URL=https://weblate.example.com \
# WEBLATE_TOKEN=wlu_xxx \
# scripts/weblate-provision.sh
#
# The component uses the "GitHub pull request" VCS backend (vcs=github), so the
# instance must already have GitHub credentials configured (WEBLATE_GITHUB_TOKEN
# / WEBLATE_GITHUB_USERNAME in the Weblate container env) — otherwise component
# creation fails. See TRANSLATING.md.

set -euo pipefail

: "${WEBLATE_URL:?Set WEBLATE_URL, e.g. https://weblate.example.com}"
: "${WEBLATE_TOKEN:?Set WEBLATE_TOKEN (Settings → API access)}"

# --- Lyftr-specific values -------------------------------------------------
PROJECT_NAME="Lyftr"
PROJECT_SLUG="lyftr"
PROJECT_WEB="https://github.com/Cawlumm/lyftr"

COMPONENT_NAME="Web"
COMPONENT_SLUG="web"
REPO="https://github.com/Cawlumm/lyftr.git"
BRANCH="main"
FILEMASK="web/src/locales/*.json"
TEMPLATE="web/src/locales/en.json" # monolingual base + template for new langs
SOURCE_LANGUAGE="en"

# Version-sensitive identifiers — confirm against your instance if creation
# rejects them (GET ${API}/file-formats/ lists valid file_format slugs):
# FILE_FORMAT : i18next JSON v4
# VCS : "github" == the GitHub pull request backend (opens PRs)
FILE_FORMAT="${FILE_FORMAT:-i18nextv4}"
VCS="${VCS:-github}"
# ---------------------------------------------------------------------------

API="${WEBLATE_URL%/}/api"
AUTH=(-H "Authorization: Token ${WEBLATE_TOKEN}")

# POST helper: prints HTTP status + body, fails the script on a 4xx/5xx that
# isn't a duplicate (Weblate returns 400 with "already exists" on re-runs).
post() {
local url="$1"; shift
local out status body
out="$(curl -sS -w $'\n%{http_code}' "${AUTH[@]}" "$url" "$@")"
status="${out##*$'\n'}"
body="${out%$'\n'*}"
echo " → HTTP ${status}"
if [[ "$status" -ge 400 ]]; then
if grep -qiE 'already exists|must be unique' <<<"$body"; then
echo " (already exists — skipping)"
return 0
fi
echo "$body" >&2
return 1
fi
}

echo "Creating project '${PROJECT_SLUG}' on ${WEBLATE_URL} ..."
post "${API}/projects/" \
--data-urlencode "name=${PROJECT_NAME}" \
--data-urlencode "slug=${PROJECT_SLUG}" \
--data-urlencode "web=${PROJECT_WEB}"

echo "Creating component '${PROJECT_SLUG}/${COMPONENT_SLUG}' ..."
post "${API}/projects/${PROJECT_SLUG}/components/" \
--data-urlencode "name=${COMPONENT_NAME}" \
--data-urlencode "slug=${COMPONENT_SLUG}" \
--data-urlencode "repo=${REPO}" \
--data-urlencode "push=${REPO}" \
--data-urlencode "branch=${BRANCH}" \
--data-urlencode "vcs=${VCS}" \
--data-urlencode "file_format=${FILE_FORMAT}" \
--data-urlencode "filemask=${FILEMASK}" \
--data-urlencode "template=${TEMPLATE}" \
--data-urlencode "new_base=${TEMPLATE}" \
--data-urlencode "source_language=${SOURCE_LANGUAGE}"

echo
echo "Done. Open ${WEBLATE_URL}/projects/${PROJECT_SLUG}/${COMPONENT_SLUG}/ to verify."
echo "Translators can now add a language; Weblate will open a PR against '${BRANCH}'."
75 changes: 75 additions & 0 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
"@tanstack/react-virtual": "^3.13.24",
"axios": "^1.6.2",
"date-fns": "^3.0.0",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^1.8.0",
"react": "^18.2.0",
"react-body-highlighter": "^2.0.5",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.3",
"react-router-dom": "^6.22.0",
"react-zxing": "^2.1.0",
"recharts": "^2.10.3",
Expand Down
Loading
Loading