diff --git a/.prettierignore b/.prettierignore index d31480228..87bd3e86c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -29,3 +29,8 @@ scripts/bench/results # Golden copies of the backup file and the restored account, written by the # parity test itself (BACKUP_PARITY_WRITE=1), so a rewrite diffs cleanly. tests/integration/__golden__ + +# inlang writes its working files (cache, metadata, README) next to the +# project settings when its CLI or SDK runs; only settings.json is tracked. +project.inlang/* +!project.inlang/settings.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb70d27cb..d30803d6b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,6 +134,26 @@ To suggest a fix or improvement: 3. Edit `messages/.json`. Keep the JSON valid; the i18n integrity test will flag missing or extra keys against `en.json` before CI accepts the PR. 4. Open a PR titled `i18n(): `. Describe what was unclear, awkward, or wrong in the previous wording. +### Translating in the browser + +Every locale can be edited in [Fink](https://fink.inlang.com/?repo=MBombeck/HealthLog&project=project.inlang), inlang's browser editor, without cloning the repository. Fink reads `project.inlang/settings.json`, which points it at `messages/.json`, shows each message next to its translations, and flags strings that are missing or use other placeholders than English. + +1. Open the link and sign in with GitHub. +2. Without write access to this repository, choose **Fork**. Fink asks you to install the Inlang GitHub App on your own account so it can push to your fork. +3. Edit, then choose **Push**. Fink commits to the fork; **Open pull request** takes you to GitHub's compare page, where you submit the change against `main`. + +Fink writes the same nested JSON this repository uses and changes only the lines you edited. A string you add for a key the locale did not have yet goes to the end of its section instead of the position it has in `en.json`, which is harmless. + +What a reviewer checks on a translation pull request: + +- Only `messages/*.json` changed, and only the strings the description names. +- Every `{placeholder}` of the English string is still there and spelled the same. The i18n integrity test fails the build otherwise. +- Medical terms are the ones the language's clinical guidelines use, and the register matches the rest of the locale (`du` in German, `você` in Brazilian Portuguese). +- Words the app or the Telegram bot matches literally stay as they are: slash commands, `taken `, and the unit symbols. +- Machine translation is fine as a first draft, but someone who reads the language has gone through every string before the pull request is opened, and the description says so. + +To run the checks Fink shows on a local checkout: `npx @inlang/cli@3.4.4 check --project ./project.inlang`. It reads the files and changes nothing. + ### Adding a new locale If you want to introduce a locale that isn't shipped (e.g. `sv`, `cs`): @@ -141,9 +161,8 @@ If you want to introduce a locale that isn't shipped (e.g. `sv`, `cs`): 1. Open an issue first using the **Translation request** template. We'll confirm the locale is in scope and decide whether it joins the AI-initial-community-maintained tier or the maintained tier. 2. Copy `messages/en.json` to `messages/.json`. 3. Translate. AI assistance is welcome as a starting point but the PR description should note whether it's an AI-initial pass or a fully human-reviewed one — that information drives the banner state. -4. Add the locale to the supported list in `src/lib/i18n/locales.ts` (or wherever the registry lives — grep `SUPPORTED_LOCALES`). -5. The Coach system prompts at `src/lib/ai/prompts/coach-prompt.ts` and `src/lib/ai/prompts/insight-generator.ts` and the doctor-report PDF strings need translation too. Include those in the same PR. -6. Run `pnpm test -- i18n-integrity` locally before submitting. +4. Add the locale to `locales` in `src/lib/i18n/config.ts` and to `locales` in `project.inlang/settings.json`, then run `pnpm typecheck`. Every per-locale table that still lacks the new locale fails to compile, and that list is the work: date and number formatting, the reply-language footers of the Coach and insights prompts, the outbound safety banks, the prose-grounding patterns and the no-key fallback copy. Include those in the same PR. +5. Run `pnpm test` before submitting. The i18n guards hold the new bundle to the English keys and placeholders. ### Style guide for translators @@ -156,7 +175,7 @@ If you want to introduce a locale that isn't shipped (e.g. `sv`, `cs`): ### Long-term -When the community grows beyond ad-hoc PRs, we'll spin up a self-hosted [Weblate](https://weblate.org/) instance. That replaces the JSON-PR workflow with a web UI but keeps every translation under our own roof — no third-party telemetry. Tracking issue: `v1.6 — Weblate self-hosted on edge-01`. +Fink covers browser editing today. It edits the files in this repository directly and keeps no copy of the translations anywhere else. If the community outgrows it, a self-hosted [Weblate](https://weblate.org/) instance remains the fallback. Tracking issue: `v1.6 — Weblate self-hosted on edge-01`. ## Code of Conduct diff --git a/README.md b/README.md index 644499bfa..299fa3065 100644 --- a/README.md +++ b/README.md @@ -109,13 +109,14 @@ Next.js 16 (App Router, React Server Components), TypeScript strict, PostgreSQL ## Documentation -| | | -| ---------------------- | ------------------------------------------------ | -| User and operator docs | [docs.healthlog.dev](https://docs.healthlog.dev) | -| Release history | [CHANGELOG.md](CHANGELOG.md) | -| API contract | [`docs/api/openapi.yaml`](docs/api/openapi.yaml) | -| Security policy | [SECURITY.md](SECURITY.md) | -| Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) | +| | | +| ---------------------- | ------------------------------------------------------------ | +| User and operator docs | [docs.healthlog.dev](https://docs.healthlog.dev) | +| Release history | [CHANGELOG.md](CHANGELOG.md) | +| API contract | [`docs/api/openapi.yaml`](docs/api/openapi.yaml) | +| Security policy | [SECURITY.md](SECURITY.md) | +| Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) | +| Translating | [CONTRIBUTING.md#translations](CONTRIBUTING.md#translations) | ## Status diff --git a/project.inlang/settings.json b/project.inlang/settings.json new file mode 100644 index 000000000..f040a027d --- /dev/null +++ b/project.inlang/settings.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "baseLocale": "en", + "locales": ["en", "de", "fr", "es", "it", "pl", "ko", "nl", "pt-BR"], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4.5.5/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + } +} diff --git a/src/__tests__/inlang-project-settings.test.ts b/src/__tests__/inlang-project-settings.test.ts new file mode 100644 index 000000000..eaa805da6 --- /dev/null +++ b/src/__tests__/inlang-project-settings.test.ts @@ -0,0 +1,48 @@ +/** + * The inlang project that the Fink browser editor opens. + * + * `project.inlang/settings.json` names the locales Fink offers and the files it + * writes. A locale added to the i18n config but not here never shows + * up in the editor, and one listed here but not shipped would invite + * translations of a bundle the app never loads. The plugin is pinned to an + * exact version: it decides how the nested JSON is written back, and the + * round trip was verified against that version only (an unpinned URL would + * pick up a new serialiser on its own). + */ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { defaultLocale, locales } from "@/lib/i18n/config"; + +const SETTINGS = join(__dirname, "../../project.inlang/settings.json"); + +interface InlangSettings { + baseLocale: string; + locales: string[]; + modules: string[]; + "plugin.inlang.messageFormat"?: { pathPattern?: string }; +} + +const settings = JSON.parse(readFileSync(SETTINGS, "utf8")) as InlangSettings; + +describe("inlang project settings", () => { + it("offers exactly the shipped locales, English as the source", () => { + expect(settings.baseLocale).toBe(defaultLocale); + expect([...settings.locales].sort()).toEqual([...locales].sort()); + }); + + it("points the message-format plugin at the catalogues", () => { + expect(settings["plugin.inlang.messageFormat"]?.pathPattern).toBe( + "./messages/{locale}.json", + ); + }); + + it("pins the one plugin to an exact version", () => { + expect(settings.modules).toHaveLength(1); + expect(settings.modules[0]).toMatch( + /^https:\/\/cdn\.jsdelivr\.net\/npm\/@inlang\/plugin-message-format@\d+\.\d+\.\d+\/dist\/index\.js$/, + ); + }); +});