Skip to content

fluent-react: Introduce the Placeholder component #49

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

Closed
wants to merge 1 commit into from

Conversation

stasm
Copy link
Contributor

@stasm stasm commented Jul 14, 2017

Placeholders can be used as return values from custom functions passed into MessageContext. Localized.render will try to match them in order with original children of the wrapped component.

Given the following HTML/JSX:

<Localized id="warning-upgrade">
  <p>
    <a href="https://www.mozilla.org/firefox/">Upgrade Firefox</a> to get started.
  </p>
</Localized>

The translation may look like this:

warning-upgrade =
    { LINK("Upgrade Firefox", title: "New version of Firefox is available!") }
    to get started.

The LINK function is defined in the MessageContext constructor:

import { MessageContext } from 'fluent';
import negotiateLanguages from 'fluent-langneg';
import { Placeholder } from 'fluent-react';

export function* generateMessages(userLocales) {
  // Choose locales that are best for the user.
  const currentLocales = negotiateLanguages(
    userLocales,
    ['en-US', 'pl'],
    { defaultLocale: 'en-US' }
  );

  for (const locale of currentLocales) {
    const cx = new MessageContext(locale, {
      functions: {
        // Text is the only positional argument we care about.
        // Title is the only named argument we care about.
        // This gives us filtering unsafe props (like href) for free!
        LINK: function([text], {title}) {
          return <Placeholder title={title}>{text}</Placeholder>;
        }
      }
    });
    cx.addMessages(MESSAGES_ALL[locale]);
    yield cx;
  }
}

Placeholders can be used as return values from custom functions passed
into MessageContext.  Localized.render will try to match them in order
with original children of the wrapped component.

Given the following HTML/JSX:

    <Localized id="warning-upgrade">
        <p>
            <a href="https://www.mozilla.org/firefox/">Upgrade Firefox</a> to get started.
        </p>
    </Localized>

The translation may look like this:

    warning-upgrade =
        { LINK("Upgrade Firefox", title: "New version of Firefox is available!") }
        to get started.

The LINK function is defined in the MessageContext constructor:

    import { MessageContext } from 'fluent';
    import negotiateLanguages from 'fluent-langneg';
    import { Placeholder } from 'fluent-react';

    export function* generateMessages(userLocales) {
      // Choose locales that are best for the user.
      const currentLocales = negotiateLanguages(
        userLocales,
        ['en-US', 'pl'],
        { defaultLocale: 'en-US' }
      );

      for (const locale of currentLocales) {
        const cx = new MessageContext(locale, {
          functions: {
            // Text is the only positional argument we care about.
            // Title is the only named argument we care about.
            // This gives us filtering unsafe props (like href) for free!
            LINK: function([text], {title}) {
              return <Placeholder title={title}>{text}</Placeholder>;
            }
          }
        });
        cx.addMessages(MESSAGES_ALL[locale]);
        yield cx;
      }
    }
@stasm
Copy link
Contributor Author

stasm commented Jul 14, 2017

This approach solves the immediate issue of strings being split into two as seen in https://github.com/mozilla/testpilot/pull/2611/files#diff-fa0fe0311fc532fa1c84a7f9414af2eaL241. Using Test Pilot as real-life data it seems like this approach is good enough for a huge number of cases for which we don't have a good solution right now. It has some limitations, though:

  • Placeholders are matched with children of the wrapped component in-order; in the future we could allow a special named argument/prop index to fix this. For now, I think in-order should be fine for the majority of cases.

  • It's not possible to have nested markup. I don't think there's an easy way around this. In other words, the following won't work:

    <Localized id="warning-upgrade">
      <p>
        <a href="https://www.mozilla.org/firefox/">Upgrade <em>Firefox</em></a> to get started.
      </p>
    </Localized>

    Values of named arguments in Fluent cannot have interpolations themselves. This is not valid FTL:

    warning-upgrade =
        { LINK("Upgrade { EM("Firefox") }", title: "New version of Firefox is available!") }
        to get started.
    

    Again, real-life demand for this should be low. If absolutely necessary, the work-around can be to split the string and pass the nested markup as an argument.

  • If original children are defined inside of the wrapped component, we can't easily use the component for string extraction. As a workaround Localized could take a value prop with the English value of the translation for this component:

    <Localized id="warning-upgrade" value={'{ LINK("Upgrade { EM("Firefox") }", title: "New version of Firefox is available!") } to get started.'}>
      <p>
        <a href="https://www.mozilla.org/firefox/">Upgrade <em>Firefox</em></a> to get started.
      </p>
    </Localized>
  • Because there is no parsing happening in the translation, this approach doesn't allow arbitrary HTML in the translation. It's not possible to put a lot of markup in the translation nor is it possible for the localizer to add markup which doesn't exist in the source. For instance, the proper French spelling of abbreviated Madame should be Mme which uses the <sup> tag. As a workaround the developer can add custom functions, like SUP, to the MessageContext constructor. In the near future, fluent-react could define a set of good default functions (following the list defined by W3C: http://w3c.github.io/html/textlevel-semantics.html#textlevel-semantics) allowing inline HTML:

    new MessageContext(locales, {
      functions: {
        EM: ([text]) => <em>{text}</em>,
        STRONG: ([text]) => <strong>{text}</strong>,
        SUP: ([text]) => <sup>{text}</sup>,
      }
    });

@stasm
Copy link
Contributor Author

stasm commented Jul 14, 2017

@zbraniecki, @Pike, @fzzzy, @mathjazz, @flodolo: I'm looking for feedback on this approach. Thanks!

@Pike
Copy link
Contributor

Pike commented Jul 14, 2017

I don't think we should invest too much into context functions, and keep DOM fragments in sight as the long term solution.

Which makes me wonder ..., what's the actual blocker to DOM fragments? You've figured out the src-to-l10n matching, do we really need the context methods to set markers, or could we go back and use a template element and proper parsing? Maybe with the same constraints like here, that we don't allow nested elements etc?

Asking, mostly because I really don't know, and I'm afraid I'd need to learn and understand React to investigate myself.

@stasm
Copy link
Contributor Author

stasm commented Jul 14, 2017

Which makes me wonder ..., what's the actual blocker to DOM fragments?

Any string-to-DOM parsing doesn't work well with MessageContext.formatToParts. If the translation is <em>Foo { $link } Bar</em>, the parts are: "<em>Foo ", React.Element and " Bar</em>". In order to properly parse the em element we'd have to run some king of post-processing: replace interpolated elements with temporary placeholders {0}, {1} etc., join the parts into a string, parse, an replace the placeholders with the elements.

@stasm
Copy link
Contributor Author

stasm commented Jul 14, 2017

Which leads me to think that the full DOM fragments solution might not even be achievable if we keep formatToParts.

@Pike
Copy link
Contributor

Pike commented Jul 14, 2017

Or that doing both DOM fragments and React.Element in the same string is not supported?

@stasm
Copy link
Contributor Author

stasm commented Jul 14, 2017

Yup.

@zbraniecki
Copy link
Collaborator

So, I don't think I understand React enough to judge, but coming form vanilla Fluent, the fact that the syntax for functions is abused here looks awkward.

My gut reaction is to try to push for DOM Overlays instead, but from @stasm I understand that it may not be ready anytime soon.

Or that doing both DOM fragments and React.Element in the same string is not supported?

That doesn't sound that bad, does it?

@stasm
Copy link
Contributor Author

stasm commented Dec 19, 2017

I'm glad that we didn't move forward with this PR. In #101 I landed a simple implementation of DOM overlays for fluent-react and I outlined the plan for the future iterations in #103. DOM overlays are already more powerful than what this PR offered and don't have its limitations. I also removed MessageContext.formatToParts; the only way to to retrieve translations is now via MessageContext.format now, which always returns strings or null.

Read more about the new approach on the wiki: Overlays.

@stasm stasm closed this Dec 19, 2017
@stasm stasm deleted the placeholder branch January 24, 2018 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants