On 31/03/16 15:22, Staś Małolepszy wrote:
On Thu, Mar 31, 2016 at 2:20 PM, Axel Hecht <[email protected]> wrote:
Hi stas,
thanks for taking the deep dive into the various pools here.
I've got a general ecosystem question to start with, is supporting 3rd-level
components a requirement for us? That's probably one thing I failed to
understand in my React.Friday, if everybody that uses react just implements
standalone monoliths, or if there's actually a culture of component-sharing
across projects?
What do you mean by 3rd-level components?
Err, yeah, what did I say there?
What I meant was 3rd party components. I.e., code that we can't just
modify to use l20n.
....
As far as I understand it, there's a strong pattern of having two
types of components. Stateful components don't render anything per
se, only their children. They provide the logic. Stateless
components (which as of React 0.14 can be implemented simply as
functions returning the virtual DOM) provide the content and the
presentation. I think we would mostly like to localized the stateless
ones.
A lot of addons implement their logic as stateful components. So
instead of something like:
Foo.method(componentInstance, arg1, arg2);
you end up doing:
<FooMethod param1=arg1 param2=arg2>
<Component />
</Foo>
It's preferred to split your app into multiple components, and have
separate containers for logic. Supposedly Facebook uses 15,000
components across all of their web properties. I think stateful
components are meant to be modular and easily re-used when the same
logic is needed someplace else. Stateless ones can be re-used when
you just need the same-loooking UI.
Other thoughts:
For pontoon support, either bare-metal-html or global state seem most
promising? Note, "pontoon" might not mean "pontoon" here technically. I'm
entertaining the thought that for Firefox UI pontooning, we need something
that's closer to dedicated privileged APIs like we use for UI tours today,
than owning-dom-window references that pontoon uses for in-place l10n right
now.
Maybe even more so bare-metal-html, as that's making it easier to relate DOM
click events to actual string IDs?
Yeah, I think bare-metal or global state indeed seem to be the best
candidates. Right now I'm leaning towards bare-metal, as that keeps
L20n framework agnostic and better mimics what platform could someday
do for us.
My next step is to drop l20n.js into Activity Streams and see how it
performs and if bare-metal is fast enough.
Component-local state OTH seems to be the hardest to maintain with live l10n
updates.
Do you mean scenarios when the user changes the language? If so, then
yes, it's hard.
Also live updates to a localization, kinto or otherwise.
data-l10n-id a surprising robust and foolproof solution to many problems :)
:-)
Axel
Thanks for your thoughts!
-stas
Axel, for now.
On 31/03/16 13:10, Staś Małolepszy wrote:
Here are my thoughts and rough notes on integrating L20n with React.
I spent some time learning React (and other popular frameworks), went
through a few tutorials and wrote some code. One of the most helpful
experience was the meet.js Summit conference which I attended two weeks
ago where I spoke to many people about this and got a first round of
feedback.
I would now like to write down these thoughts and conclusions from
conversations in a structured manner and open them up to a larger
discussion. I'd love to get more feedback on these ideas. Please
don't be shy to chime in especially if you've already written a few
React apps. And thanks in advance for your patience—this is a long
email.
How L20n works
==============
First, a brief reminder of how L20n currently works.
The developer puts the information about the available languages and
links to resources in <head>. As soon as it's ready, L20n starts the
language negotiation process using this information and subsequently
starts downloading the relevant resources in the user's most preferred
language. Translations are only available via an async API.
DOM nodes with localizable messages are marked up with data-l10n-id
attributes. These attributes are observed by L20n's mutation observer:
each time a new node with the attribute is inserted or the attribute
changes, L20n will retranslate the node in question.
Translations can contain some HTML markup and importantly are objects:
a single translation unit is responsible for localizing both the text
value of a DOM node as well as its textual attributes. Translations
are parsed into an inert DocumentFragment and superimposed (or,
overlayed) on the source node in the DOM. Original child nodes in the
source DOM are kept in the translated result; the identity doesn't
change, event listeners are preserved. Here's an example:
source:
<p data-l10n-id="foo"><button onclick="…"></button></p>
translation:
foo = <button>Send</button> your message <em>now</em>.
result:
<p data-l10n-id="foo">
<button onclick="…">Send</button> your message <em>now</em>.
</p>
Translations accept arguments with external data which is passed by the
developer. For instance, a translation "Hello, { $name }" can take
a "name" external argument and will format the whole message.
Importantly, these external arguments can also be used to configure
other parts of the translation, or to select a branch (e.g. in case of
plurals).
complete = { $num ->
[0] Pending…
*[other] { NUMBER($num, style: "percent") } complete
[1] Done!
This means that' it's important to feed these external arguments into
L20n and format them there, instead of having L20n output translations
with unresolved {…} expressions and resolve them in the consumer code.
How React works
===============
React structures the UI into components and the general consensus is
that you should be smart about separating your components into stateful
and stateless ones. The former kind is for keeping the logic and the
latter is for keeping the presentation layer. Other than the state,
you can pass so-called props to components to configure them (props can
be textual data, child elements, callbacks, etc.). Props are
immutable.
React components have a well-defined lifecycle with self-explanatory
method names like componentWillUpdate, render, componentDidMount and
componentDidUpdate. The render method is called to produce the virtual
DOM representation of the component. The virtual DOM operates on React
components and not the actual DOM elements—the encapsulation is
enforced during the reconciliation process up until the very last
moment in which the actual rendering occurs.
How to integrate L20n into React
================================
There are multiple ways we could integrate L20n into React. Some seem
to be more idiomatic for React, while others enforce a stricter
separation of concerns and let L20n do its thing independently of
React.
In the following examples I'll be using variations of the following
React snippet:
http://stasm.github.io/l20n-react-experiments/base/
The source code can be found at:
https://github.com/stasm/l20n-react-experiments/
1. L20n Components
------------------
Since React uses components, an obvious first try is to provide a set
of special-purpose components whose job is to display translated
messages.
render() {
return (
<h1>
<Translation id="hello" name={this.props.name} />
</h1>
);
}
This allows us to seamlessly take advantage of the "component"
abstraction and pass developer-provided arguments as props/attributes
and even put children inside of the <Translation/>!
There is, however, a problem with this approach. Components need to
produce an element tree. We'd need to enclose the translation in an
outer HTML element, e.g. a <span>, which would result in needless
nesting of redundant elements. One possible solution would be to do
something like this instead:
render() {
return (
<TranslatedElement elem="h1" id="hello" name=… />
);
}
(This is in fact what Yahoo's react-intl does. It also sometimes wraps
translation components in <span>.)
Or even:
render() {
return (
<TranslatedH1 id="hello" name=… />
);
}
An implementation of this approach (sine args and overlays) lives at:
http://stasm.github.io/l20n-react-experiments/components/
This approach extends quite well to attributes because we'd encapsulate
the logic of translating the <h1> together with the attributes inside
of the <TranslatedH1> component. For the same reason it would be
possible to pass children into the component. This way one could
define the source (default) text which could then be extracted into
a localization resource by tools. It would also be possible to define
interactive elements as children and run the (virtual) DOM overlay
logic:
render() {
return (
<TranslatedP id="hello" name=…>
<button onClick=…></button>
</TranslatedP>
);
}
Each instance of the <Translated*> components could store the contents
of the translations or the storage could be centralized via a React's
context (see 2.C. below).
2. Translation passed as props and/or children
----------------------------------------------
Another React's idiom is to use variables stored in the state and
expand them into element's attributes or contents. It can be the
current's component's state, or it can be stored in a high order
component which transparently wraps around another one, or it could be
a global store passed in as a context to all child components.
render() {
return <p title={…}> {…} </p>;
}
What goes into the {…} depends on the exact solution. It could be
this.state.translations.title or this.props.getTranslation('title') or
this.context.getTranslation('title'). My preference goes to functions
because then we can pass this.state as l10-args.
The challenge with this approach is that we need a way to get the list
of translations that we'll use to populate the state. Keep in mind
that the L20n API is async. In order to make a sync request inside of
the {…} we need to first prepare the data store accordingly. There are
a few possible solutions here.
A. Save translation ids as they're being requested.
In this scenario we're collecting the translation ids requested
during the first render of the component. When the render is complete
and the component is mounted, we retrieve the translations
asynchronously and save the result to the component's state this
triggering a re-render. This feels rather hacky and I don't think it's
a well-written React code.
An implementation of this approach (sine args and overlays) lives at:
http://stasm.github.io/l20n-react-experiments/mutable-state/
B. Declare which ids will be needed to render the component.
Here we're declaring which translation ids will be needed. This
can be done with a property stored on the component or by using
data-l10n-ids in the render() method. In case of a configurable
property, declaring args is hard because we don't have them up front.
This might be a show stopper for this approach. And if we use
data-l10n-ids and data-l10n-args, we need to walk the resulting DOM
tree anyways so perhaps another approach which also walks the tree
would be better.
Two implementations (one for a property and another one for
data-l10n-id) of this approach (sine args and overlays) live at:
http://stasm.github.io/l20n-react-experiments/declarative-property/
http://stasm.github.io/l20n-react-experiments/declarative-state/
C. Use a global store à la Redux Provider.
Lastly, we can use React's contexts to create a state store which
is globally available to all child components. Again, I'm still not
sure how to pass arguments into this store at the right time.
ReactDOM.render(
<TranslationProvider src="/path/to/{locale}/resource">
<App />
</TranslationProvider>,
document.getElementById("container")
);
An implementation of this approach (sine overlays) lives at:
http://stasm.github.io/l20n-react-experiments/context/
* * *
Reading more about the context made me realize that even when we store
the state (translations) in individual components (like in A. and B.),
we still probably want a central place to store the current language
chosen by or negotiated on the user's behalf. In fact, all of the
above solutions would likely benefit from a central translation store
or "provider". I implemented an example at:
http://stasm.github.io/l20n-react-experiments/components-context/
3. Virtual DOM manipulation
---------------------------
In this scenario we could make L20n hook into the render() method of
components which need to be localized. The method returns
a React.Component instance which has props for attributes and
props.children for, well, child elements. We could traverse this
virtual DOM similar to how we do it with the regular DOM in L20n's HTML
bindings and apply translation logic where needed. We could even
re-implement the whole DOM overlay mechanism to operate on React's
virtual DOM.
(No example implementation here yet!)
4. Real DOM manipulation via lifecycle methods
----------------------------------------------
What if we wanted to re-use more of the existing L20n code? We could
apply translations to the true DOM when React renders it. Two
lifecycle methods are perfect for this: componentDidMount() and
componentDidUpdate(). I've been having some trouble getting the latter
to work with High Order Components but I think the general idea is
sound.
In this scenario we keep most of the current L20n intact and only use
React as a mechanism to monitor changes to the DOM. A mutation
observer, really! Indeed, in this approach we could remove L20n's
internal mutation observer (if it's not needed by other pieces of the
UI, like web components with Shadow DOM) and only rely on React to
notify us about re-renders.
An implementation lives at:
http://stasm.github.io/l20n-react-experiments/componentDidUpdate/
5. Real DOM manipulation via mutation observer
----------------------------------------------
Lastly, as it turns out, not changing anything in L20n is also a viable
option for us to consider! When a component with data-l10n-id is
rendered or re-rendered, l20n's mutation observer picks up the change
and translates the DOM node. This means that L20n is completely
separate from React.
An implementation lives at:
http://stasm.github.io/l20n-react-experiments/mutation/
Conclusions
===========
The above examples are still WIP. I'd like to better understand how
changes to the state should be propagated to translations which need to
be formatted anew and inserted into the DOM. I'll continue researching
this topic.
Judging from my initial explorations, I like the component abstraction
as presented in #1 although understandably this approach would require
a lot of changes in L20n. Namely, we'd need a whole new React DOM
bindings. I also like the <TranslationProvider /> idea as it
translates well into L20n's concept of lightweight per-view or
per-component contexts. Lastly, option #5 is also interesting: we
don't have to do anything really to support it and it might be a great
first step into testing L20n in real React applications and learning in
more detail what we can do to make this experience easier.
Let's discus.
—Staś
_______________________________________________
tools-l10n mailing list
[email protected]
https://lists.mozilla.org/listinfo/tools-l10n