Skip to content

Fix JSON catalogue declarations, loading, and empty-locale fallback - #42

Merged
k0d13 merged 7 commits into
mainfrom
kodie/declaration-file-naming
Jul 20, 2026
Merged

Fix JSON catalogue declarations, loading, and empty-locale fallback#42
k0d13 merged 7 commits into
mainfrom
kodie/declaration-file-naming

Conversation

@k0d13

@k0d13 k0d13 commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Started as a fix for JSON catalogue declarations being silently ignored by TypeScript, and grew to cover the three other things that were broken along the same path.

Declaration file naming (@saykit/config, minor)

TypeScript resolves ./locales/en.json by stripping the extension it recognises and looking for en.d.json.ts. The en.json.d.ts form is only consulted for extensions the resolver doesn't know, so the old naming worked for .po but was ignored for .json — JSON catalogues fell through to the real file and got the literal object type inferred from their current contents instead of the declared Record<string, string>.

Catalogue declarations are now emitted as {locale}.d.{extension}.ts. Docs updated across five pages, including a new section explaining the resolution rule and the allowArbitraryExtensions requirement for non-JSON extensions.

Breaking for existing projects: leftover {locale}.{extension}.d.ts files are not migrated automatically and should be deleted by hand.

JSON catalogue loading (unplugin-saykit, patch)

A .json id is interpreted as JSON by whatever runs after the plugin — Rollup's json plugin parses the module code, webpack/rspack assign the json module type, esbuild picks its loader from the extension. Emitting export default {...} for those ids was a build-time syntax error. Non-JSON catalogues still get the ESM wrapper, which previously had no test coverage and now does.

Empty-locale fallback (@saykit/config, patch)

Formats with a single value slot per key (JSON, unlike PO's separate msgid/msgstr) report an untranslated key as an empty message and an empty translation. Record assembly wrote that empty value over the string already resolved from a less specific locale, so every untranslated JSON locale shipped blank strings instead of falling back to the source. Empty values are now skipped.

React default entry filename (@saykit/react, patch)

Frameworks that guard their server environment against client-only modules commonly deny the **/*.client.* pattern. TanStack Start does, and since its SSR environment doesn't set the react-server condition, @saykit/react resolved to dist/index.client.mjs and was rejected on its filename alone — even though a "use client" module is meant to be server-rendered. The default entry is now dist/index.mjs; the react-server condition still resolves to dist/index.server.mjs.

Examples

Reworked so each covers a distinct path, with a new examples/README.md documenting the matrix:

Example Formatter Build plugin Import style Locales
carbon json() plain unplugin-saykit/rolldown dynamic en, fr, de (empty)
nextjs po() babel-plugin-saykit static en, fr
tanstack-start json({ dialect: 'arb' }) unplugin-saykit/vite dynamic en, fr

Existing French translations were carried across formats rather than retyped. Carbon's untranslated de locale is the end-to-end check for the fallback fix: its built chunk should contain English strings, not empty ones.

Verification

  • Full suite: 288 tests passing
  • check passes for all three examples and @saykit/react
  • build passes for all three examples — tanstack-start's build was failing before this branch and now succeeds

Known gap, not addressed

@saykit/transform-jsx passes empty arrays for comments and source references where @saykit/transform-js extracts both, so no JSX example can carry a translators: comment in its catalogue. Noted in examples/README.md.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • JSON catalogues are now loaded as standard JSON, improving compatibility across build tools.
    • Added German translations and locale navigation to example applications.
    • React’s default entry point now works in server-rendered environments.
  • Bug Fixes

    • Preserved fallback translations when a more-specific locale contains an empty value.
    • Improved TypeScript declaration file naming and resolution for catalogue files.
  • Documentation

    • Updated extraction, formatter, typed-message, and example guidance for the new catalogue conventions.

@changeset-bot

changeset-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c413729

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@saykit/config Minor
unplugin-saykit Minor
@saykit/react Patch
@saykit/format-json Minor
@saykit/format-po Minor
babel-plugin-saykit Minor
@saykit/transform-js Minor
@saykit/transform-jsx Minor
saykit Patch
@saykit/carbon Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
saykit Ready Ready Preview, Comment Jul 20, 2026 3:07am

@github-actions github-actions Bot added dependencies Updates or changes related to project dependencies examples Updates or additions to example apps tests Modifications, additions, or fixes related to testing package: react Related to @saykit/react package: config Related to @saykit/config and the CLI package: unplugin Related to unplugin-saykit website Updates to the documentation website labels Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@k0d13, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 591e5646-34f4-44ce-a8cb-0ce95967d76a

📥 Commits

Reviewing files that changed from the base of the PR and between 83edc39 and c413729.

📒 Files selected for processing (6)
  • .changeset/declaration-file-naming.md
  • .changeset/empty-translation-fallback.md
  • .changeset/json-catalogue-load.md
  • .changeset/react-default-entry-filename.md
  • packages/config/src/features/catalogue/record.test.ts
  • packages/config/src/features/catalogue/record.ts

Walkthrough

SayKit updates catalogue declaration naming, JSON loading, fallback preservation, example applications, documentation, and the React package default build entry.

Changes

Catalogue declarations and fallback handling

Layer / File(s) Summary
Declaration paths and fallback assembly
packages/config/src/features/catalogue/*, packages/config/src/features/workers/*, .changeset/empty-translation-fallback.md
Declarations use {name}.d{extension}.ts, and empty translations no longer overwrite fallback values.
Declaration resolution guidance
.changeset/declaration-file-naming.md, website/content/**/*.mdx, tsconfig.base.json
Documentation and compiler configuration describe extension-aware declaration lookup and legacy-file cleanup.

JSON catalogue loading

Layer / File(s) Summary
Extension-specific plugin output
packages/plugin-unplugin/src/*, .changeset/json-catalogue-load.md
JSON catalogues are emitted as bare JSON, while non-JSON catalogues retain ESM default exports.

Example catalogue migrations

Layer / File(s) Summary
Example formatter and locale migration
examples/README.md, examples/carbon/**, examples/nextjs/src/locales/*, examples/tanstack-start/**
Examples switch relevant catalogue resources and formatters to JSON and add typed locale declarations.
TanStack locale navigation
examples/tanstack-start/src/routes/{-$locale}/index.tsx
The route adds locale links, active-locale state, and contextual reset text.

React package entry output

Layer / File(s) Summary
Default and server entry generation
packages/integration-react/package.json, packages/integration-react/tsdown.config.ts, .changeset/react-default-entry-filename.md
The default export targets dist/index.mjs; the build writes server and client-guarded variants to the updated filenames.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • k0d13/saykit#33: Updates adjacent catalogue declaration assertions in storage tests.
  • k0d13/saykit#36: Overlaps with catalogue extraction and writing workflow changes.

Suggested labels: package: carbon

Poem

A rabbit formats files neat and bright,
JSON hops through the build tonight.
Fallbacks stay when blanks appear,
Locale links twinkle far and near.
React’s entry finds its way—
Sniff, hop, ship, hooray!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and covers the main fixes to JSON catalogue declarations, loading, and empty fallback behaviour.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kodie/declaration-file-naming

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/config/src/features/catalogue/path.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using join to preserve path relativity.

Using resolve converts relative paths to absolute ones based on the current working directory. Using join ensures that the returned path retains the same relative or absolute nature as the input cataloguePath, which is often the expected behaviour for a sibling-path utility.

♻️ Proposed refactor

Update the import:

-import { parse, resolve } from 'node:path';
+import { join, parse, resolve } from 'node:path';

Use join for the path construction:

 export function declarationPathFor(cataloguePath: string) {
   const { dir, name, ext } = parse(cataloguePath);
-  return resolve(dir, `${name}.d${ext}.ts`);
+  return join(dir, `${name}.d${ext}.ts`);
 }

Also applies to: 14-26

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/config/src/features/catalogue/path.ts` at line 1, Replace resolve
with join in the path utility and update the node:path import accordingly.
Ensure the sibling-path construction preserves whether cataloguePath is relative
or absolute while retaining the existing parsing and path-segment behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/config/src/features/catalogue/record.ts`:
- Around line 46-51: The fallback selection in the record-building logic must
preserve an existing less-specific translation when a PO entry has an empty
translation. Update the value assignment around message.translation and
message.message to use message.message only when record[key] is not already set,
while retaining the current behavior for genuinely missing record entries and
skipping empty values.

---

Nitpick comments:
In `@packages/config/src/features/catalogue/path.ts`:
- Line 1: Replace resolve with join in the path utility and update the node:path
import accordingly. Ensure the sibling-path construction preserves whether
cataloguePath is relative or absolute while retaining the existing parsing and
path-segment behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4481958c-db03-4c85-965f-fda4363af625

📥 Commits

Reviewing files that changed from the base of the PR and between 4a2c428 and 83edc39.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (46)
  • .changeset/declaration-file-naming.md
  • .changeset/empty-translation-fallback.md
  • .changeset/json-catalogue-load.md
  • .changeset/react-default-entry-filename.md
  • examples/README.md
  • examples/carbon/package.json
  • examples/carbon/saykit.config.ts
  • examples/carbon/src/i18n.ts
  • examples/carbon/src/locales/de.d.json.ts
  • examples/carbon/src/locales/de.json
  • examples/carbon/src/locales/en.d.json.ts
  • examples/carbon/src/locales/en.json
  • examples/carbon/src/locales/en.po
  • examples/carbon/src/locales/fr.d.json.ts
  • examples/carbon/src/locales/fr.json
  • examples/carbon/src/locales/fr.po
  • examples/nextjs/src/locales/en.d.po.ts
  • examples/nextjs/src/locales/fr.d.po.ts
  • examples/tanstack-start/package.json
  • examples/tanstack-start/saykit.config.ts
  • examples/tanstack-start/src/i18n.ts
  • examples/tanstack-start/src/locales/en.d.json.ts
  • examples/tanstack-start/src/locales/en.json
  • examples/tanstack-start/src/locales/en.po
  • examples/tanstack-start/src/locales/en.po.d.ts
  • examples/tanstack-start/src/locales/fr.d.json.ts
  • examples/tanstack-start/src/locales/fr.json
  • examples/tanstack-start/src/locales/fr.po
  • examples/tanstack-start/src/locales/fr.po.d.ts
  • examples/tanstack-start/src/routes/{-$locale}/index.tsx
  • packages/config/src/features/catalogue/path.ts
  • packages/config/src/features/catalogue/record.test.ts
  • packages/config/src/features/catalogue/record.ts
  • packages/config/src/features/catalogue/storage.test.ts
  • packages/config/src/features/catalogue/storage.ts
  • packages/config/src/features/workers/extract-worker.test.ts
  • packages/integration-react/package.json
  • packages/integration-react/tsdown.config.ts
  • packages/plugin-unplugin/src/index.test.ts
  • packages/plugin-unplugin/src/index.ts
  • tsconfig.base.json
  • website/content/core-concepts/extraction.mdx
  • website/content/getting-started/quickstart.mdx
  • website/content/guides/custom-formatter.mdx
  • website/content/guides/typed-messages.mdx
  • website/content/reference/cli.mdx
💤 Files with no reviewable changes (6)
  • examples/carbon/src/locales/fr.po
  • examples/carbon/src/locales/en.po
  • examples/tanstack-start/src/locales/fr.po
  • examples/tanstack-start/src/locales/en.po
  • examples/tanstack-start/src/locales/en.po.d.ts
  • examples/tanstack-start/src/locales/fr.po.d.ts

Comment thread packages/config/src/features/catalogue/record.ts Outdated
@k0d13
k0d13 merged commit 73b7bd2 into main Jul 20, 2026
10 checks passed
@k0d13
k0d13 deleted the kodie/declaration-file-naming branch July 20, 2026 03:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Updates or changes related to project dependencies examples Updates or additions to example apps package: config Related to @saykit/config and the CLI package: react Related to @saykit/react package: unplugin Related to unplugin-saykit tests Modifications, additions, or fixes related to testing website Updates to the documentation website

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant