fluent.js icon indicating copy to clipboard operation
fluent.js copied to clipboard

Compile-time issues with React tutorial

Open sandalwoodbox opened this issue 4 years ago • 4 comments
trafficstars

In https://github.com/projectfluent/fluent.js/wiki/React-Tutorial, I had to make the following adjustments to the final generateBundles function to get it to compile:

Current:

// Generate bundles for each locale.
async function generateBundles() {
    return languages.map(locale => {
        const translations = await getMessages(locale);
        const bundle = new FluentBundle(locale);
        bundle.addMessages(translations);
        return bundle;
    });
}

Fixed:

// Generate bundles for each locale.
async function generateBundles() {
  return languages.map(async (locale) => {
    const translations = await getMessages(locale);
    const bundle = new FluentBundle(locale);
    bundle.addResource(new FluentResource(translations));
    return bundle;
  });
}

At first I was getting Unexpected reserved word 'await' for getMessages; I fixed that by marking the map call as async. I then had to update addMessages to addResource because addMessages doesn't exist.

sandalwoodbox avatar Apr 03 '21 17:04 sandalwoodbox

This is not a compile-time error, but the tutorial also leaves me with:

Failed prop type: The prop l10n is marked as required in LocalizationProvider, but its value is undefined.

It looks like I need to find a way to merge the final tutorial output with the ReactLocalization usage here: https://github.com/projectfluent/fluent.js/wiki/React-Bindings

sandalwoodbox avatar Apr 03 '21 18:04 sandalwoodbox

It looks like the ReactLocalization class requires synchronicity, since it only supports CachedSyncIterable / mapBundleSync: https://github.com/projectfluent/fluent.js/blob/master/fluent-react/src/localization.ts#L25

sandalwoodbox avatar Apr 03 '21 20:04 sandalwoodbox

If anyone else runs into this, I was able to work around it by wrapping LocalizationProvider in a component that handles loading the bundles asynchronously.

import React from "react";
import "intl-pluralrules";
import { negotiateLanguages } from "@fluent/langneg";
import { FluentBundle, FluentResource } from "@fluent/bundle";
import { LocalizationProvider, ReactLocalization } from "@fluent/react";
import PropTypes from "prop-types";

const AVAILABLE_LOCALES = ["en-US", "zh-CN"];

// Negotiate user language.
const languages = negotiateLanguages(navigator.languages, AVAILABLE_LOCALES, {
  defaultLocale: "en-US",
});

// Load locales from files.
async function getMessages(locale) {
  const url = `/static/locale/${locale}/content.ftl`;
  const response = await fetch(url);
  return await response.text();
}

// Generate bundles for each locale.
async function generateBundles() {
  return languages.map(async (locale) => {
    const translations = await getMessages(locale);
    const bundle = new FluentBundle(locale);
    bundle.addResource(new FluentResource(translations));
    return bundle;
  });
}

class BundleLoader extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      l10n: new ReactLocalization([]),
    };
  }

  componentDidMount() {
    generateBundles().then((bundlePromises) => {
      Promise.all(bundlePromises).then((bundles) => {
        console.log(bundles)
        this.setState({ l10n: new ReactLocalization(bundles) });
      })
    });
  }

  render() {
    return (
      <LocalizationProvider l10n={this.state.l10n}>
        {this.props.children}
      </LocalizationProvider>
    );
  }
}

BundleLoader.propTypes = {
  children: PropTypes.element.isRequired,
};

export default BundleLoader;

sandalwoodbox avatar Apr 03 '21 21:04 sandalwoodbox

@sandalwoodbox thanks for you code, it helped a lot! I had to remove staticin your URL to make it work. @Pike or @stasm, I did the setup in Typescript within my App component and I think its parts or the whole below code would fit nicely into the following wiki pages:

  • https://github.com/projectfluent/fluent.js/wiki/React-Bindings
  • Especially: https://github.com/projectfluent/fluent.js/wiki/React-Bindings#a-complete-example
  • https://github.com/projectfluent/fluent.js/wiki/React-Tutorial

Here the TS code:

import React, {ReactElement} from 'react';
import './App.css';
import ChatContainer from './components/chat-container/ChatContainer';
import {FluentBundle, FluentResource} from '@fluent/bundle';
import {negotiateLanguages} from '@fluent/langneg';
import {LocalizationProvider, ReactLocalization} from '@fluent/react';

interface State {
  l10n: ReactLocalization;
}

// eslint-disable-next-line @typescript-eslint/ban-types
export default class App extends React.Component<{}, State> {
  // eslint-disable-next-line @typescript-eslint/ban-types
  constructor(props: {}) {
    super(props);
    this.state = { l10n: new ReactLocalization([])};
  }

  // Load locales async from files
  private readonly getMessages = (locale: string): Promise<string> =>
    fetch(`/locales/${locale}.ftl`) // Must be in your public dir
      .then((response: Response) => response.text())
      .catch(e => {
        console.error(e);
        return '';
      });

  // Generate bundles for each locale
  private readonly generateBundles = (userLocales: string[]): Promise<FluentBundle[]> => {
    // Choose locales that are best for the user.
    const currentLocales = negotiateLanguages(
      userLocales,
      ['de-DE'], // You know why only German is allowed here ;)
      { defaultLocale: 'de-DE' }
    );

    return Promise.all(currentLocales.map(async (locale: string) =>
      this.getMessages(locale)
        .then((translations: string) => {
          const bundle: FluentBundle = new FluentBundle(locale);
          bundle.addResource(new FluentResource(translations));
          return bundle;
        })));
  };

  componentDidMount(): void {
    this.generateBundles(navigator.languages as string[])
      .then((fluentBundles: FluentBundle[]) => {
        this.setState({ l10n: new ReactLocalization(fluentBundles) });
      })
      .catch(e => console.error(e));
  }

  render = (): ReactElement =>
    // @ts-ignore
    <LocalizationProvider l10n={this.state.l10n}>
      // The below code works also in all nested components, try it!
      <Localized id={'your-.ftl-file-key'}>
        your-.ftl-file-translation-value
      </Localized>
    </LocalizationProvider>;
}

I tried to find how I can update the wiki pages with the gh-pages branch, but editing the raw HTML seams to be an overhead. Is there another way?

ManuelTS avatar Feb 07 '22 19:02 ManuelTS