Repository navigation
feat: runtime core macro - #2618
timofei-iatsenko wants to merge 19 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
size-limit report 📦
|
There was a problem hiding this comment.
Thanks for the PoC, the direction looks right to me. My main concern for taking this out of draft is a parity test suite against the compile-time macro: identical inputs -> identical id/message, to avoid silent drift between compile and runtime macro. Is it possible to tape it with unit tests?
I'd ship it as a subpath export @lingui/core/runtime-macro (is this a valid name? I recall something from our previous releases about the requirement for macro to end with the /macro suffix. So, not sure about this). BTW, is it really a macro now? A macro is by definition compile-time expansion. It could be a good idea for another terminology and eventually decoupling these things.
|
@timofei-iatsenko thank you for the feature, let's move it forward! Some of my projects require a native declaration of the translation messages in pure JS without any macro magic, so I started to invent workarounds like this #2586 But the current implementation works much better! As I can now declare Lingui messages in any script without depending on a JS transpiler, this makes possible to use defined strings not only with Lingui, but with any other parts of the application, where using Lingui is not possible! For example, I use Payload CMS, which uses it's own self-implemented translation system and no ability to integrate other translation systems. I submitted a feature request to fix this payloadcms/payload#17332 but afraid that it will take too long time. So, for now, using the Here is an example of defining a list of translatable labels for a request status: import { msg } from "@lingui/runtime-macro";
export const RequestStatuses = {
collecting: msg`Collecting`,
choosing: msg`Choosing`,
fulfilled: msg`Fulfilled`,
canceled: msg`Canceled`,
}
const msgObjectToFallback = (obj: Record<string, MessageDescriptor>) => {
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value.message!]))
}
export const RequestStatusesFallback = msgObjectToFallback(RequestStatuses)
export type RequestStatus = keyof typeof RequestStatusesUsing this way, I can pass the RequestStatusesFallback object as a list of labels for the "requestStatus" field and it works well, without any errors and dependencies on transpilers! |
|
And about the name of this feature Actually, Timofei invented a purge JS function that declares a message for translation, that works on vanilla JS without any transpilers, that unlocks usage of Lingui with any pure JS project! So, let's maybe name it just as |
|
Also, this feature makes possible to use Lingui in Playwright tests! This is especially convenient with the pseudo locale, reusing in the tests the same source strings and expression as in the app code! |
i can add additional check validating id's to existing cases in runtime version. Since the runtimve version is a subset from a compile time using full test suite of compile time version is not feasible.
I could not agree more that naming should be adjusted, actually naming is the hardest part. Let's break it all:
There are 2 things which are called macro used in the Lingui project.
Over the years and iterations all this blurred a little bit. The main focus in the guides and APIs was shifted from writing ICU messages directly - to using helpers only, from using runtime methods - to using a compile time plugin. Basically, ICU messages became an "implementation detail", the developer quite rarely interacting with them directly, instead uses convenient API methods. So looking at the context - this runtime macro - is still a macro, because it expands to an icu message + variables. However i could agree that we can shift away from this terminology and invent a new one. We no longer enforced by any convention from the Basically, this "runtime" version could just become a new default api.
After adding this we would have a 3 api surfaces:
And here is a question, should we keep message defintion api surface at all from (1)? eq: // this basically uses 2 functions: message definition + message lookup
i18n._("Hello world!")Here functions are clearly divided, definition and lookup: import { msg } from "@lingui/core/runtime-macro"
// defintion
const def = msg`Hello World`
// → { id: "...", message: "Hello World" }
// lookup
i18n._(def)Because both (1) and (2) technically a runtime-something, it could bring a lot of confusion. Maybe we should deprecate and drop all the ambigous methods signatures from i18n in favour of (3) ? We should also consider that JSX macro would also follow the same pattern. So naming pattern should be scalable for jsx as well. Regarding moving forward - I would like to hide it behind a feature flag, so we can iterate on this without introducing a new major version every time. Feature flag would be on the lingui config, and basically would just toggle extractor support for this methods. |
|
Thanks @timofei-iatsenko for the write-up and @MurzNN for the real-world use cases. Here's my thinking on a direction to get this out of PoC:
Next steps: (1) move to the |
JSX is out of scope by other reason - for incremental delivery. JSX without compilation macro still has a lot of sense:
BTW, adding a compile step will also optimize this runtime macro calls as well. So maybe some day we will just narrow api to that subset of runtime macro and offer compiler step just as optional improvement. (similarly to what |
|
Thank you folks for the details, yes - agree with most of them! But about "macro" term - it is still not clear to me, why it is relevant to the pure JS function The "macro" term means a short expression, that will be exposed to a full expression on the compile time, here is an explanation of this term:
So, the "macro" term is actually relevant to only I am not against this term, I am good to go with the "runtime-macro", just worry about clear understanding for new users of the feature that this package provide. Seeing the "macro" word in the name I immediately associate the function with transpiling, babel, vite, or some other magic, and don't even expect the pure vanilla JS functionality there. If we take a fresh look to the project, with introducing this feature we actually provide a "missing" core part of the Lingui:
So, I believe the best place for this function And, by the way, can we somehow reuse the same native JS function So, from the fresh look, the function And all other functions are just additional features on top of this main concept. And now, when we have the pure JS implementation of the I don't want to slow-down or sabotage the naming process, just sharing my thoughts as a kind-of newcomer to the Lingui world :) So, feel free to name it as you see it right. |
|
By the way, about tests - I see that we have a new separate test for the JS implementation of the So, maybe instead of writing a separate test, we can reuse the existing test to make it test both implementation with the same test data? |
|
Winter is coming! 🕙 😉 I've already started to use this feature in 4 different apps, and it works awesome! So, maybe let's try to release this as is, hidden by a feature flag with alpha label, before my retirement? 😄 |
|
@andrii-bodnar i wont be available to finish this. Could you take it over from here? I think almost everethying i terms of a code is in place, what's left is a proper naming, documentation support, and promotion. @MurzNN could help you to identify a gaps not covered in this implementation, if they are. |
|
Looked into the code once again, here what i've found missing:
macro: {
corePackage: ['@mycomp/native-core-macro'],
},And "What's left" section. |
|
I'll take a closer look at how we can move forward. |
- export `@lingui/core/runtime-macro` (built as a separate entry, core index unchanged) - `experimental.runtimeMacro` adds the path to `macro.corePackage` so the extractor picks it up - docs: core reference section and config option, marked experimental Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ement post Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I added the packaging, an experimental config option for extraction, and the docs, so it’s ready for a final look. I suggest shipping it as experimental and revisit the naming before the next major. |
|
Nice to see progress there, thank you Andrii! Agree that it's better to release it for now with the "experimental" label, to get testing in the wild and gather more feedback. I still believe that the name "runtime macro" is not clear for the community, so created a public discussion here #2720 to gather more opinions about this. Let's maybe put a link to this discussion in the blog post "Lingui Without a Build Step: the Runtime Macro" to involve more users in this discussion? |
|
|
||
| Variable names can't be read at runtime, so placeholders must be named: `${{ name }}` instead of `${name}`. A plain value throws an error that tells you what to write instead. This is what keeps the messages identical to the compile-time output. | ||
|
|
||
| The runtime macro also has no `t` and no JSX: you define a message with `msg` and translate it with `i18n._`. Without a compile step, the source message stays in your bundle. The [reference](/ref/core#runtime-macro) covers the details. |
There was a problem hiding this comment.
Maybe add here an example of defining the own function t like this?
| The runtime macro also has no `t` and no JSX: you define a message with `msg` and translate it with `i18n._`. Without a compile step, the source message stays in your bundle. The [reference](/ref/core#runtime-macro) covers the details. | |
| The runtime macro also has no `t` out of the box: you define a message with `msg` and translate it with `i18n._`, because the source messages stays in your source code as is, and the target language is defined only in the runtime. The [reference](/ref/core#runtime-macro) covers the details. | |
| But you still can define your own function `t` to use in the code by loading the right `messages` object in the runtime using the current application language, like this: | |
| ````js | |
| import { i18n } from "@lingui/core"; | |
| import { messages } from "path-to-locale/en/messages.js"; | |
| i18n.load("en", messages); | |
| i18n.activate("en"); | |
| const t = i18n._ | |
| console.log(t(msg`Hello ${{ name }}`)); |
There was a problem hiding this comment.
I checked it and const t = i18n._ throws because _ isn’t bound, but const t = i18n.t works. I’d suggest leaving the post as is to keep it short. A built-in t is still an open question, so any feedback on it is welcome
…post Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@MurzNN good idea, I added a link to the discussion in the blog post |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
By the way, about tests - as transpile macro and native js functions should work the same, maybe we can use the same test assets (input and expected output) for both tests, to have one source of truth? So, just place them in one folder and include from both tests, what do you think? |
Description
Adds an experimental runtime macro:
msg(aliasdefineMessage),plural,selectandselectOrdinalas plain functions in@lingui/core/runtime-macro. They need no Babel, SWC or native transform, so Lingui works where there is no build step: Node.js scripts, serverless functions, CLI tools, test runners, or a backend that shares a catalog with the frontend.They return the same message descriptor, with the same generated ID, as the compile-time macros, so catalogs stay the same.
Extraction is opt-in:
What's in this PR
@lingui/core/runtime-macrosubpath export, built as a separate entry. The main@lingui/corebundle doesn't change.experimental.runtimeMacroconfig option, so the extractor picks up imports from the new path.Differences from the compile-time macros
${{ name }}, not${name}. Variable names can't be read at runtime, and positional placeholders would change the message and its ID. A plain value throws an error that explains the fix.msginstead.tand no JSX. Define a message withmsg, translate it withi18n._.plural,selectandselectOrdinalinsidemsg. With the option enabled, a build step (the Babel plugin or the Vite native transform) also compiles these imports, and a standalone compiledpluralreturns a translated string instead of a descriptor.corePackageoption.Experimental
The API and the import path may change in a minor release. We'll revisit the name before the next major.
Related
Requests to use Lingui without a build step:
msgstub for Node's native TypeScript runtime (see also fix(macro): add shims for macro executed in nodejs without transpilation #2471)/* i18n-s */comments #2586: earlier approach with/* i18n-s */commentsCompiler plugin friction (context only, not solved here since JSX isn't covered):
Error: Cannot read properties of undefined (reading 'type')#2239, Compatible with React Compiler? #2343, React macros are broken when using the OXC-based React Compiler #2657: React Compiler and macro plugin orderingTypes of changes
Checklist