paraglide locales workspace

why

  • per-app message tables drift: same string translated independently, product change lands in one app only, translators update one workspace while others go stale
  • one owner makes messages a versioned, generated contract: every consumer rebuilds against the same source, missing message = type error not runtime lookup failure

rule

  • use Paraglide for application i18n
  • one dedicated workspace named locales owns: inlang config, source locale files, generated typed message functions, locale metadata, validation, generation command
  • applications consume its public typed API only — no parallel message folders, translation helpers, or handwritten string-key types
  • Paraglide compiles messages into tree-shakable, typed functions: missing message, wrong interpolation argument, or stale call fails at type/build time, not runtime

workspace contract

packages/
locales/
project.inlang/
settings.json
messages/
en.json
vi.json
src/
generated/
locale.ts
package.json
concernlocales ownsapplication owns
message sourcecanonical locale files and message idsno duplicate source tables.
generated APIParaglide output and its package exportimporting typed m.* functions.
locale policysupported locales, base locale, fallback decisionselecting locale at the framework/request boundary.
message wordingcanonical translation contentchoosing a domain label mapper or rendering context.
validationParaglide/inlang check and generation taskmaking workspace validation part of its pipeline dependency.

message ids:

  • camel-case semantic ids describing rendered meaning: processStatusActive, billingChargePending
  • never ids from source paths, english text, visual style, or case-conversion functions
  • values with domain meaning route through a label file under mappers/i18n/

generation boundary

  • authored: source message files, inlang settings. generated, never hand-edited: Paraglide output
  • locales exposes one documented public entrypoint for messages and locale utilities; app workspaces depend on it and on the locales generation task through the task graph
  • adding a message: pick domain owner → add exact semantic key to base locale → supply required translations → run generation + validation → import the generated typed function. no temporary raw string while waiting for translation work

verification

  1. run the locales validation and generation commands before application type checking.
  2. typecheck every consuming workspace; missing message functions and invalid interpolation arguments must fail visibly.
  3. search for parallel messages/, translations/, t() wrappers, and raw user-facing literals outside the workspace; remove unauthorized owners.
  4. add a required interpolation to one message and confirm every typed caller is updated by the compiler.
  5. ensure every domain enum shown to users has a label mapper that imports from this workspace.

good and bad

bad

// apps/web/src/i18n/messages.ts
export const messages = {
workflowStatusActive: "Active",
};
// apps/mobile/src/i18n/messages.ts
export const messages = {
workflowStatusActive: "en cours", // translator updated mobile only
};
// apps/admin/src/i18n/messages.ts
export const messages = {
workflowStatusActive: "Active", // admin is stale
};

each app owns its messages: a product change to "Running" updates web only; a translator fixes French in mobile, admin is forgotten; users see inconsistent terminology.

good

// packages/locales/messages/en.json (canonical source)
{
"workflowStatusActive": "Running"
}
// packages/locales/messages/fr.json
{
"workflowStatusActive": "En cours"
}
// apps/web/src/App.tsx
import { m } from "@scope/locales";
<span>{m.workflowStatusActive()}</span> // "Running"
// apps/mobile/src/App.tsx
import { m } from "@scope/locales";
<Text>{m.workflowStatusActive()}</Text> // "Running"

generated function verifies message name and required interpolation at build time; one canonical source keeps every consumer on the current translation.

search pages

go to any page