DOMAIN:CONTENT — CONTENT-DEVELOPER HANDOFF¶
OWNER: jouke, dinand, benjamin, joost
ALSO_USED_BY: floris, floor (Frontend), faye, sytske (Project Management)
UPDATED: 2026-03-24
HANDOFF:COPY_TO_CODEBASE¶
Methods (ranked by preference)¶
| Method | When | Pros | Cons |
|---|---|---|---|
| i18n JSON files in repo | Default for all projects | Version controlled, type-safe, CI-validated | Needs developer to merge |
| TMS → repo sync (Crowdin/Lokalise) | Multi-language projects | Automated, translator-friendly | Setup cost, monthly fee |
| Headless CMS (Strapi (FR), Payload (self-hosted)) | Marketing/content-heavy pages | Non-dev can edit, preview | Runtime dependency, cost |
| Hardcoded strings | Never | — | Untranslatable, unreviewable |
RULE: default to i18n JSON files — every project starts here
RULE: graduate to TMS when >2 target languages
RULE: use headless CMS only for marketing pages, landing pages, blog
RULE: NEVER hardcode user-visible strings — even for MVP
i18n JSON File Workflow¶
WORKFLOW:
1. Content lead creates/updates copy in shared document (Notion, Google Doc)
2. Content lead exports to i18n JSON format (or writes directly)
3. Content lead opens PR with JSON changes to messages/{locale}.json
4. Developer reviews for:
- Valid JSON syntax
- Correct key naming (follows conventions)
- No broken ICU placeholders
- String length within UI bounds
5. Content lead and developer co-approve
6. PR merges → deploys
RULE: content PRs are separate from code PRs — review independently
RULE: PR description includes screenshot of affected UI
RULE: CI validates JSON syntax + key completeness across all locales
HANDOFF:DYNAMIC_CONTENT¶
CMS Content Types¶
IF: content changes more than once per sprint
THEN: use headless CMS, not JSON files
IF: content is static (UI labels, errors, forms)
THEN: i18n JSON files, not CMS
IF: content is user-generated
THEN: database, not CMS or JSON
CMS Architecture¶
CONTENT FLOW:
CMS (Strapi (FR) / Payload (self-hosted) preferred. Contentful (US) secondary only.) → API → Next.js ISR/SSG → CDN
RULE: use ISR (Incremental Static Regeneration) — not client-side fetch
RULE: cache CMS responses — CDN for public, Redis for personalized
RULE: fallback content defined in code for CMS outage resilience
RULE: content model mirrors i18n key structure where possible
What Goes Where¶
| Content Type | Storage | Owner |
|---|---|---|
| UI labels, buttons, forms | i18n JSON | Content lead |
| Error messages | i18n JSON | Content lead |
| Legal text (ToS, privacy) | CMS or static page | Compliance (julian) |
| Marketing copy | CMS | Client / Content lead |
| Blog posts | CMS or MDX | Client |
| In-app help / tooltips | i18n JSON | Content lead |
| Email templates | CMS or template files | Content lead |
| Push notification text | i18n JSON or CMS | Content lead |
HANDOFF:COPY_REVIEW_IN_PRS¶
PR Review Checklist for Copy Changes¶
COPY PR CHECKLIST:
□ JSON is valid (CI check)
□ All locales have the new key (CI check)
□ Key follows naming convention ({feature}.{component}.{element})
□ Placeholders use ICU syntax, not string concatenation
□ Pluralization uses CLDR forms (not just one/other for non-English)
□ Copy matches approved text from content document
□ No truncation risk (flag strings >50 chars for mobile buttons)
□ Screenshot attached showing the copy in context
□ Accessibility: no color-only meaning, descriptive link text
□ No hardcoded strings added in same PR (code review)
Automation¶
TOOL: GitHub Action — i18n completeness check
# .github/workflows/i18n-check.yml
name: i18n Check
on: pull_request
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check i18n completeness
run: |
npx next-intl-lint
# OR custom: compare keys across locale files
node scripts/check-i18n-completeness.js
TOOL: Dire CLI — translation completeness
RUN: npx dire check --source messages/en.json --targets messages/nl.json messages/de.json
RULE: CI MUST fail if any locale is missing keys present in default locale
RULE: CI SHOULD warn if any string exceeds 150% of source length (UI overflow risk)
HANDOFF:CONTENT_STATUS_TRACKING¶
Status Labels¶
| Status | Meaning | Who Sets It |
|---|---|---|
draft |
Work in progress | Content lead |
review |
Ready for peer review | Content lead |
approved |
Reviewed and approved | Reviewer (peer or client) |
implemented |
Merged into codebase | Developer |
live |
Deployed to production | CI/CD |
RULE: developers only implement approved copy — never draft
RULE: content changes in draft or review status stay in shared doc, not in code
RULE: every content task in the project tracker has a status label
Handoff Tooling¶
| Tool | Use Case | Integration |
|---|---|---|
| Figma + Frontitude | Design-content sync | Plugin exports to JSON |
| Notion | Content collaboration | Manual export to JSON |
| Google Docs | Client-facing drafts | Manual export to JSON |
| Crowdin | Translation management | GitHub sync via CLI/webhook |
| Lokalise | Translation management | GitHub sync via CLI/webhook |
RULE: prefer tools with Git integration — reduces manual copy-paste
RULE: single source of truth for copy = the i18n JSON file in the repo (once implemented)
RULE: shared docs (Notion, Google Docs) are staging — code is production
ANTI_PATTERN: copy living in Figma that never makes it to code
FIX: content lead opens PR with copy — it's their deliverable, not the developer's
ANTI_PATTERN: developer writing copy because content lead hasn't delivered
FIX: block UI tasks on copy tasks in the DAG — no implementation without approved copy
ANTI_PATTERN: copy changes going directly to production without PR review
FIX: all copy changes go through PR with review checklist — no exceptions