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

RFC: LWC Localization Mechanism (Intl V2) #75

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions text/0000-intl-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
---
title: LWC Localization Mechanism (Intl)
status: DRAFT
created_at: 2023-01-23
updated_at: 2023-01-23
champion: Caridy Patiño (caridy)
rfc:
---

# LWC Localization Mechanism (Intl)

## Motivation

As of today, LWC lacks a mechanism to facilitate the interpolation of content in the LWC template with the proper internalization mechanics. This proposal introduces a novel mechanism to integrate LWC Components with Fluent Messages. This is not the first time we attempt to solve this, in 2017, we put together the first RFC, which was rejected due to the complexity of the grammar on the template.

As a result, we have a huge amount of components today that are doing a poor job producing complex messages by doing string replacement and concatenations. As a result, these components will simply not work on other locales, or will produce sentences that makes no sense to the user.

## Use Cases

* As a developer, I can opt-in to localize my template.
* As a developer, I can interpolate text, variables and HTML tags.
* As a developer, I can format number and dates in my template.
* As a developer, I can select from different statements in my template.
* As a developer, I can use grammatically to format nouns based on app configurations.
* As a developer, I can pluralize sentences based on numeric values.

## Detailed design

### The Breakthrough

Often when thinking about localization of components, we tend to think that in order to produce the right message, which often requires interpolation of text and data, we must provide either syntax or helper functions into the templating engine to describe what message to use in the UI and what data is needed for that message to be rendered accordingly. Essentially, looking at the message as the unit of operation. It turns out that this is the wrong pattern. Instead, if we think about the UI as the interpolation of two distinct templates that are defined in tandem to produce the final UI, then you start thinking about how to produce a snapshot of the two templates based on the current state of the component, and then how to merge them together into the final production for the UI. We already have a good understanding about how to render a template, and how to rehydrate it, how to update it over time, etc. The precedent of how we process and manipulate the HTML template is sufficient. And the problem then gets reduced to how to interpolate the two productions together rather than how to produce individual messages that are accessible to the template production.

#### From Abstract To Concrete

If we have a LWC Template like this:

```html
<template>
<p>
{i18n.paragraph.content}
</p>
<p><button onclick={handleBuy}>{i18n.action.buy}</button></p>
</template>
```

And a message file associated to the same template as follow:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we link to the Fluent spec you had in mind? I assume it's this one.


```
caridy marked this conversation as resolved.
Show resolved Hide resolved
paragraph
.content = { $counter ->
[one] {"A new"}
*[other] {"New"}
} { $accounts ->
[one] book was
*[other] books were
} released today.
}.
action
.buy = Buy { $counter ->
[one] Book
*[other] Books
}
}.
```

For the following component:

```js
export default class Foo extends LightningElement {
@api counter = 0;
}
```

We can process both, the template and the fluent files using the same technique, you have the component state (the component instance itself) as the context to render each of these two templates, the HTML file and the fluent file. In this case, both of them can have access to all the data available in the context provided, which in this case is `counter` field, which is a numeric value.

If we render each of these two elements individually, first the fluent, then the template, we can make sure that both have access to the exact same data, in the same tick, and the HTML Template can access to the production of the messages file via the component's `i18n` getter, which can be provided by the framework via the `LightningElement` abstraction.

What do we get out of this solution?

* We pave a way forward into a Labeling solution that can be used both internally and externally.
* We lift any label logic from the component and into the labels themselves by leveraging the Fluent engine: plurals, variables that can reflect their value into the messages, and other complex use cases that are being handled today by the component authors.

#### LWC Inner-workings

LWC Compiler will have to support fluent, and as customary in LWC, a new convention for filename for those fluent files must be defined. The compilation target of those files must be JS, and there have to be an implicit relationship between files, in this case, any component file can have an associated template file, and an associated fluent file with the messages available to that template.

```
foo/
bar.js
bar.html
bar.fluent
```

If a component has more than one template, they all can have access to the same set of messages defined implicitly in the file with the matching name, e.g.:

```
foo/
bar.js
bar.fluent
x.html
y.html
```

The way LWC can register these association is the same used for the default template, using `registerComponent`. Additionally, we must provide a way for a developer to import fluent dependencies declaratively using import declaration, the same way we allow importing templates. In this case, we must provide a new hook for the developer to return a custom fluent reference, in the same fashion that `render()` method works.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would simplify this to just say that a *.fluent file is associated with a co-located *.js file and must share the same name (e.g. foo.fluent for foo.js). AIUI there is no need to make explicit any relationship between the HTML files and the fluent files – the fluent files simply add properties to the component, and components can already have properties which are referenced by templates, so really we're just saying that the fluent file adds properties to the component.

Severing this relationship also avoids hairy questions like "What if a *.js file imports a *.html file from another directory?"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exactly! if the js exports a component (meaning it calls registerComponent), the fluent file can be associated at that point.


#### Connecting The Dots

When a component is about to be rendered, the engine must use the default associated fluent, if exist. If the hook is defined in user-land, it must be invoked to resolve the custom fluent reference. If the fluent reference is new, an internal fluent instance must be created, binding that instance to the component instance in question. The messages exported by the fluent instance must be made available via `i18n` getter on `LightningElement.prototype`. Each message exported from the fluent instance must be a getter, and must be constructed on demand when the message is accessed. This must be done prior to invocation of the template.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the hook is defined in user-land, it must be invoked to resolve the custom fluent reference.

What does this mean? Are you proposing something like programmatic style sheets, but for fluent files?

The messages exported by the fluent instance must be made available via i18n getter on LightningElement.prototype.

By this, I assume you mean that this.i18n.foo works, but this.template.querySelector('x-component').i18n.foo does not work, correct? In other words, i18n is only exposed to code inside the component, not outside the component?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the hook is defined in user-land, it must be invoked to resolve the custom fluent reference.

What does this mean? Are you proposing something like programmatic style sheets, but for fluent files?

I'm not sure anymore... if we are saying that this implicit association is sufficiently, then no. Let's think about it.

The messages exported by the fluent instance must be made available via i18n getter on LightningElement.prototype.

By this, I assume you mean that this.i18n.foo works, but this.template.querySelector('x-component').i18n.foo does not work, correct? In other words, i18n is only exposed to code inside the component, not outside the component?

Correct. Just like template.


When the template function is called, it will automatically have access to `$cmp.i18n` and all the messages available there. More over, it can participate on the reactivity phase of the rendering since the getter for a message can access values from `$cmp`.

#### Compiling Fluent Files

Fluent already provide a JS Parser that is customizable. On top of that, we can add a layer to resolve additional platform resources (platform labels) by using one of the two methods of importing messages in fluent. The output of the fluent parser must be then post processed by either a LWC platform specific transformation, or a LWC OSS specific transformation, if we can define that as generic.

##### Platform Labels

As of today, Aura framework only supports simple labels, meaning, those labels do not support entities, or any other feature provided by grammaticous. In LWC, when you import a label, you get a string value, and any interpolation needed, must be carry on by the developer. But all those labels are addressable, and statically analyzable, which means we can easily integrate them into the fluent files by defining a convention to import labels from platform in the same way developers do it today in the component's code.

This opens the door for expanding the support to more complex labels, in this case, labels with grammaticous syntax inside them. More on that below.

##### Platform Entities

##### Integration with Grammaticus

The idea of this solution is to combine the best of both worlds: Grammaticus, a robust system capable of producing grammatically correct complex sentences*, *and Fluent a framework built on standards, flexible, developer-friendly and performant to express messages on the client. We currently have access to a series of APIs that can be used to retrieve the processed output of a label in Grammaticus format. Here is one example of data that includes a Custom Object (ID: CO1), as well as, an entity that has been renamed from “account” to “company” by an admin:

```json
{
"n":{
"01ixx0000005ztp":{
"t":"n",
"l":"01Ixx0000005ZTP",
"s":"c",
"v":{
"0":"CO1",
"1":"CO1s"}
},
"account":{
"t":"n",
"l":"account",
"s":"c",
"v":{
"0":"Company",
"1":"Companies"}
}
},
"a":{},
"d":{}
}
```

Knowing that we can retrieve object metadata, as well as, nouns, articles, adjectives, etc. We can then run it through a custom parser that transforms all of these into Fluent messages:

```fluent
element
.content = { $accounts ->
[one] {"A"}
*[other] {""}
} { $accounts ->
[one] Company
*[other] Companies
}
}.
```

## Pros and Cons

Pros:
* Benefits to Developer Productivity
* Being able to access labels as data, rather than something that requires further computing at runtime.
* Being able to rely on an open source and standard solution, like fluent, allow us to get to market quicker with a well stablished solution.
* Being able to use labels declaratively.
* Benefits to Performance/Non-functional requirements
* No noted performance implications at first sight, all i18n specific data to be provisioned as part of the module graph for a component.
* Not allowing ad-hoc fetch operations for retrieving dynamic entity data.
* Service level expectations - Performance/Availability
* No additional service is required for this to function.
* Localization
* Gramatticus, our well stablished legacy platform for localization, will remain as the service that powers the translations.

Cons:

* Complexity:
* LWC Compiler must be revamped.
* New language to be integrated with code editors, and linters to be introduced.
* New metadata to be added to LWC Compilation to describe the label and entities dependencies.
* External Dependency:
* Using fluent, as the primary language to describe messages is tricky because we don't control the destiny of that project.

## Adoption strategy

### Backward Compatibility & Risks

One of the compelling things about this proposal is that it does not affect in any way the existing HTML Parser, or syntax. It does not introduce any new directive either. In previous attempts to solve this problem, that was one of the major concerns. This new feature should be backward compatibility, and users of the framework can continue using the existing label resolution in platform alongside the new feature.

### Prior Art

FormatJS, which relies of helpers and template literals to define the messages, and format the data. Our own implementation of the label system today, which promotes the import of a platform label, and the manual transformation of such string value to produce the final result. As a result, that component is just not localizable anymore.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to expand this section with examples from other popular frameworks (React, Vue, Svelte, Angular, etc.) and meta frameworks (Next, Nuxt, SvelteKit, Remix, etc.). How do they handle i18n?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can certainly add more details, formatjs website does have a good set of examples for some of those frameworks.


## How we teach this

For LWC Authors, this should be easy to learn. They will have to learn few things:

1. That there is a new `this.i18n` value available for anyone extending `LightningElement`. The values there are computed messages, and they change at the same time the UI gets updated, and of course, it can be used from a template, the same way you use any other field, or value from the component.
2. A new syntax, the fluent syntax, must be learned. There is plenty of documentation on the web for fluent.
3. The same data that is available to the template, is also available to the fluent file.
4. How to use platform labels inside their fluent files, and this is mostly a convention system since it relies on the import syntax provided by fluent.

Once they understand there 4 things, they should be able to create very compelling, fully localizable components.

## Open questions

1. Q: How to use messages in a for-loops using the context of the loop as data?

### 2. Q: Do we need a reverse parser? from Fluent to Grammaticus? if someone were going to write a label as a Fluent message, how do we make sure this can be put through the grammaticus engine for translations?

Caridy: I don't think that should be the case for 1P, certain, we can lint 1P to make sure that everything that they use in the fluent file is either interpolation of existing entities or labels, or direct usage of entities and labels. For 2P/3P, the question remains.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the downside of dropping Grammaticus support entirely? In other words, what do we gain by adding Grammaticus support?

Based on my read of this post, Grammaticus is not directly exposed to component authors today; it is just used internally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is the thing... for Admins, the UI to localize their app is backed up by the Grammaticus system in Java land. I will love to drop it entirely, but that's not going to happen. Keep in mind that neither Aura, LWR nor LWC has been able to integrate it yet.