HenryLie / svelte-i18n-lingui

Use lingui for i18n in Svelte/Sveltekit projects
https://www.npmjs.com/package/svelte-i18n-lingui
MIT License
22 stars 2 forks source link

Review of the plugin #3

Open timofei-iatsenko opened 12 months ago

timofei-iatsenko commented 12 months ago

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 commented 12 months ago

@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 commented 12 months ago

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 commented 11 months ago

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:

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 commented 11 months ago

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 commented 11 months ago

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