Create a Fluent loader for webpack (or parcel), allowing developers to write co-located and typed translations.
Fluent does not currently offer a solution for developers to ship their translation messages to users. While this is great for inspiring experimentation in this space, it can also lead to developers rolling their own subpar solutions, which might have several of these shortcomings:
- Lack of code splitting - sending more messages to the client than are actually rendered
- "Dead" and verbose selectors - when all messages are in one (or very few) files it gets harder to sort out the unused one. It also turns naming into a game of over-explicitness to rule-out name collisions.
- String-based/implicit referencing - since declaring and shipping messages is usually decoupled from loading them, referencing happens through strings
This assumes you are using @fluent/react
:
Add fluent-loader
to your dependencies and bundler config. When rendering
<LocalizationProvider />
with ReactLocalization
, append fluent-loader
's
bundles to your preexisting ones. For example:
import { Children, useState } from "react";
import { negotiateLanguages } from "@fluent/langneg";
import { ReactLocalization, LocalizationProvider } from "@fluent/react";
import { useCurrentBundle } from "fluent-loader";
const AVAILABLE_LOCALES = ['en', 'de', 'pl'];
function AppLocalizationProvider(props) {
const [l10n, setL10n] = useState<ReactLocalization>(
() => new ReactLocalization(props.bundles)
);
const currentLocales = negotiateLanguages(
navigator.languages,
AVAILABLE_LOCALES,
{ defaultLocale: 'en' }
);
useCurrentBundle(currentLocales, (bundle) => {
setL10n(new ReactLocalization(props.bundles.concat(bundle)));
});
<LocalizationProvider l10n={l10n}>
{Children.only(props.children)}
</LocalizationProvider>;
}
Now you can co-locate your translation with your components:
# src/banner/banner.ftl
title = Welcome to the new thing
body = There is so much stuff in this thing
// src/banner/banner.tsx
import * as React from "react";
import t from "./banner.ftl";
export default function Banner() {
return (
<div>
<Localized id={t.title}>
<h2 />
</Localized>
<Localized id={t.body}>
<p />
</Localized>
</div>
);
}
Resolving the dependency graph would be outsourced to Webpack, but the loader
needs to combine all the bundles, generate unique IDs and set-up the mechanism
for the useCurrentBundle
function to work (i.e. probably some site-global
event sending).
To enable type-checking it needs to generate module definitions, for the above that would be:
declare module "src/banner/banner.ftl" {
export const title: id;
export const body: id;
}
At this point it's not clear to me whether it's possible to build this bundler agnostic. But Webpack is standard enough, that it's not a big worry for me.
How to make it work well with Pontoon?
Crazy ideas
Parsing at build-time
Since we're already doing more at build time, we might as well do even more. From my understanding a share of
@fluent/bundle
's bundle size and runtime cost comes down to parsing.ftl
. While it's probably not a lot, nothing is arguably even less. So could we move parsing into a compile-time step and only depend on a light-version of@fluent/bundle
at runtime?No more Localized
How about instead of exporting message IDs when loading
.ftl
-files, we export components instead?Inspired by projectfluent/fluent.js#475