Guides
Monorepo
Two workspace layouts, what the usage scan binds across packages, and two trees in one program.
import { defineConfig } from 'loclizr'
export default defineConfig({ locales: ['en', 'de', 'de-AT'], sourceLocale: 'en', scan: { include: ['../../apps/web/src/**/*.{ts,tsx}'] },})One build reads one catalog root and writes one tree, so a workspace picks a layout:
| Layout | Catalogs, config and tree | check runs |
Scan sees |
|---|---|---|---|
| Per app | in each app | once per app | that app’s sources |
| Shared package | in packages/i18n |
once, in that package’s CI job | every app in scan.include |
Catalogs in each app
Section titled “Catalogs in each app”Each app keeps its own locales/, config and tree, and its record lands in its own pull request.
From the workspace root, run the loclizr each app pins:
- run: pnpm -C apps/web exec loclizr check- run: pnpm -C apps/admin exec loclizr check| Package manager | Root step | Runs |
|---|---|---|
| pnpm | pnpm -C apps/web exec loclizr check |
the version apps/web pins |
npm workspaces, yarn with node_modules |
npx loclizr check --cwd apps/web |
the hoisted version at the root |
Under pnpm the root has no loclizr bin, so npx loclizr downloads the registry’s latest instead
of the version the app builds with.
One shared package
Section titled “One shared package”{ "name": "@acme/i18n", "type": "module", "exports": { "./messages": "./src/loclizr/messages.js", "./groups": "./src/loclizr/groups.js" }}-
Build in
Section titled “Build in packages/i18n”packages/i18nIt holds the catalogs, runs
loclizr build, and exports the barrel and groups. -
Point
Section titled “Point scan.include at the apps”scan.includeat the appsThe config at the top does this. Paths then print relative to the package:
../../apps/web/src/App.tsx. -
Gate in the package’s CI job
Section titled “Gate in the package’s CI job”One
checkcovers every app’s strings.
What the scan binds
Section titled “What the scan binds”The scan matches import specifiers by suffix and resolves nothing, so @/loclizr/messages binds
with no alias map. Across packages that cuts both ways. Built from packages/i18n, with App.tsx
calling m.nav_home():
Import in apps/web/src/App.tsx |
Bound | usage for nav.home |
|---|---|---|
'@acme/i18n/messages' |
no | [], and LZ5004 fires |
'../../../packages/i18n/src/loclizr/messages' |
yes | ../../apps/web/src/App.tsx, scope App |
'@acme/i18n/loclizr/messages' |
yes, by suffix | same |
The unbound form loses usage and warns on every build:
warn LZ5004 scan-found-nothing
The scan read 1 file and found no import of the generated messages.
fix check scan.include in loclizr.config.ts. A usage site is found through an import such as import * as m from './loclizr/messages'To bind a package import, add "./loclizr/messages": "./src/loclizr/messages.js" to exports
and import it as row three does.
Two trees in one program
Section titled “Two trees in one program”An app with its own tree that imports the shared package has two barrels in one program.
| Layer | Same locale lists | Different lists |
|---|---|---|
| Types | Both augment LocaleRegistry by default; they merge. |
TS2717 with skipLibCheck off; silent under Vite’s default skipLibCheck: true, first tree’s AppLocale wins. |
| Runtime | Each registers locales and cookie name on import; first wins. | Outside production: a second generated directory registered [...]; the first registration keeps getLocale(). |
Either way, getLocale(), setLocale() and useLocale() are typed by the first tree’s list;
augmentLocale: false on the second makes that explicit.
What works: import the app’s tree first, augmentLocale on, and give the shared package the same
locales, sourceLocale and cookie. Its registration is a no-op; one locale resolves.
For a published package, set augmentLocale: false. A library cannot match locale lists it never
sees, and a consumer with its own tree would get TS2717 inside node_modules once skipLibCheck
is off, tsc’s default. A tree packed from dist also needs its generated .gitignore deleted:
left in place, npm pack ships the directory empty. Publishing a library
has the full build.
Declaration emit
Section titled “Declaration emit”A package built with declaration: true that infers an exported type from loclizr/react,
loclizr/server or loclizr/compiler needs TypeScript 5.4 or later. On 5.0 to 5.3 the emit fails
with TS2742; annotate the exported function’s return type.
Not supported
Section titled “Not supported”- A second catalog root:
catalogsis one pattern. - Merging another package’s catalogs into an app’s tree. A shared string lives in the shared package, or in both apps.
- Anything cross-package but
scan.include: catalogs, record and tree stay beside the config.