mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(i18n): 21-language pipeline, landing/docs/API wiring, landing+API translations
Shared Claude Code translation pipeline (scripts/i18n, no API key) plus Astro/VitePress/Scalar i18n wiring. Landing and API reference translated into all 20 languages; docs i18n wiring + English source anchors. The translated docs markdown (apps/docs/<locale>/**, 3,620 files) follows in a companion PR because it exceeds GitHub's per-PR CI file limit.
This commit is contained in:
@@ -2,11 +2,11 @@
|
||||
description: 21 supported languages and how to create or improve translations for SnapOtter using the TypeScript-enforced i18n system.
|
||||
---
|
||||
|
||||
# Translation guide
|
||||
# Translation guide {#translation-guide}
|
||||
|
||||
SnapOtter ships with 21 languages out of the box. The i18n system uses a lightweight custom runtime with TypeScript-enforced locale completeness and dynamic code-splitting.
|
||||
|
||||
## Supported languages
|
||||
## Supported languages {#supported-languages}
|
||||
|
||||
| Code | Language | Native Name | Direction |
|
||||
|------|----------|-------------|-----------|
|
||||
@@ -32,7 +32,7 @@ SnapOtter ships with 21 languages out of the box. The i18n system uses a lightwe
|
||||
| `id` | Indonesian | Bahasa Indonesia | LTR |
|
||||
| `th` | Thai | ไทย | LTR |
|
||||
|
||||
## How language detection works
|
||||
## How language detection works {#how-language-detection-works}
|
||||
|
||||
SnapOtter uses a three-tier resolution order:
|
||||
|
||||
@@ -48,7 +48,7 @@ Users can change language from:
|
||||
- The **mobile sidebar** language dropdown
|
||||
- The **Settings > System** section sets the instance-wide default (admin only)
|
||||
|
||||
## How translations work
|
||||
## How translations work {#how-translations-work}
|
||||
|
||||
All UI strings live in `packages/shared/src/i18n/`. The reference file is `en.ts`, which exports a typed object with every string the app uses (~1500 keys). Other languages are separate files (e.g., `de.ts`, `fr.ts`) that export the same shape.
|
||||
|
||||
@@ -56,7 +56,7 @@ The `TranslationKeys` type uses `DeepStringRecord` to accept any string value wh
|
||||
|
||||
Only the active locale is loaded at runtime via dynamic `import()`, keeping the main bundle small.
|
||||
|
||||
## Using translations in components
|
||||
## Using translations in components {#using-translations-in-components}
|
||||
|
||||
```tsx
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
@@ -75,7 +75,7 @@ function MyComponent() {
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing a translation
|
||||
## Contributing a translation {#contributing-a-translation}
|
||||
|
||||
We welcome translation PRs directly. You can improve an existing locale or add a new one.
|
||||
|
||||
@@ -85,9 +85,9 @@ To report a mistranslation without submitting code, open a [GitHub Issue](https:
|
||||
Translation PRs do not require prior approval. Fork the repo, make your changes, and open a PR. See the [Contributing Guide](/guide/contributing) for the full PR process and CLA requirement.
|
||||
:::
|
||||
|
||||
## How to create or update a translation
|
||||
## How to create or update a translation {#how-to-create-or-update-a-translation}
|
||||
|
||||
### 1. Fork and clone
|
||||
### 1. Fork and clone {#_1-fork-and-clone}
|
||||
|
||||
```bash
|
||||
git clone https://github.com/<your-username>/snapotter.git
|
||||
@@ -95,7 +95,7 @@ cd snapotter
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 2. Copy the reference file (new language only)
|
||||
### 2. Copy the reference file (new language only) {#_2-copy-the-reference-file-new-language-only}
|
||||
|
||||
Skip this step if you are improving an existing translation.
|
||||
|
||||
@@ -103,7 +103,7 @@ Skip this step if you are improving an existing translation.
|
||||
cp packages/shared/src/i18n/en.ts packages/shared/src/i18n/XX.ts
|
||||
```
|
||||
|
||||
### 3. Translate the strings
|
||||
### 3. Translate the strings {#_3-translate-the-strings}
|
||||
|
||||
Open your new file and translate every string value. Keep the object structure and keys exactly the same.
|
||||
|
||||
@@ -127,7 +127,7 @@ Rules:
|
||||
- Arrays (`rotatingPhrases`, `progressMessages`) must have the same number of entries
|
||||
- Do not translate: SnapOtter, JPEG, PNG, WebP, EXIF, API, and other technical terms
|
||||
|
||||
### 4. Register the locale (new language only)
|
||||
### 4. Register the locale (new language only) {#_4-register-the-locale-new-language-only}
|
||||
|
||||
Add your locale to `SUPPORTED_LOCALES` in `packages/shared/src/i18n/index.ts`:
|
||||
|
||||
@@ -135,7 +135,7 @@ Add your locale to `SUPPORTED_LOCALES` in `packages/shared/src/i18n/index.ts`:
|
||||
{ code: "xx", name: "Language Name", nativeName: "Native Name", dir: "ltr" },
|
||||
```
|
||||
|
||||
### 5. Verify
|
||||
### 5. Verify {#_5-verify}
|
||||
|
||||
```bash
|
||||
pnpm typecheck # catches missing or mistyped keys
|
||||
@@ -143,11 +143,11 @@ pnpm lint # formatting check
|
||||
pnpm dev # manually verify strings appear correctly
|
||||
```
|
||||
|
||||
### 6. Submit
|
||||
### 6. Submit {#_6-submit}
|
||||
|
||||
Open a PR against `main` with a title like `feat(i18n): add Swedish translation` or `fix(i18n): correct German typos`. The CLA bot will ask you to sign on your first contribution.
|
||||
|
||||
## Adding new translation keys
|
||||
## Adding new translation keys {#adding-new-translation-keys}
|
||||
|
||||
When adding a new feature that needs new UI strings:
|
||||
|
||||
@@ -155,7 +155,7 @@ When adding a new feature that needs new UI strings:
|
||||
2. Run `pnpm typecheck` - every locale file will fail if missing the new key
|
||||
3. Add the new key to all locale files (use English as a temporary fallback)
|
||||
|
||||
## Configuration
|
||||
## Configuration {#configuration}
|
||||
|
||||
Set the instance default language via environment variable:
|
||||
|
||||
@@ -163,7 +163,7 @@ Set the instance default language via environment variable:
|
||||
DEFAULT_LOCALE: "de" # German as the default for all new users
|
||||
```
|
||||
|
||||
## File reference
|
||||
## File reference {#file-reference}
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
@@ -173,3 +173,59 @@ DEFAULT_LOCALE: "de" # German as the default for all new users
|
||||
| `apps/web/src/contexts/i18n-context.tsx` | `I18nProvider`, `useTranslation()` hook |
|
||||
| `apps/web/src/lib/format.ts` | `format()`, `plural()`, `formatFileSize()` helpers |
|
||||
| `apps/api/src/routes/config.ts` | `GET /api/v1/config/locale` public endpoint |
|
||||
|
||||
## Translating the website, docs, and API reference {#translating-the-web-surfaces}
|
||||
|
||||
The 21-language support above covers the **app**. The public website
|
||||
(snapotter.com), this documentation site, and the REST API reference are also
|
||||
translated into all 21 languages, by a separate hash-gated pipeline that reuses
|
||||
the same tool names and descriptions from `packages/shared/src/i18n`, so
|
||||
terminology stays consistent everywhere.
|
||||
|
||||
### Machine-translated by default {#machine-translated-by-default}
|
||||
|
||||
Every non-English page on the website and docs is **machine-translated** on the
|
||||
first pass (by a Claude Code session, not a third-party service) and carries a
|
||||
small, dismissible banner saying so, with a link back here. That is deliberate:
|
||||
it ships all 21 languages quickly and honestly, then invites the community to
|
||||
refine the pages that matter most. Machine translation gets the meaning across;
|
||||
human review makes it read naturally.
|
||||
|
||||
### How the pipeline decides what to translate {#how-the-web-pipeline-decides}
|
||||
|
||||
Each translatable unit of English source is hashed, and the hash is stored next
|
||||
to its translation. On each run the pipeline:
|
||||
|
||||
- translates any unit that has no translation yet,
|
||||
- skips any unit whose stored hash still matches the English source,
|
||||
- re-translates a **machine** unit when its English source changes,
|
||||
- and flags a **human**-refined unit as `stale` (needs review) when its English
|
||||
source changes, instead of overwriting your work.
|
||||
|
||||
### Refining a web translation by PR {#refining-a-web-translation-by-pr}
|
||||
|
||||
You improve a website, docs, or API-reference translation the same way you
|
||||
improve an app locale: by editing the generated file and opening a PR.
|
||||
|
||||
1. Find the generated translation for your language:
|
||||
- website UI strings: `apps/landing/src/i18n/<locale>.json`
|
||||
- a docs page: `apps/docs/<locale>/**.md`
|
||||
- the API reference: `apps/api/src/openapi.<locale>.yaml`
|
||||
2. Edit the text. Keep code, links, `{placeholders}`, and any `⸤I18N…⸥` markers
|
||||
exactly as they are; the pipeline's validator rejects a translation that drops
|
||||
or reorders them.
|
||||
3. Open a PR. Editing a unit flips its provenance from `machine` to `human`, so
|
||||
the pipeline will **never overwrite it** on a later run. If the English source
|
||||
changes afterwards, your unit is flagged `stale` for review rather than
|
||||
silently replaced.
|
||||
|
||||
To report a mistranslation without submitting code, open a
|
||||
[GitHub Issue](https://github.com/snapotter-hq/SnapOtter/issues) with the page
|
||||
URL, the language, the incorrect text, and your suggested fix.
|
||||
|
||||
::: tip
|
||||
Maintainers run the translation pipeline; you do not need an API key to
|
||||
contribute. Just edit the generated file and open a PR. See
|
||||
[`scripts/i18n/README.md`](https://github.com/snapotter-hq/SnapOtter/blob/main/scripts/i18n/README.md)
|
||||
for how the pipeline runs.
|
||||
:::
|
||||
|
||||
Reference in New Issue
Block a user