How the transition to React Server Components enables better internationalization solutions.
This talk has been presented at React Summit 2024, check out the latest edition of this React Conference.
How the transition to React Server Components enables better internationalization solutions.
This talk has been presented at React Summit 2024, check out the latest edition of this React Conference.
Before server components, translations were loaded one page at a time using namespaces, which required bookkeeping to manage which messages were needed for each page.
The proposed solution is to use a giant JavaScript file with all messages exported separately, allowing import statements to manage dependencies and enabling tools like Webpack or TurboPack to tree-shake and minimize the loaded messages.
Paraglide.js is a tool developed by Loris that generates a giant messages file from translation files, allowing developers to use import statements to manage translations without imperative loading.
Developers should stop using namespaces because they are no longer fit for purpose with server components, requiring too much effort and overhead to manage fine-grained loading.
Namespaces are inefficient with server components because the unit of message loading changes, requiring fine-grained loading for client components and making imperative loading cumbersome and high-overhead.
The new approach benefits client components by enabling fine-grained loading, ensuring only the necessary messages are sent to the client, reducing bandwidth usage.
Import statements express the relationship between components and messages, allowing build tools to analyze and tree-shake unused messages, optimizing the loading process.
The goal is to build a multilingual app while sending as few bytes as possible over the wire, being fine-grained in loading messages and translations to respect users' bandwidth.
Loris builds internationalization tooling at a company called Inlang.
The main focus is on internationalization for server components, specifically string translations, layouts, and components, not content.
I'm Loris from Inlang, and today I'll talk about internationalization for server components. Our goal is to build a multilingual app while minimizing data sent over the wire. We used to load translations per page into namespaces, but this becomes challenging with server components. On the server, you can load everything, but on the client, it's inefficient to load a namespace for each component.
Hi, I'm Loris. I build internationalization tooling at a company called Inlang, and today I'm going to be talking about internationalization for server components.
So, just that we're all on the same page, I'm going to be talking about string translations, your layouts, your components, not your content. So, when we're doing internationalization, our goal is, of course, to build a multilingual app while sending as few bytes as possible over the wire. We want to be really fine-grained in what we reach messages and translations we load so that we only display what is actually necessary. We respect our users' bandwidth.
Before server components, the unit in which we would load translations is one page at a time. So, it would have a certain set of messages that you would need on one page, certain set of messages that you would need on another page. So, what you would do is you would sort them into namespaces, names groups. Then you would imperatively load these namespaces with some sort of load namespace functions. Each item in a library is going to have some sort of equivalent to that one.
Now, what you would usually end up with is kind of one namespace per page. Then if you have a shared component per page, maybe an auth bar or a component that's used in many pages, you would create a second namespace just for that. It also imperatively loads that one when you load the page. So, you kind of need to keep track of, okay, which messages do I need exactly and which ones do I not need. You also often have some sort of common namespace where you have messages that are used in many different places, generic stuff, forwards, backwards. So, what you can see here is that you do end up with quite a few namespaces usually per page. I'm sure that if you've built a lot of multilingual apps, you're going to be familiar with this.
But there's quite a bit of bookkeeping, but usually it's staged manageable. You end up with between two and five namespaces per page. It's a bit of work, but it's doable. But this pattern works is a lot more difficult to pull off well if you introduce server components into the mix, because the unit in which you load messages is no longer per page. Now you're trying to load exactly the messages that are needed for the client components that you're going to render. With server components, your page is kind of this patchwork of stuff that's rendered on the server and stuff that's on the client, and you need messages in both, you need translations in both. But the requirements on how you actually load these messages is very, very different. On the server, you really don't need to do any fine-grained loading at all. You can just load everything, be gluttonous, and just use what you want, like you like it. Do no bookkeeping at all. That's wonderful. But if we were to stick with the whole namespace and imperative loading approach, on the client, it's going to be very, very painful, because you're going to end up creating a namespace for each client component, then you need to imperative load that one with the page, and it's going to be a lot of work, a lot of overhead.
We want to load only the necessary messages for a page and client components. Namespaces are no longer suitable for server components. Instead, we can use import statements in JavaScript to express dependencies and optimize message loading. By having all messages in one JavaScript file, build tools can analyze and remove unused messages. Tree shaking ensures minimal message delivery. Tools like Paraglide.js can generate the necessary files, eliminating the need for imperative loading. Stop using namespaces and try Paraglide.js.
We really only want to load the messages that are used on a page and used in client components on that page. So kind of these competing scenarios where on the one hand, we really need really, really, really fine-grained loading, and on the server, we don't need fine-grained loading at all. But of course, we can't really do separate APIs for the server and the client, because when we develop our apps, our apps are living documents. Client components become server components, become client components, add components, remove components. So keeping the imperative loading in sync with all of that is going to be way too much work, way too much overhead.
So when it comes to loading translations, namespaces really are no longer the way to go when you introduce server components into the mix, because it just becomes a prohibitive amount of effort that you need to put in to actually load what you need and only what you need. Surely there has to be a better way. Let's take a step back and start thinking about what we're actually trying to do when we do these namespace releases. What we're trying to express is that this component depends on these messages, this code depends on that. And in JavaScript, we have this wonderful tool called an import statement, which expresses that relationship very, very well. And we have tools that can analyze that, at bundlers that can tweak and everything that can really, really take advantage of that.
Let's imagine that instead of sorted namespaces or JSON files, we had kind of one giant JavaScript file where all of our messages that exist in our app are present and exported separately. Now let's not worry where we got that file from. Let's just assume we have it. You can see this on the left here, what we can do in our components, regardless of if they're client components or server components, we can just import from that file and use the messages that we want. And our build tools, or Redpack or TurboPack, is going to be smart enough to see, okay, we're using exactly this message and it's only these messages and we're using them on the client. It's able to do this separately for the server and the client, even though the code is identical. So it's going to be able to see which messages we are using, and it's going to be able to remove all the rest. So we kind of get the minimum set of messages that is used on the client. If you're developing, it's very common for your client component to become a server component or vice versa, usually just by adding or removing the use client bit there. And the tree shaking in our bundlers is clever enough to do this properly. So if I remove the use client there, all the messages would no longer be sent to the client. We're sending as few bytes as possible. This is perfect, fine-grained loading.
Now, of course, you probably don't want to actually write such a messages file yourself. It's pretty straightforward if you're just using strings, but if you have your ICU message formats strings, there's a bunch of plurals, currency formats and all that. It's going to be very, very tedious to maintain. And of course, your translators are not going to be able to work with it. So you're going to want to generate this file somehow. There are a bunch of tools available in order to do this. But I myself, I have been working on one. It's called a Paraglide.js. It does exactly what I just said, just takes an oil translation files, gives you a giant messages file, and then you can just use that use input statements to express all your relationships that you need. And you no longer need to worry about doing any imperative loading at all. That's my elevator pitch. Please stop using namespaces for loading stuff. They're not fit for purpose anymore. And try a library like Paraglide.js. Thank you.
We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career
Comments