P7: compile messages to data, not to functions - #110
Conversation
|
Too many files changed for review (151 files, 100 file limit). Bypass the limit by tagging |
🦋 Changeset detectedLatest commit: a7669f4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedToo many files! This PR contains 150 files, which is 50 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (150)
You can disable this status message by setting the 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. Comment |
Closes #109.
The plugin emitted ICU strings and the runtime parsed and formatted them on demand, so the client got
messageformat, its parser, both skeleton parsers, and a format cache whose only job was to avoid paying for the same parse twice. Everything about a message is known at build time, so none of that has to ship.saykit compilenow writes one module per locale, and that module is data:A view compiles a node into a closure the first time that message is called, and memoises it. What ships is four wrappers over
Intland a ~40 line evaluator, and nothing parses anything.What changed
@saykit/configcompiles ICU to aMessagetree instead of to function source. Styles still resolve at build time, into theIntloptions bag they stand forsaykitgainsMessageandcompileMessage(message, locale).View.Messagesis a record of nodes, andview.call()compiles on first use and caches per viewSayProvideris back tostore, orlocaleandmessages, and<SayProvider>on the server takes no props again — thereact-serverbuild reads the established view and hands both acrosssaykit/runtimeis no longer a public entry point. Nothing imports it now that generated modules are data, so it is bundled into the main entryWhy an AST, and not one function per message
#95 proposed emitting the function directly:
That was the first pass of this branch. It is smaller and faster, and it has one problem with no workaround worth having: a function cannot cross the RSC boundary.
With compiled functions a server component cannot hand its messages to a client tree at all. The app has to import the catalogue in a client module and pass only the locale, which means a
providers.tsxper app, a catalogue reachable from a module that is notserver-only, and every locale imported eagerly on the client so hydration matches. That branch existed, the nextjs example carried all of it, and it is a real tax for something the compiler is supposed to remove.Data crosses. So the provider goes back to taking
localeandmessages,<SayProvider>on the server takes no props, and no example needs a providers module.Two things fall out of it that are worth having on their own:
The cost is one indirection per node at format time, against a template literal the engine JITs, after a compile that happens once per message per locale and only for messages that render. A message with no placeholders is emitted as a bare string and skips all of it.
Examples
nextjs:<SayProvider>with no props inside thewithSay-wrapped layout,getSay()forlang. No providers moduletanstack-start: unchanged,localeandmessagesfrom the loader is still the right form.d.tsis nowRecord<string, Message>