Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Review of the plugin #3

Open
timofei-iatsenko opened this issue Oct 16, 2023 · 5 comments
Open

Review of the plugin #3

timofei-iatsenko opened this issue Oct 16, 2023 · 5 comments

Comments

@timofei-iatsenko
Copy link

Hi @HenryLie, thanks for taking time to bring support for Svelte into Lingui. I quickly reviewed the docs and code and have some questions / suggestions:

It provides a macro that work for JS files to make the syntax much more succinct, but it doesn't work with modern Svelte projects that uses Vite as the bundler, since both Vite and Sveltekit expects libraries in ESM format. Lingui's macro depends on babel-plugin-macros to work, which doesn't seem to work well with Vite.

Svelte and Vite don't use Babel to transpile code, so even if babel-plugin-macros work we'll need to add an extra tool to do extra transpilation when compiling.

To use macro it indeed requires an additional compilation step. However there no issue with using macro with Vite using babel-macro or SWC. We have a couple working examples in the repo.

Using macro has few main advantages over "native" approach:

  1. Allowing to write a single message without splitting it into a chunks, therefore provide translator with more context.
  2. Allowing named placeholders in the messages: t`Hello {name}`
  3. Automatically drop compile time properties such as context, defaultMessage and generate id.
  4. Expand recursively other macro's in the message, such as plural.

So it's very beneficial to make macro work with svelte. I'm not particularly familiar with Svelte, but i can offer my help to achieve this from compiler / ast point of view.

I quickly checked svelte compiler options, and see that it supports "preprocess" param. I believe that could be useful to implement full macro support into Svelte code and template blocks.

@HenryLie
Copy link
Owner

@thekip Thank you for the spending your time reviewing the plugin!

To use macro it indeed requires an additional compilation step. However there no issue with using macro with Vite using babel-macro or SWC. We have a couple working examples in the repo.

Yeah, at first I wanted to use lingui's provided macro as it is since it seems very feature complete, but I wasn't able make it work with Sveltekit + Vite, which I think is due to the incompatible module format (babel-plugin-macros is not exporting an ESM-compatible module which both SK and Vite expects).

Considering that the recommended approach to build Svelte apps is to use SK + Vite, and they don't use Babel internally to compile the code, I tried to emulate as much of the macro's functionality as possible in this plugin, both through the extraction process and through the runtime code. In particular,
converting message declarations to id is done both statically during extraction, and during runtime to generate the key for looking up the compiled catalog.

However, I agree with your observations that there is only so much that can be done without the compiler approach. I was able to implement the basic features without doing the macro approach of preprocessing the code directly, but I started stumbling upon the implementation when trying to implement the more advanced cases like nested plurals, since the runtime side of the code are unable to get the name of the variable being passed in to the placeholders like the name in t`Hello {name}` example you gave.

So it's very beneficial to make macro work with svelte. I'm not particularly familiar with Svelte, but i can offer my help to achieve this from compiler / ast point of view.
I quickly checked svelte compiler options, and see that it supports "preprocess" param. I believe that could be useful to implement full macro support into Svelte code and template blocks.

Thanks for offering to help, it is greatly appreciated! I think that is correct, we can use the preprocess function exposed by the compiler to create a preprocessor to be called before or after the default vitePreprocess.

I don't have much experience with code replacement, I initially thought I'll need to replace the existing ast nodes with new nodes that I'll need to generate on my own to make this work. However, I see the docs' example to make modifications to the code is to handle the code as string and use a package called magic-string to make modifications and generate a source map automatically. Perhaps this way is simpler?

@timofei-iatsenko
Copy link
Author

However, I see the docs' example to make modifications to the code is to handle the code as string and use a package called magic-string to make modifications and generate a source map automatically. Perhaps this way is simpler?

I believe they use magic string approach for the sake of simplicity. Doing real-world scenarios on the code is not possible using simple string replace mechanisms. So you need in preprocess function parse the string into ast, do some changes and the stringify AST back.

Actually i could help with this, what i really need is couple of examples, something like input -> output.

In the begining it could be prototyped with babel and JS for the sake of simplicity and development speed. Then it could be ported to the SWC + Rust to make this additional compile step as fast as possible.

@HenryLie
Copy link
Owner

Got it, thanks for the pointers! With the plan to switch to the compiler approach, I went back to the drawing board and reconsidered some of the design decisions. With the current version I made some deviations from Lingui's JS macro syntax to differentiate usages in different places due to them all being executed on runtime. With the compiler approach, I think it's possible to closely follow Lingui's JS macros syntax 🎉

The current version has 6 functions:

  • 2 for the svelte stores $t and $plural to make them reactive when the locale changes
  • 2 for plain usage in plain js/ts files without reactivity support gt and gPlural
  • 2 for marking messages for extraction without actually translating them during runtime, msg and msgPlural

I'm thinking instead of providing Svelte stores and make every piece of message in the component reactive, it's going to be much simpler to force rerender of the entire tree when the user requests a locale change with a key block on the root of the tree, that will use the current locale as the value. With this approach, the store and non-store syntax can be combined, and the compiler approach will make msgPlural unnecessary too.

I think we can start with basic form t syntax. It will be the same syntax on both Svelte and JS/TS files and look very similar to Lingu's macro, save for the module name to import from:

import { t } from "svelte-i18n-lingui";
const message = t`Hello World`;

// ↓ ↓ ↓ ↓ ↓ ↓

import { i18n } from "@lingui/core";
const message = i18n._(
  /*i18n*/ {
    id: "mY42CM",
    message: "Hello World",
  }
);

Could you show me how the compilation above should be written? I'll then try implementing it either with babel or with a Vite plugin (I had issues with importing babel-plugin-macros before, Vite complains that it is a CommonJS plugin).

@timofei-iatsenko
Copy link
Author

Could you show me how the compilation above should be written? I'll then try implementing it either with babel or with a Vite plugin (I had issues with importing babel-plugin-macros before, Vite complains that it is a CommonJS plugin).

I believe that Svelte with theirs custom extension for files has the same problems with https://www.npmjs.com/package/vite-plugin-babel-macros as Vue integration. I once investigated and proposed my custom solution here. You can use this as example to start from.

But still, abilities of macro-babel-plugin is very limited. I think it would not be possible to make comprehensive Svelte implementation with it. For Svelte you would need to write custom transformations.

The anatomy of lingui macro is

  1. babel-macro-plugin collect all usages of macro in the file (it collects node of identifiers)
  2. passing array of nodes to the lingui macro
  3. macro processing each node and replacing one AST to another AST

For svelte integration i think we could drop babel-macro-plugin and find interesting nodes by our own then pass these nodes to existing macro code to reuse it.

Unfortunately, lingui macro code is not modular, so you will not able to re-use parts from it in your custom integration, but this is TBA, for PoC you can start from copy-pasting it.

Also if i were you i would play with svelte compiler, put console.log or use debugger to see what exactly you get in this preprocess step and how you can use it.

If you want we can connect in the Discord and i could guide you thru the process so you will understand it better.

@timofei-iatsenko
Copy link
Author

You could also check other svelte preprocess plugins to pickup some ideas
This one for instance: https://github.com/l-portet/svelte-switch-case/blob/master/src/index.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants