Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/lucky-moons-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'saykit': minor
'@saykit/config': minor
'@saykit/react': minor
'babel-plugin-saykit': minor
'unplugin-saykit': minor
---

Compile catalogues to data modules with `saykit compile`, and drop the runtime message parser
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ node_modules/
.coverage/
dist/
RELEASE_NOTES.md
packages/integration/skills/
packages/integration/skills/
# Locale modules the saykit CLI compiles from the catalogues beside them.
# Their declarations are committed, so `tsc` passes before anything is generated.
examples/**/locales/*.js
examples/**/_locales/**/*.js
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ SayKit is a framework-agnostic internationalisation toolkit built around compile

- Tagged-template and JSX authoring: `` say`Hello, ${name}!` `` or `<Say>Hello, {name}!</Say>`
- Compile-time extraction from JS, TS, JSX, and TSX via Babel or unplugin
- ICU MessageFormat support for plurals, ordinals, and select
- ICU MessageFormat for plurals, ordinals, and select, compiled ahead of time
- Typed `saykit.config.ts` with a Zod-validated schema
- A small core runtime, with optional framework adapters
- Import translation files directly: no build-time replacement, no shipped extraction
- Catalogues compile to plain `.js` modules: no parser, no loader, nothing to configure in your bundler

## Documentation

Expand All @@ -33,7 +33,7 @@ This is a pnpm monorepo. The published packages live in [`packages/*`](./package

| Package | Description |
| --------------------------------------------------- | -------------------------------------------------------------------- |
| [`saykit`](./packages/integration) | Core runtime: catalogues, views, macros, and ICU formatting |
| [`saykit`](./packages/integration) | Core runtime: catalogues, views, macros, and `Intl` helpers |
| [`@saykit/config`](./packages/config) | Config schema (`defineConfig`) and the `saykit` CLI |
| [`@saykit/react`](./packages/integration-react) | React integration: `<Say>`, `SayProvider`, server helpers |
| [`@saykit/carbon`](./packages/integration-carbon) | Carbon Discord-bot integration |
Expand Down Expand Up @@ -90,8 +90,8 @@ export default defineConfig({

```ts title="src/app.ts"
import { createCatalogue } from 'saykit';
import en from './locales/en.po';
import fr from './locales/fr.po';
import en from './locales/en.js';
import fr from './locales/fr.js';

const catalogue = createCatalogue({ en, fr });
const say = catalogue.locale('en');
Expand All @@ -103,11 +103,11 @@ console.log(say`Hello, ${'world'}!`);
pnpm saykit extract
```

Extraction only writes the source locale (the first entry in `locales`); new locales get an empty
placeholder file and existing translation files are left untouched, keeping diffs small and leaving
translated content to your TMS. Untranslated keys fall back to a configurable fallback chain (and
ultimately the source string) at load time, and `saykit clean` can prune orphaned and untranslated
entries from other locale files when your TMS doesn't do it for you.
Extraction writes the source locale's catalogue (`en.po`), creates empty files for new locales, and
leaves existing translations alone. It then compiles each locale into the `en.js` / `fr.js` your app
imports, filling untranslated keys from a fallback chain that ends at the source string. Ignore the
compiled `.js` in git and run `saykit compile` before dev and build. `saykit clean` prunes orphaned
entries from other locales.

## React

Expand All @@ -123,8 +123,8 @@ pnpm add -D @saykit/transform-jsx
import { Say } from '@saykit/react';
import { SayProvider } from '@saykit/react/client';
import { createCatalogue, createStore } from 'saykit';
import en from './locales/en.po';
import fr from './locales/fr.po';
import en from './locales/en.js';
import fr from './locales/fr.js';

const store = createStore(createCatalogue({ en, fr }), 'en');

Expand Down
29 changes: 13 additions & 16 deletions examples/babel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
SayKit compiled by **Babel and nothing else**: no bundler, no dev server, no loader.
`pnpm build`, then `node dist/main.js`.

This exists to pin down the `catalogues: 'inline'` default: `babel-plugin-saykit` on its
own has to rewrite the macros _and_ resolve the catalogue imports, with no other tool
involved. Every other example runs the plugin alongside a bundler integration, so this is
the only one that would notice if that stopped being true.
This exists to pin down what `babel-plugin-saykit` does on its own: rewrite the macros,
and nothing else. Catalogues are compiled to `.js` modules by `saykit compile`, which Node
imports directly. Every other example runs the plugin alongside a bundler, so this is the
only one that would notice if the plugin started needing one.

## Run

Expand All @@ -31,18 +31,15 @@ English source string, merged in at compile time rather than looked up at runtim

## What to look at

| Thing | Where |
| ----------------------------------------------------------------- | ----------------- |
| `plugins: ['saykit']`, no options: the inlining default | `babel.config.js` |
| `import en from './locales/en.po'`, gone by the time Node sees it | `src/main.ts` |
| The compiled record, one object literal per locale | `dist/main.js` |
| Thing | Where |
| ------------------------------------------------------ | ------------------- |
| `plugins: ['saykit']`, no options and nothing else | `babel.config.js` |
| `import en from './locales/en.js'`, an ordinary import | `src/main.ts` |
| Each message compiled to data, with its ICU above it | `src/locales/fr.js` |

Open `dist/main.js` after a build: there is no `.po` file, no PO parser and no SayKit
extractor in the output, just `const en = { … }` and `say.call()` invocations.
Open `dist/main.js` after a build: no `.po` file, no PO parser, no message parser and no
SayKit extractor, just `say.call()` invocations against the imported locale modules.

## Hot reload

There is none, by design. The record lands inside `dist/main.js`, whose own bytes only
change when you rebuild, which is exactly why a dev server wants `catalogues: 'module'`
and a bundler integration instead. See the
Open `src/locales/fr.js` to see what those modules hold: each message as plain data, with
every number and date format already resolved to its `Intl` options. See the
[Babel integration docs](../../website/content/integrations/babel.mdx).
8 changes: 6 additions & 2 deletions examples/babel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
"scripts": {
"check": "tsc --noEmit",
"extract": "saykit extract",
"build": "babel src --extensions .ts --out-dir dist --ignore \"src/**/*.d.po.ts\"",
"start": "pnpm build && node dist/main.js"
"build": "babel src --extensions .ts,.js --out-dir dist --ignore \"src/**/*.d.ts\"",
"start": "pnpm build && node dist/main.js",
"compile": "saykit compile",
"prebuild": "saykit compile",
"precheck": "saykit compile",
"prestart": "saykit compile"
},
"dependencies": {
"saykit": "workspace:^"
Expand Down
2 changes: 0 additions & 2 deletions examples/babel/src/locales/en.d.po.ts

This file was deleted.

4 changes: 4 additions & 0 deletions examples/babel/src/locales/en.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Message } from 'saykit';

declare const messages: Record<string, Message>;
export default messages;
2 changes: 0 additions & 2 deletions examples/babel/src/locales/fr.d.po.ts

This file was deleted.

4 changes: 4 additions & 0 deletions examples/babel/src/locales/fr.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Message } from 'saykit';

declare const messages: Record<string, Message>;
export default messages;
4 changes: 2 additions & 2 deletions examples/babel/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createCatalogue } from 'saykit';
import en from './locales/en.po';
import fr from './locales/fr.po';
import en from './locales/en.js';
import fr from './locales/fr.js';

const catalogue = createCatalogue({ en, fr });

Expand Down
2 changes: 0 additions & 2 deletions examples/browser-extension/_locales/de/messages.d.json.ts

This file was deleted.

4 changes: 4 additions & 0 deletions examples/browser-extension/_locales/de/messages.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Message } from 'saykit';

declare const messages: Record<string, Message>;
export default messages;
2 changes: 0 additions & 2 deletions examples/browser-extension/_locales/en/messages.d.json.ts

This file was deleted.

4 changes: 4 additions & 0 deletions examples/browser-extension/_locales/en/messages.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Message } from 'saykit';

declare const messages: Record<string, Message>;
export default messages;
2 changes: 0 additions & 2 deletions examples/browser-extension/_locales/fr/messages.d.json.ts

This file was deleted.

4 changes: 4 additions & 0 deletions examples/browser-extension/_locales/fr/messages.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Message } from 'saykit';

declare const messages: Record<string, Message>;
export default messages;
10 changes: 7 additions & 3 deletions examples/browser-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"check": "tsc --noEmit",
"extract": "saykit extract",
"build": "vite build",
"dev": "vite build --watch"
"dev": "vite build --watch",
"compile": "saykit compile",
"predev": "saykit compile",
"prebuild": "saykit compile",
"precheck": "saykit compile"
},
"dependencies": {
"saykit": "workspace:^"
Expand All @@ -18,9 +22,9 @@
"@saykit/config": "workspace:^",
"@saykit/format-json": "workspace:^",
"@saykit/transform-js": "workspace:^",
"@types/chrome": "^0.3.5",
"@types/chrome": "^0.3.7",
"typescript": "^7.0.2",
"unplugin-saykit": "workspace:^",
"vite": "^8.3.4"
"vite": "^8.3.5"
}
}
6 changes: 3 additions & 3 deletions examples/browser-extension/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createCatalogue } from 'saykit';
import de from '../_locales/de/messages.json';
import en from '../_locales/en/messages.json';
import fr from '../_locales/fr/messages.json';
import de from '../_locales/de/messages.js';
import en from '../_locales/en/messages.js';
import fr from '../_locales/fr/messages.js';

export const locales = ['en', 'fr', 'de'] as const;
export type Locale = (typeof locales)[number];
Expand Down
8 changes: 6 additions & 2 deletions examples/carbon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
"check": "tsc --noEmit",
"extract": "saykit extract",
"build": "tsdown",
"dev": "wrangler dev"
"dev": "wrangler dev",
"compile": "saykit compile",
"predev": "saykit compile",
"prebuild": "saykit compile",
"precheck": "saykit compile"
},
"dependencies": {
"@buape/carbon": "0.17.0",
Expand All @@ -20,6 +24,6 @@
"@saykit/transform-js": "workspace:^",
"typescript": "^7.0.2",
"unplugin-saykit": "workspace:^",
"wrangler": "^4.151.0"
"wrangler": "^4.155.0"
}
}
8 changes: 4 additions & 4 deletions examples/carbon/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createWithSay } from '@saykit/carbon';
import { createCatalogue } from 'saykit';
import de from './locales/de.json';
import en from './locales/en-US.json';
import fr from './locales/fr.json';
import ja from './locales/ja.json';
import de from './locales/de.js';
import en from './locales/en-US.js';
import fr from './locales/fr.js';
import ja from './locales/ja.js';

export const locales = ['en-US', 'fr', 'de', 'ja'] as const;
export type Locale = (typeof locales)[number];
Expand Down
2 changes: 0 additions & 2 deletions examples/carbon/src/locales/de.d.json.ts

This file was deleted.

4 changes: 4 additions & 0 deletions examples/carbon/src/locales/de.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Message } from 'saykit';

declare const messages: Record<string, Message>;
export default messages;
2 changes: 0 additions & 2 deletions examples/carbon/src/locales/en-US.d.json.ts

This file was deleted.

4 changes: 4 additions & 0 deletions examples/carbon/src/locales/en-US.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Message } from 'saykit';

declare const messages: Record<string, Message>;
export default messages;
2 changes: 0 additions & 2 deletions examples/carbon/src/locales/fr.d.json.ts

This file was deleted.

4 changes: 4 additions & 0 deletions examples/carbon/src/locales/fr.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Message } from 'saykit';

declare const messages: Record<string, Message>;
export default messages;
2 changes: 0 additions & 2 deletions examples/carbon/src/locales/ja.d.json.ts

This file was deleted.

4 changes: 4 additions & 0 deletions examples/carbon/src/locales/ja.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Message } from 'saykit';

declare const messages: Record<string, Message>;
export default messages;
8 changes: 6 additions & 2 deletions examples/custom-formatter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"check": "tsc --noEmit",
"extract": "saykit extract",
"build": "tsdown",
"start": "node dist/main.mjs"
"start": "node dist/main.mjs",
"compile": "saykit compile",
"prebuild": "saykit compile",
"precheck": "saykit compile",
"prestart": "saykit compile"
},
"dependencies": {
"saykit": "workspace:^",
Expand All @@ -18,7 +22,7 @@
"devDependencies": {
"@saykit/config": "workspace:^",
"@saykit/transform-js": "workspace:^",
"tsdown": "^0.23.1",
"tsdown": "^0.23.3",
"typescript": "^7.0.2",
"unplugin-saykit": "workspace:^"
}
Expand Down
6 changes: 3 additions & 3 deletions examples/custom-formatter/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createCatalogue } from 'saykit';
import de from './locales/de.yml';
import en from './locales/en.yml';
import fr from './locales/fr.yml';
import de from './locales/de.js';
import en from './locales/en.js';
import fr from './locales/fr.js';

export const locales = ['en', 'fr', 'de'] as const;
export type Locale = (typeof locales)[number];
Expand Down
4 changes: 4 additions & 0 deletions examples/custom-formatter/src/locales/de.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Message } from 'saykit';

declare const messages: Record<string, Message>;
export default messages;
2 changes: 0 additions & 2 deletions examples/custom-formatter/src/locales/de.d.yml.ts

This file was deleted.

4 changes: 4 additions & 0 deletions examples/custom-formatter/src/locales/en.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Message } from 'saykit';

declare const messages: Record<string, Message>;
export default messages;
2 changes: 0 additions & 2 deletions examples/custom-formatter/src/locales/en.d.yml.ts

This file was deleted.

4 changes: 4 additions & 0 deletions examples/custom-formatter/src/locales/fr.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Message } from 'saykit';

declare const messages: Record<string, Message>;
export default messages;
2 changes: 0 additions & 2 deletions examples/custom-formatter/src/locales/fr.d.yml.ts

This file was deleted.

4 changes: 2 additions & 2 deletions examples/expo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ would otherwise produce loose strings between elements.
## Why Babel

Metro is not a Vite/Rollup-family bundler, so `unplugin-saykit` does not apply. Metro compiles every
module through Babel, and `babel-plugin-saykit` hooks in there instead, doing the same two jobs:
rewriting the macros, and inlining `import en from './locales/en.json'` as a plain object.
module through Babel, and `babel-plugin-saykit` hooks in there instead to rewrite the macros.
`import en from './locales/en.js'` is an ordinary import of the module `saykit compile` writes.

```js
module.exports = function babelConfig(api) {
Expand Down
2 changes: 1 addition & 1 deletion examples/expo/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ module.exports = function babelConfig(api) {

return {
presets: ['babel-preset-expo'],
plugins: [['saykit', { catalogues: 'module' }]],
plugins: ['saykit'],
};
};
3 changes: 1 addition & 2 deletions examples/expo/metro.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
const path = require('node:path');
const { getDefaultConfig } = require('expo/metro-config');
const { withSayKit } = require('babel-plugin-saykit/metro');

const config = withSayKit(getDefaultConfig(__dirname));
const config = getDefaultConfig(__dirname);

// Two copies of React break hooks, so force every request to this app's copy
config.resolver.resolveRequest = (context, moduleName, platform) => {
Expand Down
Loading
Loading