Skip to content

Instantly share code, notes, and snippets.

@Gregoor
Last active April 15, 2020 10:04
Show Gist options
  • Save Gregoor/679e751a7ab0e0d347b11b6a046aaf82 to your computer and use it in GitHub Desktop.
Save Gregoor/679e751a7ab0e0d347b11b6a046aaf82 to your computer and use it in GitHub Desktop.
RFC: Fluent loader for Webpack/Rollup

Summary

Create a Fluent loader for webpack (or parcel), allowing developers to write co-located and typed translations.

Motivation

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

Guide-level explanation

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>
  );
}

Reference-level explanation

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;
}

Drawbacks

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.

Unresolved questions

How to make it work well with Pontoon?

@Gregoor
Copy link
Author

Gregoor commented Apr 15, 2020

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?

import * as React from "react";

import T from "./banner.ftl";

function Banner() {
  return (
    <div>
      <T.title>
        <h2 />
      </T.title>
      <T.body>
        <p />
      </T.body>
    </div>
  );
}

Inspired by projectfluent/fluent.js#475

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment