docs: rewrite the documentation for v6

Rewrite every guide for the redesigned app, add guides for new features (documents, editor modes, check, cover letters, assistant, applications, self-hosting upgrade and environment reference), remove v5-only pages with redirects, and replace every screenshot.
This commit is contained in:
Amruth Pillai
2026-09-30 05:07:06 +02:00
parent 883045b14c
commit d46b4b5815
345 changed files with 8233 additions and 6387 deletions
+100 -96
View File
@@ -1,143 +1,147 @@
---
title: "Project architecture"
description: "How the Reactive Resume monorepo is laid out, the runtime boundaries between the web and server apps, and the package ownership model."
description: "How the Reactive Resume monorepo fits together: the web and server apps, shared packages, runtime boundaries, and where new code belongs."
---
Reactive Resume is a pnpm/Turborepo monorepo. Docker runs one Node.js process. Vercel deploys two services from one project: `frontend` serves static assets through its CDN, and `backend` runs the same Hono application in a Node.js Function. Both targets share the web app, API, authentication, renderers, and database schema.
Reactive Resume is a TypeScript monorepo managed with pnpm workspaces and Turborepo. This page explains how the pieces fit together, so you can find the code behind a feature and know where a change belongs. To get a working checkout first, see [Development setup](/contributing/development).
Internal packages are source-consumed through their `package.json` export maps. Import package subpaths, not another workspace's private `src` files.
## The big picture
---
There are two apps and a set of shared packages:
## Runtime shape
- **`apps/web`** is a client-rendered React 19 single-page app built with Vite, TanStack Router, TanStack Query, Tailwind CSS, and Lingui for translations.
- **`apps/server`** is a Hono application on Node.js. It serves the API, authentication, the MCP server, uploads, OpenAPI, and the built web app.
- **`packages/*`** hold everything the apps share: API business logic, authentication, database access, schemas, PDF and DOCX rendering, and UI primitives.
The browser talks to the server through [oRPC](https://orpc.unnoq.com/) at `/api/rpc`. [Better Auth](https://www.better-auth.com/) handles sign-in, sessions, passkeys, two-factor authentication, API keys, and the OAuth provider used by MCP clients. [Drizzle](https://orm.drizzle.team/) talks to PostgreSQL.
```mermaid
flowchart TD
Browser["Browser"] --> WebRoutes["apps/web routes"]
WebRoutes --> ORPCClient["oRPC client"]
ORPCClient --> RPC["/api/rpc"]
Browser["Browser: apps/web SPA"] -->|"oRPC /api/rpc"| Server
Browser -->|"Forme (WebAssembly)"| BrowserPDF["PDF in the browser"]
MCPClient["MCP client"] -->|"/mcp"| Server
subgraph NodeProcess["Node process"]
Server["apps/server Hono adapter"]
API["packages/api feature routers"]
Auth["packages/auth"]
subgraph Server["apps/server (Hono)"]
RPC["RPC and OpenAPI handlers"]
AuthRoutes["/api/auth"]
MCP["packages/mcp"]
PDFServer["@reactive-resume/pdf/server"]
end
Server --> RPC
RPC --> API
Server --> Auth
Server --> MCP
API --> PDFServer
API --> DB["packages/db"]
API --> Storage["Local disk, S3, or private Vercel Blob"]
DB --> Postgres["PostgreSQL"]
RPC --> API["packages/api feature routers"]
MCP --> API
AuthRoutes --> Auth["packages/auth (Better Auth)"]
API --> DB["packages/db (Drizzle)"] --> Postgres[("PostgreSQL")]
API --> Storage[("Local disk, S3, or Vercel Blob")]
API --> Redis[("Redis (optional)")]
API --> ServerPDF["@reactive-resume/pdf/server"]
```
`apps/web` owns the React SPA with TanStack Router and Vite. `apps/server` owns the Hono application and mounts RPC, auth, OpenAPI, MCP, static uploads, schema JSON, and the built web app.
### How it runs
---
- **Development.** `pnpm dev` starts Vite on `PORT` (default `3000`), the Hono server on `SERVER_PORT` (default `3001`), and the email template preview on port `3002`. Vite proxies `/api`, `/mcp`, `/uploads`, `/.well-known`, and `/schema.json` to Hono, so you always open `http://localhost:3000`.
- **Docker.** The production image runs one Node.js process on port `3000`. Hono mounts the API, auth, MCP, and static routes, then serves the built web app.
- **Vercel.** One project deploys two services: `frontend` serves the static web build from the CDN, and `backend` runs the same Hono app in a Node.js Function. See [Deployment checks](/contributing/deployment-checks).
There is no request-time React server rendering. The web build prerenders the marketing homepage for each locale, and `apps/server/src/static/web.ts` serves HTML shells with OpenGraph, canonical, and JSON-LD metadata injected.
### What happens at startup
The server checks the environment, applies database migrations, and verifies the migrated schema before it initializes auth and accepts traffic. With `STRICT_SCHEMA_CHECK=true`, schema drift stops the server; otherwise it logs the drift and continues.
## Workspace map
| Workspace | Ownership |
| Workspace | What it owns |
| --- | --- |
| `apps/web` | TanStack Router routes, web features, browser PDF.js preview/viewer code, PWA setup, oRPC browser client |
| `apps/server` | Hono route composition, production HTTP adapters, MCP transport, OpenAPI/well-known handlers, static file serving, startup checks |
| `packages/api` | oRPC procedures and feature-owned business behavior under `src/features/*` |
| `packages/auth` | Better Auth config, auth helpers, and exported auth types |
| `packages/db` | Drizzle client and schema; root `migrations/` stores generated migrations |
| `packages/env` | Server environment validation and root `.env` loading |
| `packages/schema` | Zod schemas and typed resume/page/template models |
| `packages/resume` | Pure resume-domain helpers, including JSON Patch behavior and network icon mapping |
| `packages/pdf` | Resume document, template primitives, Forme conversion, templates, font registration, and browser/server generation adapters |
| `packages/docx` | DOCX export generation |
| `packages/mcp` | MCP tools, prompts, resources, server card, and tool metadata |
| `packages/ui` | Shared Base UI/shadcn-style primitives and hooks |
| `packages/ai` | AI provider types, prompts, resume parsing/sanitization helpers, and model-facing tool contracts |
| `apps/web` | Routes (`src/routes`, file-based), user-facing features (`src/features`), the PDF.js preview and public viewer, the PWA, and the oRPC browser client |
| `apps/server` | Hono route composition (`src/http`), RPC and OpenAPI adapters, the MCP transport, static and upload handlers, SEO for HTML shells, and startup checks |
| `packages/api` | oRPC procedures and business logic, one folder per feature under `src/features/*` |
| `packages/auth` | Better Auth configuration, helpers, and types |
| `packages/db` | Drizzle client and schema; generated migrations live in the root `migrations/` folder |
| `packages/env` | Server environment validation; loads the root `.env` |
| `packages/schema` | Zod schemas for resumes, cover letters, applications, pages, and templates |
| `packages/resume` | Pure resume logic with no database, HTTP, or DOM dependencies, such as JSON Patch helpers and social network icons |
| `packages/pdf` | Resume templates (which also lay out cover letters), the Forme adapter, font resolution, custom-style support, and browser and server PDF adapters |
| `packages/docx` | DOCX export |
| `packages/mcp` | MCP tools, prompts, resources, and the server card |
| `packages/ai` | AI provider types, prompts, and model-facing helpers |
| `packages/import` | Resume importers |
| `packages/ui` | Shared UI primitives and hooks in the Base UI / shadcn style |
| `packages/fonts` | Font metadata |
| `packages/email` | Email transport and templates |
| `packages/utils` | Narrow cross-cutting utilities with explicit export subpaths |
| `packages/config` | Shared development configuration |
| `tooling` | Development-only scripts and repo tooling |
| `packages/utils` | Narrow cross-cutting helpers behind explicit export subpaths |
| `packages/config` | Shared TypeScript and tooling configuration |
| `packages/dsh-plugin` | A separately built and published plugin that connects a DeepSeek Harness session to Reactive Resume over MCP |
| `tooling` | Development-only scripts: PDF translation catalog, semantic CSS reference, icon builds, database reset, deployment smoke test |
---
Internal packages are consumed as source through the `exports` map in each `package.json`, which points at `src` files. Don't expect a `dist` folder unless a package builds one explicitly.
## Where new code goes
| You are changing | Put it here |
| --- | --- |
| A page, loader, or user workflow | A route in `apps/web/src/routes` plus the feature folder in `apps/web/src/features/<area>` |
| An authenticated API procedure or business rule | `packages/api/src/features/<area>` |
| Pure resume data behavior | `packages/resume` |
| The shape of resume data | `packages/schema` first, then API DTOs, importers, PDF templates, and web forms that use it |
| A PDF template or rendering behavior | `packages/pdf` |
| PDF.js canvas or viewer UI | `apps/web/src/features/resume` (never `packages/pdf`) |
| DOCX export | `packages/docx` |
| An MCP tool, prompt, or resource | `packages/mcp` |
| A generic UI primitive or hook | `packages/ui`; workflow-specific UI stays in its web feature |
| A database column or table | `packages/db/src/schema/*`, then `pnpm db:generate` |
| A server environment variable | `packages/env/src/server.ts`, `.env.example`, and `globalEnv` in `turbo.json` |
| A dev-only script | `tooling/` |
A new template touches several places: `packages/schema/src/templates.ts`, `packages/pdf/src/templates/index.ts`, the template source under `packages/pdf/src/templates/<name>/`, and preview images under `apps/web/public/templates/{jpg,pdf}`.
Add a helper to `packages/utils` only when no domain package is a better owner. JSON Patch behavior belongs in `@reactive-resume/resume/patch`, and DOCX builders belong in `@reactive-resume/docx`.
## Boundary rules
- Use `@reactive-resume/*` package exports for cross-workspace imports.
- Do not import another workspace through `apps/**`, `packages/**`, `@reactive-resume/*/src/**`, or a TypeScript path alias to another workspace's `src`.
- Keep browser-only code in web features or explicit browser subpaths.
- Keep server-only code in server packages or explicit server subpaths.
- Keep environment-neutral domain packages free of DB, HTTP, DOM, and app imports.
- Add public package exports deliberately. Wildcard exports are reserved for leaf-style public surfaces such as UI components/hooks and schema resume files.
Turborepo enforces these rules with `pnpm exec turbo boundaries`:
The checks are executable:
- Import other workspaces by package name and export subpath, such as `@reactive-resume/pdf/browser`. Never reach into another workspace's `src` through a relative path, `@reactive-resume/*/src/*`, or a TypeScript path alias.
- Each workspace's `turbo.json` declares tags. `app:web` and `app:server` mark the apps. `runtime:server` marks server-only packages (API, auth, database, environment, email, MCP), `runtime:browser` marks browser-only UI, and `runtime:universal` marks environment-neutral domain packages.
- Runtime-specific code sits behind explicit subpaths such as `@reactive-resume/pdf/browser`, `@reactive-resume/pdf/server`, and `@reactive-resume/env/server`. Keep root exports environment-neutral unless the whole package is server-only.
- Wildcard exports are reserved for leaf libraries with a file-like surface: `@reactive-resume/ui/components/*`, `@reactive-resume/ui/hooks/*`, and the schema model files. Everything else uses explicit exports.
```bash
pnpm exec turbo boundaries
pnpm exec biome check biome.json turbo.json tooling/grit/no-cross-workspace-src-imports.grit apps/web/tsconfig.json apps/*/turbo.json packages/*/turbo.json
```
After you change a shared contract, an export, or an import path, run `pnpm exec turbo boundaries` and check the affected consumers.
---
## The web app
## Feature placement
`apps/web/src/routes` stays route-owned: route files handle the URL, loaders, redirects, and metadata. Implementation lives in `apps/web/src/features`, grouped by product area: `documents`, `resume` (editor, preview, export, sharing, custom styles), `letters`, `applications`, `assistant`, `ats-checker`, `settings`, `command-palette`, `auth`, `homepage`, `theme`, `locale`, and `user`.
When adding code, choose the owner by behavior:
`apps/web/src/router.tsx` creates the router context with `queryClient`, `orpc`, `theme`, `locale`, `session`, and `flags`. Read these from route context instead of fetching them again. Never edit `routeTree.gen.ts` by hand; Vite regenerates it when you add or rename a route.
| Change | Put it here |
| --- | --- |
| Route, loader, route-level server handler, or web workflow | `apps/web/src/routes` plus `apps/web/src/features/<domain>` |
| API procedure or authenticated business behavior | `packages/api/src/features/<domain>` |
| Pure resume data logic | `packages/resume` |
| Resume schema or template list shape | `packages/schema` |
| PDF template/rendering behavior | `packages/pdf` |
| PDF.js canvas/viewer UI | `apps/web/src/features/resume` |
| DOCX export behavior | `packages/docx` |
| MCP tool/prompt/resource behavior | `packages/mcp` |
| Shared UI primitive/hook | `packages/ui` |
| Cross-cutting helper | Prefer a domain package first; otherwise add an explicit `packages/utils` export |
The oRPC client in `apps/web/src/libs/orpc/client.ts` calls `/api/rpc` with credentials. On Vercel, `apps/web/src/libs/orpc/fetch.ts` stages large request bodies through Blob storage.
---
When you add a public marketing route, also update its server fallback and SEO handling in `apps/server/src/static/web.ts`. Vite's dev fallback can hide a production 404.
## Web layout
## The API
`apps/web/src/routes` stays route-owned. Route files handle URL shape, loaders, redirects, metadata, and SSR flags.
`packages/api/src/routers/index.ts` combines the feature routers (`resume`, `coverLetters`, `documents`, `applications`, `agent`, `ai`, `aiProviders`, `auth`, `storage`, `statistics`, `flags`) into the contract served at `/api/rpc`. Each feature folder owns its procedures, services, helpers, and tests.
Domain UI and browser-heavy implementation code lives under `apps/web/src/features`. Current feature areas include resume preview/export/public pages, command palette, auth, settings, theme, locale, and user menu behavior.
Use `protectedProcedure` from `packages/api/src/context.ts` for authenticated procedures, and check resource ownership inside the feature logic. API keys, bearer tokens, and cookies all resolve through the same shared auth path; don't add a separate one.
Generic app-local components remain in `apps/web/src/components`; shared reusable primitives live in `packages/ui`.
Keep helpers inside the feature that uses them. Don't reintroduce technical-layer folders such as `services/` or `helpers/` at the package root.
Dialog runtime state is centralized in `apps/web/src/dialogs/store.ts`, while dialog schemas and renderers are registered by domain under `apps/web/src/dialogs/{auth,api-key,resume}`.
## PDF rendering
---
`packages/pdf` renders every PDF. Templates are React components built from the package's primitives. The code in `src/forme` renders them with a small React reconciler and converts the result into a [Forme](https://www.formepdf.com/) document. The Forme engine, compiled to WebAssembly, lays out and draws the pages. No Chromium, Browserless, or print service is involved.
## API layout
- `@reactive-resume/pdf/browser` creates PDFs in the browser. The editor's download and preview use it.
- `@reactive-resume/pdf/server` creates PDFs on the server, for the public resume download and API exports.
- `packages/pdf/src/templates/shared/filtering.ts` holds the section filtering shared by all templates. Template-specific visual exceptions stay in that template's folder.
- `packages/pdf/src/hooks/use-register-fonts.ts` resolves font families, weights, and fallback stacks for other scripts.
`packages/api/src/routers/index.ts` exports the top-level oRPC contract. Feature modules under `packages/api/src/features/*` own their procedure modules, services, helpers, tests, and public package exports.
Default section titles in the PDF come from a generated catalog, `packages/pdf/src/section-title-catalog.json`, built from the web app's translations. See [Contributing translations](/contributing/translations#updating-catalogs-in-a-checkout).
Avoid reintroducing technical-layer folders such as `services/` or `helpers/` at the package root. If a helper is used by one feature, keep it in that feature. If it becomes shared, name the shared capability explicitly and export it intentionally.
## MCP
---
`packages/mcp` implements the MCP server with canonical, unprefixed tool names such as `list_resumes`, `read_resume`, `apply_resume_patch`, `list_cover_letters`, and `list_applications`. The server process imports it from `@reactive-resume/mcp` and injects an in-process oRPC router client, so MCP tools run the same business logic as the web app. MCP must never import code from `apps/web`. For the user-facing side, see [Using the MCP server](/guides/using-the-mcp-server).
## PDF and export boundaries
## Related pages
`packages/pdf` owns PDF generation. Templates are written with React primitives; `src/forme` renders them with a small React renderer and converts the result into a [Forme](https://www.formepdf.com/) document, which the Forme engine (WebAssembly) lays out and draws:
- `@reactive-resume/pdf/browser` creates browser PDF blobs.
- `@reactive-resume/pdf/server` creates server PDF files.
- Template code stays under `packages/pdf/src/templates`.
Localized section-title resolution stays in the caller because it depends on web/server locale context. PDF.js preview and viewer code stays in `apps/web/src/features/resume`, not in `packages/pdf`.
DOCX export generation lives in `packages/docx`.
---
## MCP boundary
MCP implementation lives in `packages/mcp`. It exposes canonical unprefixed tool names such as `list_resumes`, `read_resume`, and `apply_resume_patch`.
The server process imports MCP from `@reactive-resume/mcp` and injects the in-process oRPC router client. It must not import MCP code from `apps/web/src`.
- [Development setup](/contributing/development): run the app locally and learn the everyday commands.
- [Deployment checks](/contributing/deployment-checks): how CI verifies the Vercel build and how to smoke-test an installation.
- [Contributing translations](/contributing/translations): Crowdin, the glossary, and catalog commands.
+38 -19
View File
@@ -1,51 +1,70 @@
---
title: "Deployment checks"
description: "How CI verifies the Vercel build artifact, and how to run the deployment smoke test against Vercel or Docker."
description: "How CI verifies the Vercel build artifact on every pull request, and how to run the deployment smoke test against a Vercel or Docker installation."
---
The **Vercel compatibility** workflow (`.github/workflows/vercel.yml`) has two jobs.
Reactive Resume ships as a Docker image and as a Vercel project. The **Vercel compatibility** workflow (`.github/workflows/vercel.yml`) catches problems that only show up in a deployed build. It has two jobs: an offline artifact build that runs on every pull request, and a live smoke test you start by hand. You can run the same smoke test against your own installation.
## Artifact build
Runs on every pull request and every push to `main`. It needs no Vercel account and no secrets, so fork pull requests run it safely.
The `artifact` job runs on every pull request, every push to `main`, and every manual run. It needs no Vercel account and no secrets, so pull requests from forks run it safely.
The job:
1. Starts an isolated PostgreSQL service.
2. Writes a local `.vercel/project.json` with the `services` framework and runs `vercel build --prod` offline, with placeholder Blob credentials. This also applies migrations to the isolated database.
2. Writes a local `.vercel/project.json` with the `services` framework, then runs `vercel build --prod` offline with placeholder Blob credentials. The build applies migrations to the isolated database.
3. Checks the `backend` service Function:
- runtime is `nodejs24.x`, `maxDuration` is `300`, and the handler is `apps/server/vercel.mjs`;
- a copy of the Function outside the checkout loads with `--no-experimental-require-module`, which matches the Vercel runtime, so a dependency the build left out fails the job;
- the copy rejects an unauthenticated staging request and serves the prerendered homepage.
- its runtime is `nodejs24.x`, `maxDuration` is `300`, and its handler is `apps/server/vercel.mjs`;
- a copy of the Function outside the checkout loads with `--no-experimental-require-module`, like the Vercel runtime does, so a dependency the build left out fails the job;
- the copy rejects an unauthenticated upload-staging request with `401` and serves the prerendered homepage with its JSON-LD metadata.
If a new server dependency fails the loading check, add it and its own dependencies to `bundledInteropPackages` in `apps/server/tsdown.config.ts`. CommonJS dependencies need this: Vercel's service builder loads them through pnpm links that it leaves out of the Function.
### When the loading check fails
If a new server dependency breaks the loading check, add it and its own dependencies to `bundledInteropPackages` in `apps/server/tsdown.config.ts`. CommonJS dependencies need this, because Vercel's service builder leaves out the pnpm links they load through.
## Live smoke test
Runs only when started manually (`workflow_dispatch`). It uses the `vercel-smoke` GitHub environment and runs `tooling/deployment/smoke.mjs` against `VERCEL_SMOKE_URL`.
The `live-smoke` job runs only when you start the workflow manually (`workflow_dispatch`). It uses the `vercel-smoke` GitHub environment and runs `tooling/deployment/smoke.mjs` against the installation at `VERCEL_SMOKE_URL`.
<Warning>
Point the smoke test only at a dedicated test installation. It creates an account, a public resume, and files, then deletes them.
Point the smoke test only at a dedicated test installation. It signs up a new account, publishes a resume, and uploads files, then deletes the account and everything in it.
</Warning>
The script checks health, public pages, signup, resume CRUD, public PDF rendering, a 10 MiB upload and download, and one-time use of staged requests.
The script checks, in order:
To also check a 25 MiB agent attachment, configure a deterministic OpenAI-compatible test provider that serves the model `smoke-model`:
1. `/api/health` reports `healthy`, and `/`, `/auth/login`, `/robots.txt`, `/sitemap.xml`, and `/.well-known/oauth-protected-resource` respond. A missing asset returns `404`.
2. An email sign-up creates a session.
3. A resume created from sample data can be read back and made public, and its public page and server-rendered public PDF load.
4. A 10 MiB file uploads, downloads intact, and is deleted. On Vercel the upload goes through a staged request, and the script checks that a staged request can't be replayed.
5. Optionally, a 25 MiB assistant attachment uploads and is deleted (see below).
6. The account is deleted, even if an earlier check failed.
The installation must allow sign-ups and email sign-in, so `FLAG_DISABLE_SIGNUPS` and `FLAG_DISABLE_EMAIL_AUTH` must not be `true`.
### Attachment check
To include the 25 MiB attachment check, configure an OpenAI-compatible test provider that answers a connection test for the model `smoke-model`. No paid AI model is needed; a deterministic stub works.
| Name | Kind | Value |
| --- | --- | --- |
| `VERCEL_SMOKE_URL` | Variable | Test installation origin |
| `VERCEL_SMOKE_AI_BASE_URL` | Variable | Test provider base URL |
| `VERCEL_SMOKE_AI_API_KEY` | Secret | Test provider API key |
| `VERCEL_SMOKE_URL` | Variable | Origin of the test installation |
| `VERCEL_SMOKE_AI_BASE_URL` | Variable | Base URL of the test provider |
| `VERCEL_SMOKE_AI_API_KEY` | Secret | API key for the test provider |
No paid AI model is needed.
The installation needs `ENCRYPTION_SECRET` set to save the provider. A provider on a private or `http://` address also needs `FLAG_ALLOW_UNSAFE_AI_BASE_URL=true`, which is only safe on an isolated test installation.
## Run the smoke test locally
## Run the smoke test yourself
Against a local Docker installation:
The script needs only Node.js 24 and a checkout. Set `SMOKE_URL` to the installation's origin:
```bash
SMOKE_URL=http://localhost:3000 node tooling/deployment/smoke.mjs
```
Add `SMOKE_AI_BASE_URL` and `SMOKE_AI_API_KEY` to include the attachment check.
Add `SMOKE_AI_BASE_URL` and `SMOKE_AI_API_KEY` to include the attachment check. The script detects the platform on its own: on Docker, the staging endpoint returns `404` and uploads go directly to the server.
## Related pages
- [Development setup](/contributing/development): run the app and the test suites locally.
- [Deploying to Vercel](/self-hosting/vercel): set up your own Vercel installation.
- [Self-hosting with Docker](/self-hosting/docker): run the production image.
+217 -244
View File
@@ -1,324 +1,297 @@
---
title: "Development setup"
description: "Set up a local development environment for Reactive Resume with pnpm, Docker services, environment variables, and the web and server apps."
description: "Run Reactive Resume locally with Node.js 24, pnpm, and Docker, then use the everyday commands for the database, tests, linting, and pull requests."
---
<Info>
**Prerequisites**: - [Node.js](https://nodejs.org/) v24 - [pnpm](https://pnpm.io/) v11.21.0 -
[Docker](https://docs.docker.com/get-docker/) and Docker Compose - [Git](https://git-scm.com/)
</Info>
This guide takes you from a fresh clone to a running local copy of Reactive Resume, then covers the commands you'll use while working on it. For how the code is organized, read [Project architecture](/contributing/architecture).
These steps set up Reactive Resume for local development, whether you're contributing to the project or customizing it for yourself.
## Before you start
---
You need:
## Setting up your development environment
- **[Node.js](https://nodejs.org/) 24.** The version is pinned in `.nvmrc` and the root `engines` field, so `nvm use` or `fnm use` picks it up.
- **[pnpm](https://pnpm.io/installation) 12.** The root `packageManager` field pins the exact version (currently `pnpm@12.8.1`), and pnpm switches to it automatically when you run it inside the repository.
- **[Docker](https://docs.docker.com/get-docker/) with Docker Compose** for PostgreSQL, Redis, and S3-compatible storage. Start the Docker daemon first.
- **[Git](https://git-scm.com/).**
## Set up your checkout
Run every command from the repository root unless a step says otherwise.
<Steps>
<Step title="Clone the Repository">
```bash
git clone https://github.com/reactive-resume/reactive-resume.git reactive-resume
cd reactive-resume
```
</Step>
<Step title="Install Dependencies">
Install [pnpm](https://pnpm.io/installation) directly, then install the project dependencies:
<Step title="Clone the repository">
```bash
git clone https://github.com/reactive-resume/reactive-resume.git
cd reactive-resume
```
</Step>
```bash
pnpm install
```
</Step>
<Step title="Start Infrastructure Services">
If you want to run the app directly on your machine with `pnpm dev`, start only the infrastructure services:
```bash
docker compose -f compose.dev.yml up -d postgres redis seaweedfs seaweedfs_create_bucket
```
This starts the following infrastructure services:
- **PostgreSQL** — Database (port 5432)
- **Redis** — AI Agent workspace streams/state (port 6379)
- **SeaweedFS** — S3-compatible storage (port 8333)
<Step title="Install dependencies">
```bash
pnpm install --frozen-lockfile
```
<Info>
**From v5.1.0 onwards** — PDF generation now runs entirely in the browser with the Forme PDF engine (WebAssembly), so no Browserless or Chromium container is required for development.
</Info>
<Tip>
`compose.dev.yml` can also run the app in a development container with `docker compose -f compose.dev.yml up -d`.
Use the service-filtered command above when you want local editor tooling and `pnpm dev` on the host.
</Tip>
<Tip>
Wait for all services to be healthy before proceeding. Check with `docker compose -f compose.dev.yml ps`.
</Tip>
</Step>
<Step title="Configure Environment Variables">
Copy `.env.example` to `.env.local` in the project root:
The install also sets up the [Lefthook](https://github.com/evilmartians/lefthook) Git hooks described in [Commits and pull requests](#commits-and-pull-requests).
</Step>
```bash
cp .env.example .env.local
```
<Step title="Start the infrastructure services">
```bash
docker compose -f compose.dev.yml up -d postgres redis seaweedfs seaweedfs_create_bucket
docker compose -f compose.dev.yml ps
```
Then edit `.env.local` as needed. For local development on the host, set at minimum:
This starts:
```bash
# Application
PORT=3000
SERVER_PORT=3001
APP_URL=http://localhost:3000
| Service | Purpose | Port |
| --- | --- | --- |
| `postgres` | The database | `5432` |
| `redis` | Optional: shared rate limits, resumable assistant replies, live resume events, and view de-duplication | `6379` |
| `seaweedfs` | S3-compatible storage for uploads | `8333` |
| `seaweedfs_create_bucket` | Creates the `reactive-resume` bucket, then exits | — |
# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
Wait until `ps` shows the services as healthy. PDF generation runs on the Forme engine in WebAssembly, so you don't need a Chromium or Browserless container.
</Step>
# Authentication
AUTH_SECRET=development-secret-change-in-production
<Step title="Create your environment file">
Copy the template only if you don't have a `.env.local` yet:
# Storage (SeaweedFS)
S3_ACCESS_KEY_ID=seaweedfs
S3_SECRET_ACCESS_KEY=seaweedfs
S3_ENDPOINT=http://localhost:8333
S3_BUCKET=reactive-resume
S3_FORCE_PATH_STYLE=true
```bash
test -e .env.local || cp .env.example .env.local
```
# Email (Mailpit for local development)
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_FROM="Reactive Resume <noreply@rxresu.me>"
The template uses container hostnames. Because the app runs on your machine, change these values in `.env.local` to `localhost`:
# AI Agent workspace and saved AI providers
REDIS_URL=redis://localhost:6379
ENCRYPTION_SECRET=change-me-to-a-secure-agent-secret-in-production
```
```dotenv
APP_URL=http://localhost:3000
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
S3_ENDPOINT=http://localhost:8333
REDIS_URL=redis://localhost:6379
```
<Tip>
**Email testing**: The development stack includes [Mailpit](https://mailpit.axllent.org/). Emails the app sends are captured there and viewable at [http://localhost:8025](http://localhost:8025), so nothing reaches a real address during development.
</Tip>
Then generate the secrets:
</Step>
<Step title="Run Database Migrations If Needed">
The server startup path runs migrations before serving traffic. To apply migrations manually without starting the app,
run the root migration script, which loads `.env.local` before invoking Drizzle Kit:
```bash
pnpm run db:migrate
```
</Step>
<Step title="Start the Development Server">
```bash
pnpm run dev
```
Your local Reactive Resume instance will be available at [http://localhost:3000](http://localhost:3000).
</Step>
```bash
openssl rand -hex 32 # paste the output into AUTH_SECRET
openssl rand -hex 32 # paste the output into ENCRYPTION_SECRET
```
`AUTH_SECRET` is required. `ENCRYPTION_SECRET` (at least 32 characters) is needed only for saved AI providers and the assistant, but it's easiest to set it now. Redis is optional; with it, assistant replies survive a page reload and rate limits are shared between server processes. Every variable is described in [Environment variables](/self-hosting/environment-variables).
<Tip>
Working on something that doesn't need uploads? Start only `postgres` and set `STORAGE_BACKEND=local`. Files then go to the `data/` folder in your checkout.
</Tip>
</Step>
<Step title="Start the app">
```bash
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000). The server applies database migrations on startup, so a fresh database is ready as soon as the app loads.
</Step>
<Step title="Create a local account">
Select **Sign up** and create an account. Without SMTP settings, the app doesn't send email: verification and password-reset links are printed in the terminal running `pnpm dev`. Copy the link from there into your browser.
</Step>
</Steps>
---
### What `pnpm dev` runs
## Available scripts
`pnpm dev` loads `.env.local` through dotenvx and starts three processes with Turborepo:
The scripts you will use most during development:
| Process | Address | Notes |
| --- | --- | --- |
| Vite (web app) | `http://localhost:3000` (`PORT`) | Hot reload. Proxies `/api`, `/mcp`, `/uploads`, `/.well-known`, and `/schema.json` to the server. |
| Hono (server) | `http://localhost:3001` (`SERVER_PORT`) | Restarts on change through `tsx watch`. |
| Email preview | `http://localhost:3002` | Previews the email templates in `packages/email`. |
### Development
Use `pnpm dev:web` to start only Vite. API calls still need a server running.
| Command | Description |
| ------------------------------ | ----------------------------------------------------------- |
| `pnpm dev` | Start the web and server development processes |
| `pnpm build` | Build the production web bundle and server bundle |
| `pnpm start` | Start the built production server |
| `pnpm typecheck` | Run TypeScript type checking |
| `pnpm test` | Run Vitest across workspaces |
| `pnpm exec biome check .` | Run a non-mutating Biome check |
| `pnpm check` | Run Biome with write/fix behavior (`--write --unsafe`) |
| `pnpm exec turbo boundaries` | Check workspace/package boundary rules |
## Everyday commands
### Database
| Command | What it does |
| --- | --- |
| `pnpm dev` | Start the web app, server, and email preview |
| `pnpm dev:web` | Start only the web app |
| `pnpm build` | Build the web app and server for production (regenerates PDF translations first) |
| `NODE_ENV=production pnpm start` | Run the built server on `PORT`. It reads exported variables or the root `.env`, not `.env.local`. |
| `pnpm typecheck` | Type-check every workspace with `tsgo` |
| `pnpm test` | Run the Vitest suites of every workspace |
| `pnpm test:e2e` | Run the Playwright browser tests (see [Browser tests](#browser-tests)) |
| `pnpm exec biome check <paths>` | Lint and format-check without changing files |
| `pnpm check` | **Changes files:** regenerates PDF translations and runs Biome with `--write --unsafe` |
| `pnpm exec turbo boundaries` | Check package boundary rules |
| `pnpm knip` | Find unused files, exports, and dependencies |
| `pnpm lingui:extract` | Extract new UI strings into `apps/web/locales/*.po` |
| `pnpm docs:gen` | Regenerate the OpenAPI spec and the custom-styles CSS reference |
| Command | Description |
| ---------------------- | -------------------------------------------- |
| `pnpm db:generate` | Generate migration files from schema changes |
| `pnpm db:migrate` | Apply pending migrations |
| `pnpm db:studio` | Open Drizzle Studio (database GUI) |
### Internationalization
| Command | Description |
| ------------------------- | -------------------------------------- |
| `pnpm run lingui:extract` | Extract translatable strings from code |
## Understanding the project structure
```
reactive-resume/
├── apps/
│ ├── web/ # TanStack Router routes, web features, and browser UI
│ └── server/ # Hono production server, HTTP adapters, static serving
├── packages/
│ ├── api/ # oRPC features and business behavior
│ ├── auth/ # Better Auth configuration and helpers
│ ├── db/ # Drizzle client and schema
│ ├── docx/ # DOCX export generation
│ ├── mcp/ # MCP tools, prompts, resources, and metadata
│ ├── pdf/ # PDF rendering (Forme) and PDF generation adapters
│ ├── resume/ # Pure resume-domain helpers
│ ├── schema/ # Zod schemas and typed models
│ ├── ui/ # Shared Base UI/shadcn-style primitives
│ └── ...
├── tooling/ # Development-only scripts and repository tooling
├── migrations/ # Generated database migrations
├── docs/ # Documentation
└── data/ # Local development data and uploads
```
---
## Working with the database
### Viewing the database
Use Drizzle Studio to explore and manage your database:
Prefer package-scoped commands while you work. Package names come from each `package.json`: the apps are `web` and `server`, and shared packages are `@reactive-resume/<name>`.
```bash
pnpm run db:studio
pnpm --filter web typecheck
pnpm --filter @reactive-resume/pdf test
pnpm exec biome check apps/web/src/features/resume
```
This opens a web-based GUI at [https://local.drizzle.studio](https://local.drizzle.studio).
## Work with the database
### Making schema changes
| Command | What it does |
| --- | --- |
| `pnpm db:generate` | Generate a migration from schema changes |
| `pnpm db:migrate` | Apply pending migrations without starting the app |
| `pnpm db:studio` | Open Drizzle Studio at [local.drizzle.studio](https://local.drizzle.studio) |
1. Edit the schema in `packages/db/src/schema/*`
2. Generate a migration:
```bash
pnpm run db:generate
```
3. Apply the migration:
```bash
pnpm run db:migrate
```
All three load `.env.local` before calling Drizzle Kit, which doesn't read `.env` files by itself.
<Warning>Always review generated migrations before applying them, especially when working with existing data.</Warning>
To change the schema:
---
1. Edit the tables in `packages/db/src/schema/*`.
2. Run `pnpm db:generate`. The migration is written to the root `migrations/` folder.
3. Read the generated SQL, then apply it with `pnpm db:migrate` or by restarting `pnpm dev`.
## Working with translations
<Warning>
Review every generated migration before you apply it. Don't reset the database or delete Docker volumes to work around a setup error; find the cause instead.
</Warning>
Reactive Resume uses [Lingui](https://lingui.dev/) for internationalization.
## Run tests
### Adding translatable text
### Unit and integration tests
Use the `t` macro for strings or `<Trans>` component for JSX:
Tests use [Vitest](https://vitest.dev/) and sit next to the code they cover as `*.test.ts(x)` or `*.spec.ts(x)`. Most packages run in Node; `packages/ui` uses `happy-dom`.
```bash
# One package
pnpm --filter @reactive-resume/pdf test
# One file (the path is relative to the package)
pnpm --filter @reactive-resume/pdf test src/templates/shared/filtering.test.ts
# One test by name
pnpm --filter @reactive-resume/pdf exec vitest run src/templates/shared/filtering.test.ts -t "filterItems"
# Coverage (V8, written to the package's coverage/ folder)
pnpm --filter @reactive-resume/pdf test:coverage
```
Pass file paths straight after `test`. An extra `--` stops Vitest from filtering the run. Most test scripts use `--passWithNoTests`, so a green run with zero tests proves nothing about your change.
Two suites need a real PostgreSQL database: set `COVER_LETTER_TEST_DATABASE_URL` and `OAUTH_TEST_DATABASE_URL`. Give the OAuth suite its own database, because it writes signing keys. Never point test variables at a database with real data. `.github/workflows/e2e.yml` shows the full setup.
### Browser tests
End-to-end tests use [Playwright](https://playwright.dev/) and live in `tests/e2e/specs`, with fixtures in `tests/e2e/fixtures`. They cover sign-up and sign-in, section editing and autosave, JSON export and import, public sharing with statistics and passwords, OAuth consent for MCP clients, and the assistant against a scripted AI provider.
Playwright starts the **built** server (`node apps/server/dist/index.mjs`) in production mode and waits for `/api/health`, so build first. Use a disposable database, and export the variables yourself; these scripts don't load `.env.local`.
```bash
pnpm exec playwright install chromium
export APP_URL=http://localhost:3000 PORT=3000
export DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
export AUTH_SECRET=$(openssl rand -hex 32) ENCRYPTION_SECRET=$(openssl rand -hex 32)
export LOCAL_STORAGE_PATH="$PWD/data/e2e"
export FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true
export FLAG_ALLOW_UNSAFE_AI_BASE_URL=true # lets the assistant spec reach its local stub
pnpm db:migrate
pnpm build
pnpm test:e2e # all specs
pnpm test:e2e tests/e2e/specs/auth.spec.ts # one spec
pnpm test:e2e:ui # Playwright's interactive UI
```
Without `FLAG_ALLOW_UNSAFE_AI_BASE_URL=true`, the assistant spec skips itself. Playwright runs Chromium with no retries. Locally it reuses a server that's already running on `PORT`. See `tests/e2e/README.md` for the full recipe.
<Note>
Keep the unsafe AI and OAuth redirect flags for isolated test installations. They relax SSRF and redirect protections.
</Note>
## Add translatable text
The web app uses [Lingui](https://lingui.dev/). Wrap every user-facing string in a macro:
```tsx
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
// For plain strings
const message = t`Hello, World!`;
const label = t`Download PDF`;
// For JSX content
<Trans>Welcome to Reactive Resume</Trans>;
<Trans>Your resume is ready.</Trans>;
```
### Extracting translations
Then run `pnpm lingui:extract`. It updates the catalogs in `apps/web/locales/*.po` and regenerates the PDF section-title catalog. You only add English strings; translators handle the rest on Crowdin. See [Contributing translations](/contributing/translations).
After adding new translatable text, extract them to the locale files:
## Code style
```bash
pnpm run lingui:extract
```
- TypeScript is strict, including `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess`. Packages type-check with `tsgo --noEmit`.
- [Biome](https://biomejs.dev/) formats and lints: tabs, double quotes, 120-column lines, separated type imports, organized imports, and sorted Tailwind classes in `clsx`, `cva`, and `cn`. Set your editor to use Biome.
- React components with explicit props use a named props type, such as `type FooProps = {...}` with `function Foo(props: FooProps)`.
Translation files live in `apps/web/locales`, in `.po` format.
## Commits and pull requests
---
The Git hooks run automatically:
## Code quality
- **Before each commit**, Lefthook checks staged files for merge conflict markers and runs Biome with `--write --unsafe` on them, then stages the fixes.
- **On each commit message**, commitlint enforces [Conventional Commits](https://www.conventionalcommits.org/), such as `fix(pdf): keep the timeline dot round` or `docs: update the development guide`.
### Linting & formatting
Before you open a pull request:
Uses [Biome](https://biomejs.dev/) for linting, formatting, import organization, and Tailwind class sorting:
1. Run the type checks and tests for the packages you changed, plus a non-mutating Biome check.
2. Run `pnpm exec turbo boundaries` if you changed imports, exports, or shared contracts.
3. Run `pnpm build` if you changed runtime or bundling behavior.
4. Keep the pull request focused. Describe the problem, the new behavior, and the checks you ran, and link the related [GitHub issue](https://github.com/reactive-resume/reactive-resume/issues).
```bash
# Non-mutating check
pnpm exec biome check .
Pull requests run these GitHub Actions workflows:
# Project script with write/fix behavior
pnpm check
```
| Workflow | What it checks |
| --- | --- |
| `e2e.yml` | Unit tests for every workspace (`turbo run test:ci --concurrency=1`), a production build, and the Playwright suite |
| `vercel.yml` | Builds the Vercel artifact offline and checks the backend Function. See [Deployment checks](/contributing/deployment-checks). |
| `autofix.yml` | Runs `pnpm knip --fix` and `pnpm check`, then pushes any fixes to your branch |
### Type checking
Run TypeScript type checking:
```bash
pnpm run typecheck
```
<Tip>
Configure your IDE to use Biome for formatting and lint diagnostics. The repo uses tabs, double quotes, 120-column
lines, and organized import groups.
</Tip>
---
Keep credentials and personal resume data out of code, logs, test fixtures, issues, and pull requests.
## Troubleshooting
<AccordionGroup>
<Accordion title="Port 3000 or 3001 is already in use">
The Vite web server uses `PORT` (default `3000`), and the Hono server uses `SERVER_PORT` (default `3001`).
Either stop the conflicting process or choose alternate ports:
```bash
PORT=3002 SERVER_PORT=3003 pnpm dev
```
Stop the other process, or set different values for `PORT` and `SERVER_PORT` in `.env.local` and change `APP_URL` to match. Keep port `3002` free for the email preview.
</Accordion>
<Accordion title="Database connection refused">
Ensure Docker containers are running:
```bash
docker compose -f compose.dev.yml ps
docker compose -f compose.dev.yml up -d
```
Check that PostgreSQL is healthy and accessible on port 5432.
<Accordion title="The database connection is refused">
Check that the containers are healthy with `docker compose -f compose.dev.yml ps`. Code on your machine connects to `localhost`; code inside a container uses the service name, such as `postgres`.
</Accordion>
<Accordion title="S3/Storage errors">
Verify SeaweedFS is running and the bucket exists:
<Accordion title="Uploads fail with S3 errors">
Read the storage logs:
```bash
docker compose -f compose.dev.yml logs seaweedfs
docker compose -f compose.dev.yml logs seaweedfs_create_bucket
```
If the bucket wasn't created, restart the bucket creation service:
```bash
docker compose -f compose.dev.yml restart seaweedfs_create_bucket
docker compose -f compose.dev.yml logs seaweedfs seaweedfs_create_bucket
```
Check that `S3_ENDPOINT` is `http://localhost:8333` and the bucket exists. If you don't need S3, set `STORAGE_BACKEND=local`.
</Accordion>
<Accordion title="Type errors after pulling changes">
The route tree may need regeneration. Run the dev server which auto-generates routes:
```bash
pnpm run dev
```
Or run type checking to see specific errors:
```bash
pnpm run typecheck
```
<Accordion title="Type errors about routes after pulling or adding a route">
`apps/web/src/routeTree.gen.ts` is generated. Start `pnpm dev` (or run `pnpm build`) to regenerate it, and never edit it by hand.
</Accordion>
<Accordion title="AI providers are unavailable">
Saved AI providers and the assistant need `ENCRYPTION_SECRET` (at least 32 characters). Set it in `.env.local` and restart `pnpm dev`.
</Accordion>
<Accordion title="A server dependency fails to load on Vercel">
CommonJS server dependencies must be bundled. Add the package and its dependencies to `bundledInteropPackages` in `apps/server/tsdown.config.ts`. See [Deployment checks](/contributing/deployment-checks).
</Accordion>
</AccordionGroup>
---
## Next steps
<CardGroup cols={2}>
<Card title="Project Architecture" icon="folder-open" href="/contributing/architecture">
How the project and codebase are structured.
</Card>
<Card title="GitHub Repository" icon="github" href="https://github.com/reactive-resume/reactive-resume">
View the source code and contribute to the project.
</Card>
<Card title="Project architecture" icon="sitemap" href="/contributing/architecture">
Learn where each part of the code lives and where new code belongs.
</Card>
<Card title="GitHub repository" icon="github" href="https://github.com/reactive-resume/reactive-resume">
Browse the source, open issues, and send pull requests.
</Card>
</CardGroup>
+66 -110
View File
@@ -1,162 +1,118 @@
---
title: "Contributing translations"
description: "Contribute translations for Reactive Resume through Crowdin by joining the project, proposing strings, requesting new languages, and syncing updates."
description: "Help translate Reactive Resume on Crowdin: read the glossary, keep placeholders intact, request a new language, and update catalogs in a checkout."
---
Reactive Resume is used all over the world. If you speak a language other than English, you can help by contributing translations.
Reactive Resume is available in more than 50 languages, and every translation comes from volunteers. If you speak a language other than English, you can help people build their resume in it. You don't need to write code: translations happen in your browser on Crowdin.
---
## How translations reach the app
## How translations work
1. Developers write every interface string in English. The strings are collected in one source catalog, `apps/web/locales/en-US.po`.
2. Translators work on that catalog in the [Reactive Resume project on Crowdin](https://crowdin.com/project/reactive-resume).
3. After each change to the `main` branch, a GitHub workflow uploads the source catalog, downloads the latest translations, and opens a pull request titled "Sync Translations from Crowdin".
4. Once that pull request is merged, your translations ship with the next release of the app. There's usually a delay of a few days to a few weeks, depending on the release cycle.
Reactive Resume uses [Crowdin](https://crowdin.com/) as its localization management platform. Crowdin gives translators an interface for contributing translations without writing code or editing files directly.
The same translations also label the default section headings in downloaded PDFs, such as "Experience" and "Education", and the word "Present" in date ranges.
<Info>
The Reactive Resume Crowdin project is available at
[https://crowdin.com/project/reactive-resume](https://crowdin.com/project/reactive-resume).
</Info>
## Read the glossary first
Once translations are submitted and approved on Crowdin, they are automatically synced to the codebase and will be available in the next release of the app.
Most of the interface is made of short, standalone labels like `Board`, `Resume`, or `Check`. Without a sentence around them, it's easy to pick the wrong meaning. For example, **Resume** is always the document, never the verb "to resume".
---
The [glossary](https://github.com/reactive-resume/reactive-resume/blob/main/GLOSSARY.md) explains what each recurring term means in Reactive Resume, lists the wrong senses that earlier translations used, and names the terms that stay in English: the product name, technology names such as PDF, DOCX, JSON, API, and MCP, AI provider names, and template names such as Azurill and Pikachu. Read the entry for a term before you translate it.
## Updating catalogs in a checkout
The PDF renderer uses a generated subset of the same translations for default section headings. Run `pnpm pdf:translations` after editing or syncing the PO catalogs. The root `pnpm lingui:extract`, `pnpm check`, and `pnpm build` commands also regenerate this file automatically.
Commit `packages/pdf/src/section-title-catalog.json` with the catalog updates. Edit the source PO files rather than the generated JSON; a tooling test checks that they stay synchronized.
## Getting started
## Translate on Crowdin
<Steps>
<Step title="Create a Crowdin Account">
If you don't already have an account, sign up at [crowdin.com](https://crowdin.com/). You can register using your
email or sign up with Google, Facebook, Twitter, GitHub, or GitLab.
<Tip>
For detailed instructions on creating an account and getting started, see Crowdin's official [For
Translators](https://support.crowdin.com/for-translators/) documentation.
</Tip>
<Step title="Create a Crowdin account">
Sign up at [crowdin.com](https://crowdin.com/) with your email or an existing account such as GitHub or Google. Crowdin's [guide for translators](https://support.crowdin.com/for-translators/) explains the basics.
</Step>
<Step title="Join the Reactive Resume Project">
Navigate to the [Reactive Resume project on Crowdin](https://crowdin.com/project/reactive-resume) and click **Join**
to become a contributor.
</Step>
<Step title="Join the project">
Open the [Reactive Resume project](https://crowdin.com/project/reactive-resume) and select **Join**.
</Step>
<Step title="Select Your Language">
From the project dashboard, click on the language you want to translate. You'll see a list of files that need
translation along with the progress for each.
</Step>
<Step title="Start Translating">
Click on a file to open the Crowdin Editor. You'll see the source text (English) on the left and a text field for
your translation on the right. - Translate the text accurately while preserving any placeholders or formatting - Use
the suggestions from Translation Memory and Machine Translation as a starting point - Vote on existing translations
if you agree with them
</Step>
<Step title="Save Your Translations">
Your translations are saved automatically as you work. Once reviewed, they'll be included in the next app release.
</Step>
<Step title="Choose your language">
Select your language on the project dashboard to see how much is already translated.
</Step>
<Step title="Translate strings">
Open the file to start the Crowdin editor. The English source is on one side and your translation on the other. Use translation memory and machine suggestions as a starting point, and vote for existing translations you agree with. Crowdin saves your work as you go.
</Step>
</Steps>
---
If a string is unclear, leave a comment on it in Crowdin. You can also search the source code for the English text to see where it appears.
## Translation guidelines
To maintain consistency across all translations, please follow these guidelines:
### Keep placeholders unchanged
### Preserve placeholders
Some strings contain placeholders like `{name}` or `{count}`. These must remain unchanged in your translation:
Words in curly braces are filled in by the app. Keep them exactly as they are, but move them wherever your grammar needs them. For example:
```
English: "Hello, {name}!"
Spanish: "¡Hola, {name}!"
English: “{name}” moved to Trash
German: „{name}“ in den Papierkorb verschoben
```
### Keep formatting
Numbered placeholders such as `{0}` often come with a note in Crowdin, like `placeholder {0}: application.role`, that tells you what the value is.
Preserve any HTML tags or markdown formatting in the source text:
### Keep numbered tags around the same words
Tags such as `<0>` and `</0>` mark text that gets a link or emphasis. Keep each pair, and wrap the words that carry the same meaning in your language. For example:
```
English: "Click <1>here</1> to continue"
German: "Klicken Sie <1>hier</1>, um fortzufahren"
English: Have a resume already? <0>Import it</0>
French: Vous avez déjà un CV ? <0>Importez-le</0>
```
### Use formal or informal tone consistently
### Translate every plural form
Choose either formal or informal language based on what's standard for software in your language, and stick with it throughout.
Some strings change with a number. They use this pattern:
### Technical terms
```
{0, plural, one {# application ready to import} other {# applications ready to import}}
```
Some technical terms (like "PDF", "URL", "JSON") are often kept in English across languages. Use your judgment based on what's common in your language's software community.
Translate the text inside each set of braces, keep `#` where the number goes, and don't translate the keywords `plural`, `one`, and `other`. Crowdin shows the plural categories your language needs.
---
### Be consistent
## Requesting a new language
- Choose a formal or informal tone based on what's normal for software in your language, and use it everywhere.
- Where your language normally calls this document a CV, use CV.
- Reuse the same word for a term throughout. The glossary lists the terms that matter most.
If your language is not listed in the Crowdin project, you can request it to be added.
## Request a new language
<Warning>
Before requesting a new language, please check if it's already available in the [Crowdin
project](https://crowdin.com/project/reactive-resume).
</Warning>
First check whether your language is already listed in the [Crowdin project](https://crowdin.com/project/reactive-resume). If it isn't:
To request a new language:
1. Open a new issue on [GitHub](https://github.com/reactive-resume/reactive-resume/issues/new/choose).
2. Title it "Add [language name] translation".
3. Include the language name and its locale code, such as `ja-JP` for Japanese.
1. Go to the [GitHub Issues](https://github.com/reactive-resume/reactive-resume/issues) page
2. Click **New Issue**
3. Select the appropriate template or create a blank issue
4. Title it something like: "Add [Language Name] to Reactive Resume"
5. Include the language name and locale code (e.g., "Japanese - ja-JP") in the issue description.
Once a maintainer adds the language, you can start translating it on Crowdin.
Once approved, the language will be added to Crowdin and you can begin translating.
## Updating catalogs in a checkout
---
This section is for developers working in the repository.
## When will my translations appear?
After you add or change user-facing strings with Lingui macros, extract them:
Translations submitted on Crowdin are synced to the codebase periodically. Once merged, they will be included in the next release of Reactive Resume.
```bash
pnpm lingui:extract
```
<Info>
There may be a delay between submitting translations and seeing them live in the app. This is normal and depends on
the release cycle.
</Info>
This updates every catalog in `apps/web/locales/*.po` and then runs `pnpm pdf:translations`, which regenerates two files from the catalogs:
---
- `packages/pdf/src/section-title-catalog.json`: default section titles for PDFs.
- `packages/schema/src/resume/present-labels.json`: the translated word for "Present" in date ranges.
## Tips for effective translation
`pnpm check` and `pnpm build` also regenerate them. Commit the generated files together with the catalog changes. Edit the `.po` files, never the JSON; a test in `tooling/locales` fails when the two drift apart.
<CardGroup cols={2}>
<Card title="Use Context" icon="eye">
Crowdin often shows context, screenshots, or comments to help you understand where the text appears in the app.
</Card>
<Card title="Check Existing Translations" icon="check">
Review translations by other contributors and vote for accurate ones to help maintain quality.
</Card>
<Card title="Ask Questions" icon="comment">
Use Crowdin's comment feature to ask about unclear strings or discuss translations with other contributors.
</Card>
<Card title="Stay Consistent" icon="book">
Check the project glossary (if available) to ensure terminology is used consistently across the app.
</Card>
</CardGroup>
Only edit `en-US.po` by extracting it from code. Other catalogs arrive through the Crowdin pull request, so change translations on Crowdin rather than in the repository, or your edit is overwritten by the next sync.
---
To add a new locale, a maintainer adds its code to `locales` in `apps/web/lingui.config.ts`, to `localeSchema` in `packages/utils/src/locale.ts`, and to `localeMap` in `apps/web/src/libs/locale.ts`, then runs `pnpm lingui:extract`.
## Need help?
## Related pages
<CardGroup cols={2}>
<Card icon="book-open" title="Crowdin Translator Docs" href="https://support.crowdin.com/for-translators/">
Official Crowdin documentation for translators.
</Card>
<Card icon="github" title="GitHub Issues" href="https://github.com/reactive-resume/reactive-resume/issues">
Report issues or request new languages.
</Card>
</CardGroup>
---
Thank you for helping translate Reactive Resume.
- [Changing appearance and language](/guides/changing-appearance-and-language): switch the app's language.
- [Development setup](/contributing/development): run Reactive Resume locally.
- [Crowdin translator docs](https://support.crowdin.com/for-translators/): how the Crowdin editor works.