From 96ec9809d887b69e1d527f2a30212dbea6ad2ed7 Mon Sep 17 00:00:00 2001 From: Charlie Hopkins-Brinicombe Date: Wed, 1 Apr 2026 09:46:01 +0100 Subject: [PATCH] Refactor documentation paths in JSON and MDX files to remove locale prefixes, ensuring consistency across all languages. Updated README and localization workflow to reflect changes in file structure and validation scripts. --- .cursor/rules/localization-workflow.mdc | 10 +- .github/workflows/validate-translations.yml | 15 +- README.md | 20 +- {en/account => account}/billing.mdx | 24 +-- {en/account => account}/branding.mdx | 4 +- {en/account => account}/members.mdx | 4 +- {en/account => account}/overview.mdx | 6 +- .../endpoints/points/archive-a-boost.mdx | 2 +- .../endpoints/points/archive-boosts-batch.mdx | 2 +- .../endpoints/points/create-boosts.mdx | 2 +- .../endpoints/streaks/grant-freezes.mdx | 2 +- .../endpoints/streaks/restore-streaks.mdx | 2 +- {en/admin-api => admin-api}/introduction.mdx | 2 +- .../authentication.mdx | 4 +- .../client-libraries.mdx | 0 .../achievements/all-achievements.mdx | 2 +- .../mark-an-achievement-as-completed.mdx | 2 +- .../get-all-active-leaderboards.mdx | 2 +- .../leaderboards/get-leaderboard.mdx | 2 +- .../metrics/send-a-metric-change-event.mdx | 2 +- .../endpoints/points/get-points-boosts.mdx | 2 +- .../points/get-points-level-summary.mdx | 2 +- .../endpoints/points/get-points-levels.mdx | 2 +- .../endpoints/points/get-points-summary.mdx | 2 +- .../endpoints/points/get-points.mdx | 2 +- .../endpoints/streaks/get-streak-rankings.mdx | 2 +- .../endpoints/streaks/get-streaks.mdx | 2 +- .../endpoints/users/create-a-user.mdx | 2 +- ...single-metric-event-summary-for-a-user.mdx | 2 +- .../users/get-a-single-metric-for-a-user.mdx | 2 +- .../endpoints/users/get-a-single-user.mdx | 2 +- .../get-a-users-completed-achievements.mdx | 2 +- .../users/get-a-users-leaderboard.mdx | 2 +- .../users/get-a-users-points-boosts.mdx | 2 +- .../users/get-a-users-points-summary.mdx | 2 +- .../endpoints/users/get-a-users-points.mdx | 2 +- .../endpoints/users/get-a-users-streak.mdx | 2 +- .../endpoints/users/get-a-users-wrapped.mdx | 2 +- .../users/get-all-metrics-for-a-user.mdx | 2 +- .../endpoints/users/get-user-preferences.mdx | 2 +- .../endpoints/users/identify-a-user.mdx | 2 +- .../endpoints/users/update-a-user.mdx | 2 +- .../users/update-user-preferences.mdx | 2 +- .../idempotency.mdx | 8 +- .../introduction.mdx | 10 +- .../rate-limiting.mdx | 2 +- {en/components => components}/plan-badge.jsx | 0 .../rate-limit-badge.jsx | 2 +- docs.json | 178 +++++++++--------- es/account/billing.mdx | 2 +- es/guides/how-to-build-an-energy-feature.mdx | 2 +- es/guides/how-to-build-an-xp-feature.mdx | 2 +- es/platform/emails.mdx | 2 +- es/platform/leaderboards.mdx | 12 +- es/platform/streaks.mdx | 2 +- .../engagement.mdx | 8 +- .../overview.mdx | 14 +- .../retention.mdx | 4 +- .../introduction.mdx | 12 +- .../quickstart.mdx | 30 +-- .../gamified-fitness-platform.mdx | 24 +-- .../gamified-study-platform.mdx | 48 ++--- .../how-to-build-a-leaderboards-feature.mdx | 40 ++-- .../how-to-build-a-streaks-feature.mdx | 32 ++-- .../how-to-build-an-achievements-feature.mdx | 42 ++--- .../how-to-build-an-energy-feature.mdx | 40 ++-- .../how-to-build-an-xp-feature.mdx | 46 ++--- i18n.json | 172 ++++++++--------- package.json | 1 + {en/platform => platform}/achievements.mdx | 52 ++--- {en/platform => platform}/emails.mdx | 86 ++++----- {en/platform => platform}/events.mdx | 34 ++-- {en/platform => platform}/leaderboards.mdx | 48 ++--- {en/platform => platform}/metrics.mdx | 10 +- {en/platform => platform}/overview.mdx | 18 +- {en/platform => platform}/points.mdx | 98 +++++----- .../push-notifications.mdx | 28 +-- {en/platform => platform}/streaks.mdx | 30 +-- {en/platform => platform}/users.mdx | 60 +++--- scripts/localize-component-imports.mjs | 23 ++- scripts/localize-internal-links.mjs | 36 +++- scripts/sync-heading-anchors.mjs | 39 +++- scripts/sync-openapi-titles.mjs | 40 +++- scripts/validate-link-fragments.mjs | 63 +++++++ scripts/validate-translations.mjs | 116 +++++++----- .../achievements/achievement-completed.mdx | 2 +- .../leaderboards/leaderboard-changed.mdx | 0 .../leaderboards/leaderboard-finished.mdx | 2 +- .../leaderboards/leaderboard-rank-changed.mdx | 0 .../leaderboards/leaderboard-started.mdx | 0 .../events/points/points-boost-finished.mdx | 0 .../events/points/points-boost-started.mdx | 0 .../events/points/points-changed.mdx | 2 +- .../events/points/points-level-changed.mdx | 2 +- .../events/streaks/streak-extended.mdx | 2 +- .../events/streaks/streak-freeze-consumed.mdx | 0 .../events/streaks/streak-freeze-earned.mdx | 0 .../events/streaks/streak-lost.mdx | 0 .../events/streaks/streak-started.mdx | 0 {en/webhooks => webhooks}/idempotency.mdx | 2 +- {en/webhooks => webhooks}/introduction.mdx | 12 +- {en/webhooks => webhooks}/observability.mdx | 2 +- {en/webhooks => webhooks}/quickstart.mdx | 10 +- {en/webhooks => webhooks}/retries.mdx | 0 {en/webhooks => webhooks}/security.mdx | 2 +- 105 files changed, 959 insertions(+), 750 deletions(-) rename {en/account => account}/billing.mdx (86%) rename {en/account => account}/branding.mdx (89%) rename {en/account => account}/members.mdx (94%) rename {en/account => account}/overview.mdx (76%) rename {en/admin-api => admin-api}/endpoints/points/archive-a-boost.mdx (62%) rename {en/admin-api => admin-api}/endpoints/points/archive-boosts-batch.mdx (62%) rename {en/admin-api => admin-api}/endpoints/points/create-boosts.mdx (63%) rename {en/admin-api => admin-api}/endpoints/streaks/grant-freezes.mdx (64%) rename {en/admin-api => admin-api}/endpoints/streaks/restore-streaks.mdx (63%) rename {en/admin-api => admin-api}/introduction.mdx (75%) rename {en/api-reference => api-reference}/authentication.mdx (95%) rename {en/api-reference => api-reference}/client-libraries.mdx (100%) rename {en/api-reference => api-reference}/endpoints/achievements/all-achievements.mdx (64%) rename {en/api-reference => api-reference}/endpoints/achievements/mark-an-achievement-as-completed.mdx (64%) rename {en/api-reference => api-reference}/endpoints/leaderboards/get-all-active-leaderboards.mdx (59%) rename {en/api-reference => api-reference}/endpoints/leaderboards/get-leaderboard.mdx (61%) rename {en/api-reference => api-reference}/endpoints/metrics/send-a-metric-change-event.mdx (62%) rename {en/api-reference => api-reference}/endpoints/points/get-points-boosts.mdx (63%) rename {en/api-reference => api-reference}/endpoints/points/get-points-level-summary.mdx (65%) rename {en/api-reference => api-reference}/endpoints/points/get-points-levels.mdx (63%) rename {en/api-reference => api-reference}/endpoints/points/get-points-summary.mdx (63%) rename {en/api-reference => api-reference}/endpoints/points/get-points.mdx (63%) rename {en/api-reference => api-reference}/endpoints/streaks/get-streak-rankings.mdx (62%) rename {en/api-reference => api-reference}/endpoints/streaks/get-streaks.mdx (62%) rename {en/api-reference => api-reference}/endpoints/users/create-a-user.mdx (58%) rename {en/api-reference => api-reference}/endpoints/users/get-a-single-metric-event-summary-for-a-user.mdx (67%) rename {en/api-reference => api-reference}/endpoints/users/get-a-single-metric-for-a-user.mdx (63%) rename {en/api-reference => api-reference}/endpoints/users/get-a-single-user.mdx (58%) rename {en/api-reference => api-reference}/endpoints/users/get-a-users-completed-achievements.mdx (62%) rename {en/api-reference => api-reference}/endpoints/users/get-a-users-leaderboard.mdx (64%) rename {en/api-reference => api-reference}/endpoints/users/get-a-users-points-boosts.mdx (65%) rename {en/api-reference => api-reference}/endpoints/users/get-a-users-points-summary.mdx (67%) rename {en/api-reference => api-reference}/endpoints/users/get-a-users-points.mdx (62%) rename {en/api-reference => api-reference}/endpoints/users/get-a-users-streak.mdx (61%) rename {en/api-reference => api-reference}/endpoints/users/get-a-users-wrapped.mdx (61%) rename {en/api-reference => api-reference}/endpoints/users/get-all-metrics-for-a-user.mdx (62%) rename {en/api-reference => api-reference}/endpoints/users/get-user-preferences.mdx (62%) rename {en/api-reference => api-reference}/endpoints/users/identify-a-user.mdx (58%) rename {en/api-reference => api-reference}/endpoints/users/update-a-user.mdx (58%) rename {en/api-reference => api-reference}/endpoints/users/update-user-preferences.mdx (63%) rename {en/api-reference => api-reference}/idempotency.mdx (82%) rename {en/api-reference => api-reference}/introduction.mdx (70%) rename {en/api-reference => api-reference}/rate-limiting.mdx (92%) rename {en/components => components}/plan-badge.jsx (100%) rename {en/components => components}/rate-limit-badge.jsx (89%) rename {en/experimentation => experimentation}/engagement.mdx (81%) rename {en/experimentation => experimentation}/overview.mdx (83%) rename {en/experimentation => experimentation}/retention.mdx (90%) rename {en/getting-started => getting-started}/introduction.mdx (73%) rename {en/getting-started => getting-started}/quickstart.mdx (76%) rename {en/guides => guides}/gamified-fitness-platform.mdx (96%) rename {en/guides => guides}/gamified-study-platform.mdx (96%) rename {en/guides => guides}/how-to-build-a-leaderboards-feature.mdx (76%) rename {en/guides => guides}/how-to-build-a-streaks-feature.mdx (80%) rename {en/guides => guides}/how-to-build-an-achievements-feature.mdx (75%) rename {en/guides => guides}/how-to-build-an-energy-feature.mdx (73%) rename {en/guides => guides}/how-to-build-an-xp-feature.mdx (72%) rename {en/platform => platform}/achievements.mdx (81%) rename {en/platform => platform}/emails.mdx (89%) rename {en/platform => platform}/events.mdx (80%) rename {en/platform => platform}/leaderboards.mdx (83%) rename {en/platform => platform}/metrics.mdx (91%) rename {en/platform => platform}/overview.mdx (82%) rename {en/platform => platform}/points.mdx (75%) rename {en/platform => platform}/push-notifications.mdx (89%) rename {en/platform => platform}/streaks.mdx (75%) rename {en/platform => platform}/users.mdx (84%) create mode 100644 scripts/validate-link-fragments.mjs rename {en/webhooks => webhooks}/events/achievements/achievement-completed.mdx (70%) rename {en/webhooks => webhooks}/events/leaderboards/leaderboard-changed.mdx (100%) rename {en/webhooks => webhooks}/events/leaderboards/leaderboard-finished.mdx (84%) rename {en/webhooks => webhooks}/events/leaderboards/leaderboard-rank-changed.mdx (100%) rename {en/webhooks => webhooks}/events/leaderboards/leaderboard-started.mdx (100%) rename {en/webhooks => webhooks}/events/points/points-boost-finished.mdx (100%) rename {en/webhooks => webhooks}/events/points/points-boost-started.mdx (100%) rename {en/webhooks => webhooks}/events/points/points-changed.mdx (74%) rename {en/webhooks => webhooks}/events/points/points-level-changed.mdx (53%) rename {en/webhooks => webhooks}/events/streaks/streak-extended.mdx (59%) rename {en/webhooks => webhooks}/events/streaks/streak-freeze-consumed.mdx (100%) rename {en/webhooks => webhooks}/events/streaks/streak-freeze-earned.mdx (100%) rename {en/webhooks => webhooks}/events/streaks/streak-lost.mdx (100%) rename {en/webhooks => webhooks}/events/streaks/streak-started.mdx (100%) rename {en/webhooks => webhooks}/idempotency.mdx (89%) rename {en/webhooks => webhooks}/introduction.mdx (79%) rename {en/webhooks => webhooks}/observability.mdx (94%) rename {en/webhooks => webhooks}/quickstart.mdx (97%) rename {en/webhooks => webhooks}/retries.mdx (100%) rename {en/webhooks => webhooks}/security.mdx (98%) diff --git a/.cursor/rules/localization-workflow.mdc b/.cursor/rules/localization-workflow.mdc index a241acc..6cba1e6 100644 --- a/.cursor/rules/localization-workflow.mdc +++ b/.cursor/rules/localization-workflow.mdc @@ -5,17 +5,19 @@ alwaysApply: true # Localization Workflow -- Treat `en/` as the canonical source locale and keep identical file paths/filenames across all locales. +- Treat repo-root docs paths as the canonical English source and keep identical relative file paths/filenames across all locales. - Keep all locale navigation in `docs.json` under `navigation.languages`; do not reintroduce `navigation.global` or top-level `navbar`. - Keep SEO metatags in English unless explicitly requested otherwise. - Do not translate code snippets, inline code, URLs, route slugs, or API identifiers. -- Shared media lives under repo-root `assets/`. Locale pages are nested under `en/` or `es/` (typically `locale/
/.mdx`), so relative `src` paths must account for that depth (for example `../../assets/...`). `scripts/localize-internal-links.mjs` rewrites markdown `](/...)` and JSX `href="/..."` only: for whichever **`locale` is being processed**, bare paths get that prefix, and paths prefixed with **any other** locale from `i18n.json` are re-prefixed to that same active locale; it does not change media `src` or snippet imports. +- Shared media lives under repo-root `assets/`. Default-language pages are at repo-root section paths (for example `
/.mdx`) and targets are under locale directories (for example `es/
/.mdx`), so relative `src` paths must account for each file depth. `scripts/localize-internal-links.mjs` rewrites markdown `](/...)` and JSX `href="/..."` only: for target locales, bare paths get that locale prefix and paths prefixed with another locale are re-prefixed to the active locale; for source/default processing, locale prefixes are stripped back to root paths. It does not change media `src` or snippet imports. - `scripts/localize-component-imports.mjs` rewrites MDX import paths that reference `/components/*.jsx` so each locale page imports its own locale components (for whichever locale is being processed). - Shared MDX snippets live under repo-root `snippets/` (not per-locale). Import with relative paths from each page; do not add shared MDX snippets to Lingo `i18n.json` buckets unless you intend to translate them. -- Locale-aware React components that can contain text live under `[locale]/components/*.jsx` and are manually maintained per locale (do not add them to Lingo `i18n.json` buckets; do not keep these in shared `snippets/`). +- Locale-aware React components that can contain text live under `components/*.jsx` for default language and `/components/*.jsx` for targets, and are manually maintained per locale (do not add them to Lingo `i18n.json` buckets; do not keep these in shared `snippets/`). - Keep a **single** OpenAPI document at repo-root `openapi.yml` (not under `en/` or `es/`). Endpoint and webhook MDX must use Mintlify’s form `openapi: openapi.yml ` or `openapi: openapi.yml webhook `. Do not translate the `openapi:` line. - English **nav titles** for those pages come from OpenAPI **`summary`** via `scripts/sync-openapi-titles.mjs` (`npm run translate:sync-openapi-titles`). It runs at the start of `translate:generate` and on the translate-on-main workflow. Lingo translates the `title` field for target locales; the script only fills a missing target `title` with English (bootstrap)—it does not overwrite existing translations. -- Mintlify custom heading IDs use markdown `## Title {#slug}` (see Mintlify docs). Slugs are aligned from English via `scripts/sync-heading-anchors.mjs`. Never translate or alter `{#…}`; sync runs after Lingo in PIT/CI. Do not merge heading-count drift between `en/` and target locales without fixing structure first. +- Mintlify custom heading IDs use markdown `## Title {#slug}` (see Mintlify docs). Slugs are aligned from English source root files via `scripts/sync-heading-anchors.mjs`. Never translate or alter `{#…}`; sync runs after Lingo in PIT/CI. Do not merge heading-count drift between source root and target locales without fixing structure first. +- For Mintlify `` components that may be deep-linked, always set explicit `id` values (do not rely on title-derived hashes) and keep those `id`s identical across locales. +- Do not use fragile OpenAPI parameter hash links like `#parameter-*` in docs links; link to the endpoint page and reference parameter names inline in prose/code. - Treat `lingo/brand-voice.md` as the source of truth for **brand voice** (tone and style) only. Do not add structural or tooling rules there (for example Mintlify `{#slug}` handling); those stay in this rule, README, and optionally in the Lingo engine **Instructions** field—not brand voice. - Do not add script-based brand voice sync; sync brand voice on demand via Lingo MCP tools from chat. - Treat `lingo/glossary.csv` as the glossary source of truth; use MCP sync with canonical key `sourceLocale|targetLocale|type|sourceText`. diff --git a/.github/workflows/validate-translations.yml b/.github/workflows/validate-translations.yml index 0957519..f14edb6 100644 --- a/.github/workflows/validate-translations.yml +++ b/.github/workflows/validate-translations.yml @@ -7,7 +7,15 @@ on: - "i18n.json" - "openapi.yml" - "package.json" - - "en/**" + - "account/**" + - "admin-api/**" + - "api-reference/**" + - "components/**" + - "experimentation/**" + - "getting-started/**" + - "guides/**" + - "platform/**" + - "webhooks/**" - "es/**" - ".github/workflows/translate-on-main.yml" - ".github/workflows/validate-translations.yml" @@ -15,7 +23,7 @@ on: jobs: validate: - if: ${{ !contains(fromJson('["translations-setup"]'), github.head_ref) }} + if: ${{ !contains(fromJson('["translations-setup", "restructure-translations"]'), github.head_ref) }} runs-on: ubuntu-latest steps: - name: Checkout @@ -49,6 +57,9 @@ jobs: - name: Ensure component imports are locale-localized run: npm run translate:localize-component-imports:check + - name: Ensure fragile link fragments are not used + run: npm run translate:validate-links + - name: Ensure heading anchors match English slugs run: npm run translate:sync-anchors:check diff --git a/README.md b/README.md index 703969f..7e4e8d0 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,8 @@ Notes: - `scripts/localize-internal-links.mjs`: Rewrites internal absolute **navigation** links in each locale’s MDX for whatever locale is being processed (`--target`, `--all`, or default targets): bare paths get that locale’s prefix (e.g. `/platform/points` → `/es/platform/points` when processing `es/`), and any path already prefixed with **another** locale from `i18n.json` (`en`, `es`, future `fr`, etc.) is re-prefixed to the active locale (so `/en/...` or stale `/es/...` on a `fr/` page become `/fr/...`). Longer codes are matched first (e.g. `en-US` before `en`). It does **not** change relative image or video `src` paths, MDX/JSX **import** paths, or shared **MDX snippets**; those must be correct in source so all locales stay aligned. Prefer `npm run translate:localize-links --` with `--target`, `--all`, or `--all --check`. - `scripts/localize-component-imports.mjs`: Rewrites MDX `import ... from "...//components/...jsx"` paths so they match the active locale being processed (`--target`, `--all`, or default targets). This keeps locale pages importing locale-local components after PIT/CI translation. - **Shared MDX snippets (`snippets/*.mdx`)**: Reusable MDX blocks stay in repo-root `snippets/` (not per-locale). Import them with relative paths from each page (for example `../../snippets/foo.mdx` from `locale/
/.mdx`, or more `../` segments for deeper pages). They are excluded from Lingo buckets in `i18n.json` so they stay English and identical everywhere. -- **Localized React components (`[locale]/components/*.jsx`)**: UI components that can contain locale text are stored per locale (for example `en/components/rate-limit-badge.jsx`, `es/components/rate-limit-badge.jsx`) and manually maintained per locale (not translated by PIT/CI automation). -- **Media paths**: Pages live under `en/
/...` or `es/
/...`, while shared files sit in repo-root `assets/`. Use a path like `../../assets/...` from a typical `locale/
/.mdx` file. +- **Localized React components (`components/*.jsx` for default language, `/components/*.jsx` for targets)**: UI components that can contain locale text are stored per locale (for example `components/rate-limit-badge.jsx`, `es/components/rate-limit-badge.jsx`) and manually maintained per locale (not translated by PIT/CI automation). +- **Media paths**: Default-language pages live at repo root paths like `
/.mdx` and target locales live under `/
/.mdx`; shared files sit in repo-root `assets/`. Keep relative media paths correct for each file depth. - **`openapi.yml`**: One OpenAPI 3.1 spec at the **repository root** (alongside `docs.json`). API and webhook pages reference it explicitly in frontmatter, for example `openapi: openapi.yml get /users/{id}` or `openapi: openapi.yml webhook points.changed`. Do not duplicate the YAML under locale folders; Lingo must not alter `openapi:` lines. - **`scripts/sync-openapi-titles.mjs`**: Copies each operation/webhook **`summary`** from `openapi.yml` into the English page’s **`title:`** frontmatter (Mintlify’s default when `title` is omitted). Target locales get an English `title` only if missing (bootstrap); run **`npm run translate:generate`** so Lingo translates those titles. Runs automatically at the start of `translate:generate` and in translate-on-main before Lingo. - `scripts/sync-heading-anchors.mjs`: Writes Mintlify [custom heading IDs](https://www.mintlify.com/docs/create/text#custom-heading-ids) as **`## Title {#slug}`** markdown. Slugs match Mintlify’s auto rules from the **English** title so hashes like `#pro-plan` stay stable across locales. Run `npm run translate:sync-anchors` after bulk heading edits; translation pipelines run it automatically (see **Heading anchors and Lingo** below). The script can also migrate one-line **`

`** left from older tooling back to `{#slug}` syntax. @@ -71,7 +71,7 @@ Notes: #### Heading anchors and Lingo -- **Canonical behavior** is enforced by **`scripts/sync-heading-anchors.mjs`** (runs at the end of `translate:generate` and on the translate-on-main workflow). It rewrites `en/` from current English titles and reapplies the **same** `{#slug}` suffixes to translated headings in document order. Hash links stay aligned even if Lingo alters titles or fragments. +- **Canonical behavior** is enforced by **`scripts/sync-heading-anchors.mjs`** (runs at the end of `translate:generate` and on the translate-on-main workflow). It rewrites default-language root files from current English titles and reapplies the **same** `{#slug}` suffixes to translated headings in document order. Hash links stay aligned even if Lingo alters titles or fragments. - **Lingo:** Keep **`lingo/brand-voice.md`** to tone and style only. For structural rules (for example Mintlify `{#slug}` on headings), optionally add a separate line in your Lingo engine **Instructions** field (not brand voice), such as: “Preserve `{#…}` heading fragments exactly—do not translate or alter the slug.” That only reduces churn; **the script + `translate:sync-anchors:check` are authoritative.** - **Strict MDX parsers** (for example unconfigured `mdx-js`) can treat `{` as JSX and error on `{#slug}`; **Mintlify’s `mint dev` / deploy pipeline** is expected to handle this documented syntax. If you see Acorn errors locally, update the Mintlify CLI (`npm i -D mint@latest` or global `mintlify`) or check [Mintlify support](https://mintlify.com/docs); do not switch to raw HTML headings unless Mintlify asks you to. - **Do not** fold this into `localize-internal-links.mjs`: that script only handles absolute path `href`s; heading anchors are a separate structural pass and must run **after** Lingo (and after link localization) so all three stay consistent. @@ -89,12 +89,12 @@ Use `npm run