diff --git a/.changeset/twelve-eggs-nail.md b/.changeset/twelve-eggs-nail.md new file mode 100644 index 0000000..2164fd8 --- /dev/null +++ b/.changeset/twelve-eggs-nail.md @@ -0,0 +1,8 @@ +--- +"unplugin-saykit": patch +"babel-plugin-saykit": patch +"@saykit/format-po": patch +"@saykit/config": patch +--- + +Add support for importing translation files directly diff --git a/.github/labeller.yml b/.github/labeller.yml index 9999593..d14c620 100644 --- a/.github/labeller.yml +++ b/.github/labeller.yml @@ -40,7 +40,7 @@ 'package: unplugin': - changed-files: - - any-glob-to-any-file: ['packages/unplugin/**'] + - any-glob-to-any-file: ['packages/plugin-unplugin/**'] 'package: transform-js': - changed-files: diff --git a/examples/carbon/package.json b/examples/carbon/package.json index 14657b2..c1da054 100644 --- a/examples/carbon/package.json +++ b/examples/carbon/package.json @@ -5,7 +5,6 @@ "scripts": { "check": "tsc --noEmit", "extract": "saykit extract", - "compile": "saykit compile", "build": "tsdown", "dev": "wrangler dev" }, diff --git a/examples/carbon/saykit.config.ts b/examples/carbon/saykit.config.ts index 71fa780..5ebac81 100644 --- a/examples/carbon/saykit.config.ts +++ b/examples/carbon/saykit.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from '@saykit/config'; -import createPoFormatter from '@saykit/format-po'; -import createJsTransformer from '@saykit/transform-js'; +import po from '@saykit/format-po'; +import js from '@saykit/transform-js'; export default defineConfig({ sourceLocale: 'en', @@ -8,9 +8,9 @@ export default defineConfig({ buckets: [ { include: ['src/**/*.ts'], - output: 'src/locales/{locale}/messages.{extension}', - formatter: createPoFormatter(), - transformer: createJsTransformer(), + output: 'src/locales/{locale}.{extension}', + formatter: po(), + transformer: js(), }, ], }); diff --git a/examples/carbon/src/i18n.ts b/examples/carbon/src/i18n.ts index ee345cd..f1d7b0a 100644 --- a/examples/carbon/src/i18n.ts +++ b/examples/carbon/src/i18n.ts @@ -3,8 +3,8 @@ import { Say } from 'saykit'; const say = new Say({ locales: ['en', 'fr'], messages: { - en: await import('./locales/en/messages.json').then((m) => m.default), - fr: await import('./locales/fr/messages.json').then((m) => m.default), + en: await import('./locales/en.po').then((m) => m.default), + fr: await import('./locales/fr.po').then((m) => m.default), }, }); diff --git a/examples/carbon/src/locales/en/messages.po b/examples/carbon/src/locales/en.po similarity index 93% rename from examples/carbon/src/locales/en/messages.po rename to examples/carbon/src/locales/en.po index 2cf2c6e..88594ab 100644 --- a/examples/carbon/src/locales/en/messages.po +++ b/examples/carbon/src/locales/en.po @@ -1,8 +1,15 @@ msgid "" msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language: en\n" +"Language-Team: \n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: en\n" +"Plural-Forms: \n" "X-Generator: saykit\n" #: src/commands/maths.ts:17 diff --git a/examples/carbon/src/locales/fr/messages.po b/examples/carbon/src/locales/fr.po similarity index 93% rename from examples/carbon/src/locales/fr/messages.po rename to examples/carbon/src/locales/fr.po index 0305343..fb589de 100644 --- a/examples/carbon/src/locales/fr/messages.po +++ b/examples/carbon/src/locales/fr.po @@ -1,8 +1,15 @@ msgid "" msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language: fr\n" +"Language-Team: \n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: fr\n" +"Plural-Forms: \n" "X-Generator: saykit\n" #: src/commands/maths.ts:17 diff --git a/examples/nextjs/next-env.d.ts b/examples/nextjs/next-env.d.ts index 1511519..20e7bcf 100644 --- a/examples/nextjs/next-env.d.ts +++ b/examples/nextjs/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import './.next/types/routes.d.ts'; +import './.next/dev/types/routes.d.ts'; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 5c092b0..e0af977 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -5,7 +5,6 @@ "scripts": { "check": "tsc --noEmit", "extract": "saykit extract", - "compile": "saykit compile", "dev": "next dev", "build": "next build", "start": "next start" diff --git a/examples/nextjs/saykit.config.ts b/examples/nextjs/saykit.config.ts index b8404f8..554f08b 100644 --- a/examples/nextjs/saykit.config.ts +++ b/examples/nextjs/saykit.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from '@saykit/config'; -import createPoFormatter from '@saykit/format-po'; -import createJsTransformer from '@saykit/transform-js'; -import createJsxTransformer from '@saykit/transform-jsx'; +import po from '@saykit/format-po'; +import js from '@saykit/transform-js'; +import jsx from '@saykit/transform-jsx'; export default defineConfig({ sourceLocale: 'en', @@ -9,9 +9,9 @@ export default defineConfig({ buckets: [ { include: ['src/**/*.{ts,tsx}'], - output: 'src/locales/{locale}/messages.{extension}', - formatter: createPoFormatter(), - transformer: [createJsTransformer(), createJsxTransformer()], + output: 'src/locales/{locale}.{extension}', + formatter: po(), + transformer: [js(), jsx()], }, ], }); diff --git a/examples/nextjs/src/i18n.ts b/examples/nextjs/src/i18n.ts index 5f42da8..92d6870 100644 --- a/examples/nextjs/src/i18n.ts +++ b/examples/nextjs/src/i18n.ts @@ -1,13 +1,12 @@ import 'server-only'; import { unstable_createWithSay } from '@saykit/react/server'; import { Say } from 'saykit'; +import en from './locales/en.po'; +import fr from './locales/fr.po'; const say = new Say({ locales: ['en', 'fr'], - messages: { - en: await import('./locales/en/messages.json').then((m) => m.default), - fr: await import('./locales/fr/messages.json').then((m) => m.default), - }, + messages: { en, fr }, }); export const withSay = unstable_createWithSay(say); diff --git a/examples/nextjs/src/locales/en/messages.po b/examples/nextjs/src/locales/en.po similarity index 61% rename from examples/nextjs/src/locales/en/messages.po rename to examples/nextjs/src/locales/en.po index d3e3491..895fd56 100644 --- a/examples/nextjs/src/locales/en/messages.po +++ b/examples/nextjs/src/locales/en.po @@ -1,8 +1,15 @@ msgid "" msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language: en\n" +"Language-Team: \n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: en\n" +"Plural-Forms: \n" "X-Generator: saykit\n" msgid "Hello from the <0>{region}" diff --git a/examples/nextjs/src/locales/fr/messages.po b/examples/nextjs/src/locales/fr.po similarity index 62% rename from examples/nextjs/src/locales/fr/messages.po rename to examples/nextjs/src/locales/fr.po index f0e90df..410089b 100644 --- a/examples/nextjs/src/locales/fr/messages.po +++ b/examples/nextjs/src/locales/fr.po @@ -1,8 +1,15 @@ msgid "" msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language: fr\n" +"Language-Team: \n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: fr\n" +"Plural-Forms: \n" "X-Generator: saykit\n" msgid "Hello from the <0>{region}" diff --git a/examples/tanstack-start/package.json b/examples/tanstack-start/package.json index 5bfc775..a63b023 100644 --- a/examples/tanstack-start/package.json +++ b/examples/tanstack-start/package.json @@ -6,7 +6,6 @@ "scripts": { "check": "tsc --noEmit", "extract": "saykit extract", - "compile": "saykit compile", "build": "vite build && tsc --noEmit", "dev": "vite dev" }, diff --git a/examples/tanstack-start/saykit.config.ts b/examples/tanstack-start/saykit.config.ts index b8404f8..554f08b 100644 --- a/examples/tanstack-start/saykit.config.ts +++ b/examples/tanstack-start/saykit.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from '@saykit/config'; -import createPoFormatter from '@saykit/format-po'; -import createJsTransformer from '@saykit/transform-js'; -import createJsxTransformer from '@saykit/transform-jsx'; +import po from '@saykit/format-po'; +import js from '@saykit/transform-js'; +import jsx from '@saykit/transform-jsx'; export default defineConfig({ sourceLocale: 'en', @@ -9,9 +9,9 @@ export default defineConfig({ buckets: [ { include: ['src/**/*.{ts,tsx}'], - output: 'src/locales/{locale}/messages.{extension}', - formatter: createPoFormatter(), - transformer: [createJsTransformer(), createJsxTransformer()], + output: 'src/locales/{locale}.{extension}', + formatter: po(), + transformer: [js(), jsx()], }, ], }); diff --git a/examples/tanstack-start/src/i18n.ts b/examples/tanstack-start/src/i18n.ts index 18ff8ba..010801f 100644 --- a/examples/tanstack-start/src/i18n.ts +++ b/examples/tanstack-start/src/i18n.ts @@ -3,8 +3,8 @@ import { Say } from 'saykit'; const say = new Say({ locales: ['en', 'fr'], messages: { - en: await import('./locales/en/messages.json').then((m) => m.default), - fr: await import('./locales/fr/messages.json').then((m) => m.default), + en: await import('./locales/en.po').then((m) => m.default), + fr: await import('./locales/fr.po').then((m) => m.default), }, }); diff --git a/examples/tanstack-start/src/locales/en/messages.po b/examples/tanstack-start/src/locales/en.po similarity index 61% rename from examples/tanstack-start/src/locales/en/messages.po rename to examples/tanstack-start/src/locales/en.po index 645d445..2097d78 100644 --- a/examples/tanstack-start/src/locales/en/messages.po +++ b/examples/tanstack-start/src/locales/en.po @@ -1,8 +1,15 @@ msgid "" msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language: en\n" +"Language-Team: \n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: en\n" +"Plural-Forms: \n" "X-Generator: saykit\n" msgid "Count: {count}" diff --git a/examples/tanstack-start/src/locales/fr/messages.po b/examples/tanstack-start/src/locales/fr.po similarity index 62% rename from examples/tanstack-start/src/locales/fr/messages.po rename to examples/tanstack-start/src/locales/fr.po index b325550..bae7c8d 100644 --- a/examples/tanstack-start/src/locales/fr/messages.po +++ b/examples/tanstack-start/src/locales/fr.po @@ -1,8 +1,15 @@ msgid "" msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language: fr\n" +"Language-Team: \n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: fr\n" +"Plural-Forms: \n" "X-Generator: saykit\n" msgid "Count: {count}" diff --git a/packages/config/src/commands/build.ts b/packages/config/src/commands/build.ts deleted file mode 100644 index 7a17587..0000000 --- a/packages/config/src/commands/build.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Command } from '@commander-js/extra-typings'; -import { resolveConfig } from '~/features/loader/resolve.js'; -import Logger from '~/features/logger.js'; -import { BucketBuildWorker } from '~/features/workers/build-worker.js'; - -export default new Command() - .name('build') - .description('') - .option('-v, --verbose', 'enable verbose logging', false) - .option('-q, --quiet', 'suppress all logging', false) - // .option('-w, --watch', 'watch source files for changes', false) - .action(async (options) => { - const config = await resolveConfig('saykit'); - const logger = new Logger(options); - logger.header('🏗 Building Messages'); - - const tasks = config.buckets.map(async (bucket) => { - const worker = new BucketBuildWorker(config, bucket, logger); - await worker.buildAll(); - // if (options.watch) await worker.watch(); - }); - - await Promise.all(tasks); - }); diff --git a/packages/config/src/commands/compile.ts b/packages/config/src/commands/compile.ts deleted file mode 100644 index 97568f0..0000000 --- a/packages/config/src/commands/compile.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Command } from '@commander-js/extra-typings'; -import { resolveConfig } from '~/features/loader/resolve.js'; -import Logger from '~/features/logger.js'; -import { BucketCompileWorker } from '~/features/workers/compile-worker.js'; - -export default new Command('compile') - .description('Compile translations into runtime-ready locale files') - .option('-v, --verbose', 'enable verbose logging', false) - .option('-q, --quiet', 'suppress all logging', false) - .action(async (options) => { - const config = await resolveConfig('saykit'); - const logger = new Logger(options); - logger.header('🛠 Compiling Translations'); - - const tasks = config.buckets.map(async (bucket) => { - const worker = new BucketCompileWorker(config, bucket, logger); - await worker.compileAll(); - }); - - await Promise.all(tasks); - }); diff --git a/packages/config/src/commands/extract.ts b/packages/config/src/commands/extract.ts index b2c1c2c..084df8a 100644 --- a/packages/config/src/commands/extract.ts +++ b/packages/config/src/commands/extract.ts @@ -1,5 +1,5 @@ import { Command } from '@commander-js/extra-typings'; -import { resolveConfig } from '~/features/loader/resolve.js'; +import { resolveConfig } from '~/features/loader/index.js'; import Logger from '~/features/logger.js'; import { BucketExtractWorker } from '~/features/workers/extract-worker.js'; @@ -7,21 +7,13 @@ export default new Command('extract') .description('Extract messages from source files') .option('-v, --verbose', 'enable verbose logging', false) .option('-q, --quiet', 'suppress all logging', false) + .option('-w, --watch', 'watch source files for changes', false) .action(async (options) => { - const config = await resolveConfig('saykit'); + const config = resolveConfig(); const logger = new Logger(options); logger.header('🛠 Extracting Messages'); - const tasks = config.buckets.map(async (bucket) => { - const worker = new BucketExtractWorker(config, bucket, logger); - await worker.scanAll(); - await worker.writeAll(); - }); - - const results = await Promise.allSettled(tasks); - const rejections = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected'); - if (rejections.length > 0) { - const errors = rejections.map((r) => r.reason).join('\n'); - throw new Error(`Bucket extraction failed:\n${errors}`); - } + const workers = config.buckets.map((b) => new BucketExtractWorker(config, b, logger)); + await Promise.all(workers.map((w) => w.scan().then(() => w.write()))); + if (options.watch) await Promise.all(workers.map((w) => w.watch())); }); diff --git a/packages/config/src/commands/index.ts b/packages/config/src/commands/index.ts index 190bc53..d0ad69d 100644 --- a/packages/config/src/commands/index.ts +++ b/packages/config/src/commands/index.ts @@ -1,8 +1,6 @@ #!/usr/bin/env node import { program } from '@commander-js/extra-typings'; -import build from './build.js'; -import compile from './compile.js'; import extract from './extract.js'; program @@ -10,6 +8,4 @@ program .helpOption('-h, --help', 'Display help for command') .helpCommand('help [command]', 'Display help for command') .addCommand(extract) - .addCommand(compile) - .addCommand(build) .parse(); diff --git a/packages/config/src/features/catalogue/path.ts b/packages/config/src/features/catalogue/path.ts index 3c11021..51ee3dd 100644 --- a/packages/config/src/features/catalogue/path.ts +++ b/packages/config/src/features/catalogue/path.ts @@ -11,3 +11,8 @@ export function expandBucketOutputPath( .replaceAll('{extension}', extension.slice(1)); return resolve(outputMessageTemplate); } + +export function expandBucketOutputIgnoreDirectory(bucket: Bucket) { + const [prefix] = bucket.output.split('{locale}'); + return resolve(prefix || '.'); +} diff --git a/packages/config/src/features/catalogue/storage.ts b/packages/config/src/features/catalogue/storage.ts index f6bb175..63b3e4b 100644 --- a/packages/config/src/features/catalogue/storage.ts +++ b/packages/config/src/features/catalogue/storage.ts @@ -1,7 +1,12 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'; -import { dirname } from 'node:path'; +import { dirname, join } from 'node:path'; import type { Bucket, Message } from '~/shapes.js'; -import { expandBucketOutputPath } from './path.js'; +import { expandBucketOutputIgnoreDirectory, expandBucketOutputPath } from './path.js'; + +const DECLARATION_CONTENT = ` +declare const translations: Record; +export default translations; +`.trim(); export async function readCatalogueMessages( bucket: Bucket, @@ -10,7 +15,7 @@ export async function readCatalogueMessages( ) { const content = await readFile(path, 'utf8').catch(() => ''); if (!content) return []; - return bucket.formatter.parse(content, { locale }); + return bucket.formatter.parse(content); } export async function writeCatalogueMessages( @@ -19,7 +24,18 @@ export async function writeCatalogueMessages( messages: Message[], path = expandBucketOutputPath(bucket, locale), ) { - const content = bucket.formatter.stringify(messages, { locale }); + const existingContent = await readFile(path, 'utf8').catch(() => undefined); + const catalogueContent = bucket.formatter.stringify(messages, { locale, existingContent }); + const declarationPath = `${path}.d.ts`; + const ignoreDirectory = expandBucketOutputIgnoreDirectory(bucket); + const ignorePath = join(ignoreDirectory, '.gitignore'); + const ignoreContent = `.gitignore\n*.${bucket.formatter.extension.slice(1)}.d.ts`; + await mkdir(dirname(path), { recursive: true }); - await writeFile(path, content); + await mkdir(ignoreDirectory, { recursive: true }); + await Promise.all([ + writeFile(path, catalogueContent), + writeFile(declarationPath, DECLARATION_CONTENT), + writeFile(ignorePath, ignoreContent), + ]); } diff --git a/packages/config/src/features/loader/resolve.ts b/packages/config/src/features/loader/resolve.ts index d43d678..095cab8 100644 --- a/packages/config/src/features/loader/resolve.ts +++ b/packages/config/src/features/loader/resolve.ts @@ -1,13 +1,8 @@ import { extname } from 'node:path'; -import { Configuration } from '~/shapes.js'; +import type { Config } from '~/shapes.js'; import { findConfigFile } from './files.js'; import { configLoaders } from './module.js'; -function unwrapNamedConfig(config: object, name: string) { - if (!(name in config) || !config[name as keyof object]) return config; - return config[name as keyof object]; -} - export function resolveConfig(name = 'saykit') { const file = findConfigFile(name, process.cwd()); if (!file) throw new Error(`Could not find config file for "${name}"`); @@ -18,10 +13,6 @@ export function resolveConfig(name = 'saykit') { let config = load(file.id, file.content); if (!config || typeof config !== 'object') throw new Error(`Invalid config file for "${name}"`); - config = unwrapNamedConfig(config, name); - - const result = Configuration.safeParse(config); - if (result.error) throw new Error(`Invalid config file for "${name}"`, { cause: result.error }); - return result.data; + return config as Config; } diff --git a/packages/config/src/features/messages/hash.ts b/packages/config/src/features/messages/hash.ts index 75c7ada..fcb132c 100644 --- a/packages/config/src/features/messages/hash.ts +++ b/packages/config/src/features/messages/hash.ts @@ -7,5 +7,9 @@ export function generateHash(input: string, context?: string) { const elements = result.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) || []; const bytes = Uint8Array.from(elements); - return btoa(String.fromCharCode(...bytes)).slice(0, 6); + return btoa(String.fromCharCode(...bytes)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, '') + .slice(0, 6); } diff --git a/packages/config/src/features/runtime/translations.ts b/packages/config/src/features/runtime/translations.ts deleted file mode 100644 index e13b1a5..0000000 --- a/packages/config/src/features/runtime/translations.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { mkdir, writeFile } from 'node:fs/promises'; -import { dirname } from 'node:path'; -import { expandBucketOutputPath } from '~/features/catalogue/path.js'; -import { generateHash } from '~/features/messages/hash.js'; -import type { Bucket, Configuration, Message } from '~/shapes.js'; -import { readCatalogueMessages } from '../catalogue/storage.js'; - -function getFallbackLocaleChain(config: Configuration, locale: string) { - return [...(config.fallbackLocales?.[locale] ?? []), config.sourceLocale]; -} - -export async function hydrateTranslations( - cache: Map>, - config: Configuration, - bucket: Bucket, - locale: string, - messages: Message[], -) { - if (cache.has(locale)) return cache.get(locale)!; - const translations = await applyFallbackTranslations(cache, config, bucket, locale, messages); - cache.set(locale, translations); - return translations; -} - -export async function applyFallbackTranslations( - cache: Map>, - config: Configuration, - bucket: Bucket, - locale: string, - messages: Message[], -) { - const fallbackLocales = getFallbackLocaleChain(config, locale); - const translations: Record = {}; - - for (const message of messages) { - const key = message.id ?? generateHash(message.message, message.context); - - if (message.translation) { - translations[key] = message.translation; - continue; - } - - for (const fallbackLocale of fallbackLocales) { - const fallbackMessages = // - await readCatalogueMessages(bucket, fallbackLocale); - const fallbackTranslations = // - await hydrateTranslations(cache, config, bucket, fallbackLocale, fallbackMessages); - - if (fallbackTranslations[key]) { - translations[key] = fallbackTranslations[key]; - break; - } - } - } - - return translations; -} - -export async function writeRuntimeTranslations( - bucket: Bucket, - locale: string, - translations: Record, - path = expandBucketOutputPath(bucket, locale, '.json'), -) { - const content = JSON.stringify(translations, null, 2); - await mkdir(dirname(path), { recursive: true }); - await writeFile(path, content); -} diff --git a/packages/config/src/features/workers/build-worker.ts b/packages/config/src/features/workers/build-worker.ts deleted file mode 100644 index 2ccf442..0000000 --- a/packages/config/src/features/workers/build-worker.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { join } from 'node:path'; -import type Logger from '~/features/logger.js'; -import { watchDebounced } from '~/features/watch.js'; -import type { Bucket, Configuration } from '~/shapes.js'; -import { BucketCompileWorker } from './compile-worker.js'; -import { BucketExtractWorker } from './extract-worker.js'; -import { BucketWorker, normalisePathForLogs } from './shared.js'; - -export class BucketBuildWorker extends BucketWorker { - extract: BucketExtractWorker; - compile: BucketCompileWorker; - - constructor(config: Configuration, bucket: Bucket, logger: Logger) { - const parameters = [config, bucket, logger] as const; - super(...parameters); - this.extract = new BucketExtractWorker(...parameters); - this.compile = new BucketCompileWorker(...parameters); - } - - async buildAll() { - await this.extract.scanAll(); - await this.extract.writeAll(); - await this.compile.compileAll(); - } - - async watch() { - this.logger.header(`👀 Watching bucket for changes: ${this.bucket.include}`); - - for await (const event of watchDebounced('.', { recursive: true })) { - if (!event.filename || !this.bucket.match(event.filename)) continue; - - const filePath = join(process.cwd(), event.filename); - const changed = await this.extract.updatePath(filePath); - - if (changed) { - this.logger.info(`Recompiling due to changes in ${normalisePathForLogs(filePath)}`); - await this.compile.compileAll(); - } - } - } -} diff --git a/packages/config/src/features/workers/compile-worker.ts b/packages/config/src/features/workers/compile-worker.ts deleted file mode 100644 index 18f2b73..0000000 --- a/packages/config/src/features/workers/compile-worker.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { readCatalogueMessages } from '~/features/catalogue/storage.js'; -import { hydrateTranslations, writeRuntimeTranslations } from '~/features/runtime/translations.js'; -import { BucketWorker } from './shared.js'; - -export class BucketCompileWorker extends BucketWorker { - #translationsByLocale = new Map>(); - - async compileAll() { - this.logger.info(`Compiling bucket: ${this.bucket.include}`); - - for (const locale of this.config.locales) { - await this.compileLocale(locale); - } - - this.logger.success(`Compilation complete for bucket: ${this.bucket.include}`); - } - - async compileLocale(locale: string) { - this.logger.step(`Compiling locale: ${locale}`); - - const messages = await readCatalogueMessages(this.bucket, locale); - const translations = await hydrateTranslations( - this.#translationsByLocale, - this.config, - this.bucket, - locale, - messages, - ); - - this.logger.step(`Writing runtime file for ${locale}`); - await writeRuntimeTranslations(this.bucket, locale, translations); - } -} diff --git a/packages/config/src/features/workers/extract-worker.ts b/packages/config/src/features/workers/extract-worker.ts index 48a542b..0c97922 100644 --- a/packages/config/src/features/workers/extract-worker.ts +++ b/packages/config/src/features/workers/extract-worker.ts @@ -1,14 +1,14 @@ -import { extractMessagesFromFile } from '~/features/catalogue/extractor.js'; -import { mergeExtractedMessages, reconcileLocaleMessages } from '~/features/catalogue/merge.js'; -import { readCatalogueMessages, writeCatalogueMessages } from '~/features/catalogue/storage.js'; -import { globBucket } from '~/features/watch.js'; -import type { Message } from '~/shapes.js'; -import { BucketWorker, normalisePathForLogs } from './shared.js'; +import { join } from 'node:path'; +import type { Message } from '~/shapes'; +import { extractMessagesFromFile } from '../catalogue/extractor'; +import { mergeExtractedMessages, reconcileLocaleMessages } from '../catalogue/merge'; +import { readCatalogueMessages, writeCatalogueMessages } from '../catalogue/storage'; +import { globBucket, watchDebounced } from '../watch'; +import { BucketWorker, normalisePathForLogs } from './shared'; export class BucketExtractWorker extends BucketWorker { #indexedMessagesByPath = new Map(); - - get messages(): Message[] { + get #indexedMessages() { return Array.from(this.#indexedMessagesByPath.values()).flat(); } @@ -31,18 +31,18 @@ export class BucketExtractWorker extends BucketWorker { return true; } - async scanAll() { + async scan() { this.logger.info(`Scanning bucket: ${this.bucket.include}`); const paths = await globBucket(this.bucket); this.logger.step(`Found ${paths.length} file(s)`); - await Promise.all(paths.map((path) => this.#indexPath(path))); + await Promise.all(paths.map((p) => this.#indexPath(p))); - this.logger.info(`Total extracted messages: ${this.messages.length}`); + this.logger.info(`Total extracted messages: ${this.#indexedMessages.length}`); } - async writeAll() { - const mergedMessages = mergeExtractedMessages(this.messages); + async write() { + const mergedMessages = mergeExtractedMessages(this.#indexedMessages); this.logger.info(`Writing ${mergedMessages.length} messages to locales`); for (const locale of this.config.locales) { @@ -60,9 +60,19 @@ export class BucketExtractWorker extends BucketWorker { this.logger.success(`Extraction complete for bucket: ${this.bucket.include}`); } - async updatePath(path: string) { + async update(path: string) { const changed = await this.#indexPath(path); - if (changed) await this.writeAll(); + if (changed) await this.write(); return changed; } + + async watch() { + this.logger.header(`👀 Watching bucket for changes: ${this.bucket.include}`); + + for await (const event of watchDebounced('.', { recursive: true })) { + if (!event.filename || !this.bucket.match(event.filename)) continue; + const path = join(process.cwd(), event.filename); + await this.update(path); + } + } } diff --git a/packages/config/src/features/workers/index.ts b/packages/config/src/features/workers/index.ts deleted file mode 100644 index 6c472ee..0000000 --- a/packages/config/src/features/workers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { BucketBuildWorker } from './build-worker.js'; -export { BucketCompileWorker } from './compile-worker.js'; -export { BucketExtractWorker } from './extract-worker.js'; diff --git a/packages/config/src/features/workers/shared.ts b/packages/config/src/features/workers/shared.ts index 50d2ae2..f9c90a2 100644 --- a/packages/config/src/features/workers/shared.ts +++ b/packages/config/src/features/workers/shared.ts @@ -1,17 +1,17 @@ import { relative } from 'node:path'; import type Logger from '~/features/logger.js'; -import type { Bucket, Configuration } from '~/shapes.js'; +import type { Bucket, Config } from '~/shapes.js'; export function normalisePathForLogs(path: string) { return relative(process.cwd(), path).replaceAll('\\', '/'); } export abstract class BucketWorker { - protected config: Configuration; + protected config: Config; protected bucket: Bucket; protected logger: Logger; - constructor(config: Configuration, bucket: Bucket, logger: Logger) { + constructor(config: Config, bucket: Bucket, logger: Logger) { this.config = config; this.bucket = bucket; this.logger = logger; diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 4e9e856..444ebc9 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,8 +1,8 @@ import type { input } from 'zod'; -import { Configuration } from './shapes.js'; +import { Config } from './shapes.js'; -export function defineConfig>(config: C) { - return Configuration.parse(config); +export function defineConfig>(config: C) { + return Config.parse(config); } export type * from './shapes.js'; diff --git a/packages/config/src/shapes.ts b/packages/config/src/shapes.ts index c08a550..f45a81d 100644 --- a/packages/config/src/shapes.ts +++ b/packages/config/src/shapes.ts @@ -13,12 +13,10 @@ export type Message = z.infer; export const Formatter = z.object({ extension: z.templateLiteral(['.', z.string()]), - parse: z.custom<(content: string, context: { locale: string }) => Message[]>( - (v) => typeof v === 'function', - ), - stringify: z.custom<(messages: Message[], context: { locale: string }) => string>( - (v) => typeof v === 'function', - ), + parse: z.custom<(content: string) => Message[]>((v) => typeof v === 'function'), + stringify: z.custom< + (messages: Message[], context: { locale: string; existingContent?: string }) => string + >((v) => typeof v === 'function'), }); export type Formatter = z.infer; @@ -51,17 +49,21 @@ export const Bucket = z .transform((v) => ({ ...v, match: picomatch(v.include, { ignore: v.exclude }) as (id: string) => boolean, + output: Object.assign(v.output, { + match: picomatch( + v.output.replace('{locale}', '*').replace('{extension}', v.formatter.extension.slice(1)), + ), + }), })); export type Bucket = z.infer; -export const Configuration = z +export const Config = z .object({ sourceLocale: z.string(), locales: z.tuple([z.string()], z.string()), - fallbackLocales: z.record(z.string(), z.string().array()).optional(), buckets: Bucket.array(), }) .refine((config) => config.sourceLocale === config.locales[0], { error: 'sourceLocale must be the same as locales[0]', }); -export type Configuration = z.infer; +export type Config = z.infer; diff --git a/packages/format-po/src/formatter.ts b/packages/format-po/src/formatter.ts index 316a312..a50106f 100644 --- a/packages/format-po/src/formatter.ts +++ b/packages/format-po/src/formatter.ts @@ -19,12 +19,9 @@ function createPoFormatter(options: FormatterOptions = {}): Formatter { return { extension: '.po', - parse(content, context) { + parse(content) { const po = PO.parse(content); - if (po.headers.Language !== context.locale) - throw new Error('PO file locale does not match the expected locale'); - return po.items.map((item) => { const id = item.extractedComments.find((c) => c.startsWith('id:'))?.slice(3); const comments = item.extractedComments // @@ -42,13 +39,17 @@ function createPoFormatter(options: FormatterOptions = {}): Formatter { }); }, - stringify(messages, context) { + stringify(messages, { locale, existingContent }) { const po = new PO(); + if (existingContent) { + const existing = PO.parse(existingContent); + Object.assign(po.headers, existing.headers); + } - po.headers['Content-Type'] = 'text/plain; charset=UTF-8'; - po.headers['Content-Transfer-Encoding'] = '8bit'; - po.headers.Language = context.locale; - po.headers['X-Generator'] = 'saykit'; + po.headers['Content-Type'] ||= 'text/plain; charset=UTF-8'; + po.headers['Content-Transfer-Encoding'] ||= '8bit'; + po.headers['Language'] ||= locale; + po.headers['X-Generator'] ||= 'saykit'; for (const message of messages.sort((a, b) => a.message.localeCompare(b.message))) { const item = new PO.Item(); diff --git a/packages/plugin-babel/src/index.ts b/packages/plugin-babel/src/index.ts index 53da19d..e49236c 100644 --- a/packages/plugin-babel/src/index.ts +++ b/packages/plugin-babel/src/index.ts @@ -1,6 +1,8 @@ -import { relative } from 'node:path'; -import { type PluginObj, type parse as Parse } from '@babel/core'; +import { readFileSync } from 'node:fs'; +import { dirname, relative, resolve } from 'node:path'; +import { types as t, type PluginObj, type parse as Parse } from '@babel/core'; import { resolveConfig } from '@saykit/config/features/loader'; +import { generateHash } from '@saykit/config/features/messages'; declare module '@babel/core' { interface PluginObj { @@ -13,7 +15,7 @@ export default (): PluginObj => { return { name: 'saykit', - visitor: {}, + parserOverride(code, opts, parse) { const id_ = opts.sourceFileName; if (!id_ || id_.includes('node_modules')) return parse(code, opts); @@ -24,5 +26,43 @@ export default (): PluginObj => { return parse(transformed, opts); }, + + visitor: { + // TODO: This is fragile, it does not work with dynamic imports, document this + ImportDeclaration(path, state) { + const importee = path.node.source.value; + if (!importee.startsWith('.')) return; + const importer = state.filename ?? state.file.opts.filename; + if (!importer) return; + const id_ = resolve(dirname(importer), importee); + + const id = relative(process.cwd(), id_).replaceAll('\\', '/').split('?')[0]!; + const bucket = config.buckets.find((b) => b.output.match(id)); + if (!bucket) return; + + const specifier = path.node.specifiers.find((s) => s.type === 'ImportDefaultSpecifier'); + if (!specifier) + throw path.buildCodeFrameError('SayKit inline imports require a default import'); + + const content = readFileSync(id, 'utf8'); + const messages = bucket.formatter.parse(content); + const entries = messages.map( + (m) => [m.id || generateHash(m.message, m.context), m.translation || m.message] as const, + ); + + path.replaceWith( + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier(specifier.local.name), + t.objectExpression( + entries.map(([key, value]) => + t.objectProperty(t.stringLiteral(key), t.stringLiteral(value)), + ), + ), + ), + ]), + ); + }, + }, }; }; diff --git a/packages/plugin-unplugin/src/index.ts b/packages/plugin-unplugin/src/index.ts index 9733ec6..9b79813 100644 --- a/packages/plugin-unplugin/src/index.ts +++ b/packages/plugin-unplugin/src/index.ts @@ -1,5 +1,7 @@ +import { readFile } from 'node:fs/promises'; import { relative } from 'node:path'; import { resolveConfig } from '@saykit/config/features/loader'; +import { generateHash } from '@saykit/config/features/messages'; import { createUnplugin } from 'unplugin'; export default createUnplugin((_options?: never) => { @@ -8,6 +10,7 @@ export default createUnplugin((_options?: never) => { return { name: 'saykit', enforce: 'pre', + transform: { filter: { id: { exclude: /node_modules/ } }, handler: (code, id_) => { @@ -16,5 +19,23 @@ export default createUnplugin((_options?: never) => { return bucket?.transformer.transform(code, id) ?? code; }, }, + + load: { + // TODO: Can bucket output be used in this filter? + filter: { id: { exclude: /node_modules/ } }, + handler: async (id_) => { + const id = relative(process.cwd(), id_).replaceAll('\\', '/').split('?')[0]!; + const bucket = config.buckets.find((b) => b.output.match(id)); + if (!bucket) return; + + const content = await readFile(id, 'utf8'); + const messages = bucket.formatter.parse(content); + const entries = messages.map( + (m) => [m.id || generateHash(m.message, m.context), m.translation || m.message] as const, + ); + + return `export default ${JSON.stringify(Object.fromEntries(entries))}`; + }, + }, }; }); diff --git a/website/app/(home)/layout.tsx b/website/app/(home)/layout.tsx index 851cd91..89a06f2 100644 --- a/website/app/(home)/layout.tsx +++ b/website/app/(home)/layout.tsx @@ -1,7 +1,7 @@ -import { HomeLayout } from 'fumadocs-ui/layouts/home'; -import type { ReactNode } from 'react'; -import { baseOptions } from '@/lib/layout.shared'; - -export default function Layout({ children }: { children: ReactNode }) { - return {children}; -} +import { HomeLayout } from 'fumadocs-ui/layouts/home'; +import type { ReactNode } from 'react'; +import { baseOptions } from '@/lib/layout.shared'; + +export default function Layout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/website/app/(home)/page.tsx b/website/app/(home)/page.tsx index 8022d47..64bf875 100644 --- a/website/app/(home)/page.tsx +++ b/website/app/(home)/page.tsx @@ -1,278 +1,266 @@ -import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock'; -import { - ArrowRight, - CheckCircle2, - Code2, - Languages, - PackageCheck, - Rocket, - ScanSearch, - SquareTerminal, -} from 'lucide-react'; -import type { Metadata } from 'next'; -import Link from 'next/link'; -import type { ReactNode } from 'react'; -import { createHighlighter } from 'shiki'; - -const frameworks = ['React', 'Next.js', 'Expo', 'Carbon']; - -const workflow = [ - { - name: 'Define', - icon: Code2, - description: 'Author messages inline with tagged templates or React components.', - }, - { - name: 'Extract', - icon: ScanSearch, - description: 'Scan source files and collect ICU-ready messages into translation files.', - }, - { - name: 'Translate', - icon: Languages, - description: 'Hand translators clean PO files with comments, context, and stable identifiers.', - }, - { - name: 'Compile', - icon: PackageCheck, - description: 'Build locale catalogs into runtime-ready message bundles for your app.', - }, - { - name: 'Deploy', - icon: Rocket, - description: - 'Ship small runtime helpers while keeping extraction and transforms in build time.', - }, -] satisfies { name: string; icon: React.ElementType; description: string }[]; - -const features = [ - 'Compile-time extraction from JS, TS, JSX, and TSX', - 'ICU MessageFormat support for plurals, ordinals, and select', - 'Framework-agnostic core runtime with adapters where needed', - 'Typed config and CLI for extract, compile, and build', -]; - -const CODE_EXAMPLE = `import { Say } from '@saykit/react'; - -export function Inbox({ count }: { count: number }) { - return ( -
-

Inbox

-

- -

-
- ); -}`; - -const SHIKI_THEMES = { dark: 'github-dark', light: 'github-light' } as const; - -export const metadata: Metadata = { - title: 'SayKit', - description: 'Compile-time i18n for JavaScript, TypeScript, React, Next.js, Expo, and Carbon.', -}; - -function Pill({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} - -export default async function HomePage() { - const highlighter = await createHighlighter({ - themes: Object.values(SHIKI_THEMES), - langs: ['tsx'], - }); - - const highlight = (code: string, lang: 'tsx') => - highlighter.codeToHtml(code, { lang, themes: SHIKI_THEMES, defaultColor: false }); - - const codeHtml = highlight(CODE_EXAMPLE, 'tsx'); - - return ( -
-
- - {/* Hero */} -
-
- Compile-time i18n for modern TypeScript apps - -
-

- Write messages in code. Ship translations with almost no runtime baggage. -

-

- SayKit is a framework-agnostic i18n toolkit built around compile-time extraction, - typed configuration, and small runtime primitives. It keeps authoring ergonomic for - developers and translation flows clean for everyone else. -

-
- -
- - Read the docs - - - View on GitHub - -
- -
- {frameworks.map((f) => ( - {f} - ))} -
-
- - {/* Code cards */} -
- } - title="Code example" - allowCopy={false} - className="rounded-3xl m-0" - > -
-          
-
-          
-
-

- Why it feels good -

-
    - {features.map((f) => ( -
  • - - {f} -
  • - ))} -
-
- -
-

- Terminal -

-
-

- $ - saykit extract -

-

✓ 2 messages extracted

-

→ locales/en.po

-

- $ - saykit compile -

-

✓ 3 locales compiled

-

→ locales/en.json, fr.json, ja.json

-
-
-
-
-
- - {/* Workflow */} -
-
-
-

- End to end workflow -

-

- A simple translation pipeline that stays close to your codebase -

-

- SayKit is designed for the whole path from authoring to deployment, without pushing - app teams into a heavyweight platform. -

-
- -
- {workflow.map(({ name, icon: Icon, description }, i) => ( -
-
-
- -
- - {String(i + 1).padStart(2, '0')} - -
-
-

{name}

-

{description}

-
-
- ))} -
-
-
- - {/* CTA */} -
-
-

- Ready to try it? -

-
-

- Start with the docs, then adapt the example closest to your stack. -

-

- The repo already includes examples for Next.js, TanStack Start, and Carbon, and the - core package is intended to stay useful even outside framework-specific adapters. -

-
-
- - Installation - - - View integrations - -
-
-
-
- ); -} +import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock'; +import { + ArrowRight, + CheckCircle2, + Code2, + Languages, + Rocket, + ScanSearch, + SquareTerminal, +} from 'lucide-react'; +import type { Metadata } from 'next'; +import Link from 'next/link'; +import type { ReactNode } from 'react'; +import { createHighlighter } from 'shiki'; + +const frameworks = ['React', 'Next.js', 'TanStack Start', 'React Native', 'Expo', 'Carbon']; + +const workflow = [ + { + name: 'Define', + icon: Code2, + description: 'Author messages inline with tagged templates or React components.', + }, + { + name: 'Extract', + icon: ScanSearch, + description: 'Scan source files and collect ICU-ready messages into translation files.', + }, + { + name: 'Translate', + icon: Languages, + description: + 'Hand translators clean, structured translation files with comments, context, and stable identifiers.', + }, + { + name: 'Deploy', + icon: Rocket, + description: + 'Import translation files directly into your app, no compile step, no extra build-time transform. Ship small runtime helpers and keep extraction at dev time.', + }, +] satisfies { name: string; icon: React.ElementType; description: string }[]; + +const features = [ + 'Compile-time extraction from JS, TS, JSX, and TSX', + 'ICU MessageFormat support for plurals, ordinals, and select', + 'Framework-agnostic core runtime with adapters where needed', + 'Import translation files directly, no compile step, no build-time transform', + 'Typed config and CLI with watch mode for instant feedback', +]; + +const CODE_EXAMPLE = `import { Say } from '@saykit/react'; + +export function Inbox({ count }: { count: number }) { + return ( +
+

Inbox

+

+ +

+
+ ); +}`; + +const SHIKI_THEMES = { dark: 'github-dark', light: 'github-light' } as const; + +export const metadata: Metadata = { + title: 'SayKit', + description: 'Compile-time i18n for JavaScript, TypeScript, React, Next.js, Expo, and Carbon.', +}; + +function Pill({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +const highlighter = await createHighlighter({ + themes: Object.values(SHIKI_THEMES), + langs: ['tsx'], +}); +const highlight = (code: string, lang: 'tsx') => + highlighter.codeToHtml(code, { lang, themes: SHIKI_THEMES, defaultColor: false }); +const codeHtml = highlight(CODE_EXAMPLE, 'tsx'); + +export default async function HomePage() { + return ( +
+
+ + {/* Hero */} +
+
+ Compile-time i18n for modern TypeScript apps + +
+

+ Write messages in code. Ship translations with almost no runtime baggage. +

+

+ SayKit is a framework-agnostic i18n toolkit built around compile-time extraction, + typed configuration, and small runtime primitives. It keeps authoring ergonomic for + developers and translation flows clean for everyone else. +

+
+ +
+ + Read the docs + + + View on GitHub + +
+ +
+ {frameworks.map((f) => ( + {f} + ))} +
+
+ + {/* Code cards */} +
+ } + title="Code example" + allowCopy={false} + className="rounded-3xl m-0" + > +
+          
+
+          
+
+

+ Why it feels good +

+
    + {features.map((f) => ( +
  • + + {f} +
  • + ))} +
+
+ +
+

+ Terminal +

+
+

+ $ + saykit extract +

+

✓ 2 messages extracted

+

→ locales/en, fr, ja

+
+
+
+
+
+ + {/* Workflow */} +
+
+
+

+ End to end workflow +

+

+ A simple translation pipeline that stays close to your codebase +

+

+ SayKit is designed for the whole path from authoring to deployment, without pushing + app teams into a heavyweight platform. +

+
+ +
+ {workflow.map(({ name, icon: Icon, description }, i) => ( +
+
+
+ +
+ + {String(i + 1).padStart(2, '0')} + +
+
+

{name}

+

{description}

+
+
+ ))} +
+
+
+ + {/* CTA */} +
+
+

+ Ready to try it? +

+
+

+ Start with the docs, then adapt the example closest to your stack. +

+

+ The repo already includes examples for Next.js, TanStack Start, and Carbon, and the + core package is intended to stay useful even outside framework-specific adapters. +

+
+
+ + Installation + + + View integrations + +
+
+
+
+ ); +} diff --git a/website/content/core-concepts/configuration.mdx b/website/content/core-concepts/configuration.mdx index 109ad01..64bed30 100644 --- a/website/content/core-concepts/configuration.mdx +++ b/website/content/core-concepts/configuration.mdx @@ -11,8 +11,8 @@ Create a `saykit.config.ts` in your project root: ```ts title='saykit.config.ts' import { defineConfig } from '@saykit/config'; -import createPoFormatter from '@saykit/format-po'; -import createJsTransformer from '@saykit/transform-js'; +import po from '@saykit/format-po'; +import js from '@saykit/transform-js'; export default defineConfig({ sourceLocale: 'en', @@ -21,8 +21,8 @@ export default defineConfig({ { include: ['src/**/*.{ts,tsx}'], output: 'src/locales/{locale}.{extension}', - transformer: createJsTransformer(), - formatter: createPoFormatter(), + transformer: js(), + formatter: po(), }, ], }); @@ -49,11 +49,6 @@ The configuration is type-checked using Zod schemas at runtime: type: '[string, ...string[]]', required: true, }, - fallbackLocales: { - description: - "Define fallback chains for locales. If a message isn't found in a locale, SayKit will check the fallback locales in order.", - type: 'Record', - }, buckets: { description: 'Array of bucket configurations that define which files to extract from and where to output translations.', @@ -152,12 +147,6 @@ Transformers control how messages are extracted and transformed: ), required: true, }, - transform: { - description: - 'Function to transform source code. Must be synchronous - cannot return Promise.', - type: '(code: string, id: string) => string', - required: true, - }, }} /> @@ -177,7 +166,7 @@ Formatters control how messages are serialized to disk: 'Function to parse file content into messages. Must be synchronous - cannot return Promise.', type: ( <> - (content: string, context: {'{ locale: string }'}) {'=>'}{' '} + (content: string) {'=>'}{' '} Message @@ -195,7 +184,7 @@ Formatters control how messages are serialized to disk: Message - [], context: {'{ locale: string }'}) {'=>'} string + []) {'=>'} string ), required: true, diff --git a/website/content/core-concepts/runtime.mdx b/website/content/core-concepts/runtime.mdx index 13b0e66..ce66b42 100644 --- a/website/content/core-concepts/runtime.mdx +++ b/website/content/core-concepts/runtime.mdx @@ -19,17 +19,12 @@ You create a `Say` instance with a list of supported locales, plus either preloa ```ts import { Say } from 'saykit'; +import en from './locales/en.po'; +import fr from './locales/fr.po'; const say = new Say({ locales: ['en', 'fr'], - messages: { - en: await import('./locales/en/messages.json').then((m) => m.default), - fr: await import('./locales/fr/messages.json').then((m) => m.default), - }, - // or with a loader: - async loader(locale) { - return import(`./locales/${locale}/messages.json`).then((m) => m.default); - }, + messages: { en, fr }, }); ``` @@ -158,14 +153,3 @@ say.call({ ``` In normal application code you usually do not write this by hand. Instead, the compiler transform rewrites say\`Hello, \$\{name}!\` into the appropriate runtime call for you. - -## Fallbacks - -There are two different fallback ideas in SayKit: - -- runtime locale matching, handled by `say.match()` -- translation fallback chains, configured with `fallbackLocales` during compilation - -Today, the `Say` instance itself does not walk a per-message fallback chain at runtime. Instead, fallback translations are applied when translations are compiled, based on [`fallbackLocales`](/core-concepts/configuration). - -That means the runtime instance is responsible for selecting and using one locale, while the compile step is responsible for building the final translation catalogue for that locale. diff --git a/website/content/getting-started/installation.mdx b/website/content/getting-started/installation.mdx index eb411b4..8d2c903 100644 --- a/website/content/getting-started/installation.mdx +++ b/website/content/getting-started/installation.mdx @@ -8,7 +8,7 @@ description: Install SayKit and set up internationalisation in your project ### Core Packages - `saykit` - Core runtime library with the Say class and message formatting -- `@saykit/config` - Configuration schema and CLI tools for extracting, compiling, and building translations +- `@saykit/config` - Configuration schema and CLI tools for extracting translations ### Framework Integrations diff --git a/website/content/getting-started/introduction.mdx b/website/content/getting-started/introduction.mdx index f42f852..65ebc93 100644 --- a/website/content/getting-started/introduction.mdx +++ b/website/content/getting-started/introduction.mdx @@ -26,11 +26,11 @@ import { Braces, WandSparkles, Puzzle, Languages } from 'lucide-react'; title="Compile-Time Extraction" description="Automatically extract translatable messages from your source code during build" /> - {/* } title="Framework Agnostic" - description="Works with React, Next.js, and any framework that supports plugins" - /> */} + description="Works with React, Carbon, and any framework that supports plugins" + /> } title="ICU MessageFormat" @@ -62,9 +62,9 @@ Install the React, Babel, or unplugin integration for your build setup -### Extract and Compile Messages +### Extract Messages -Use the CLI tools to extract messages and compile translations +Use the CLI to extract messages from your source files.