Skip to content

Instantly share code, notes, and snippets.

@nkbt
Created April 6, 2016 04:31
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nkbt/c1624ef82ef1d3b4de93d477386a1cc9 to your computer and use it in GitHub Desktop.
Save nkbt/c1624ef82ef1d3b4de93d477386a1cc9 to your computer and use it in GitHub Desktop.

Hi all, we have a problem doing i18n with React. This is about template strings.

What we need:

<p>
  By clicking SignUp, you accept our <a href="/terms">Terms of Service</a>
</p>

How I would do it (not in React):

i18n.t('By clicking SignUp, you accept our {{terms}}', {
  terms: `<a href="/terms">${i18n.t('Terms of Service')}</a>`
})

Since React does not operate with strings/templates - this is not really possible to achieve.

Possible hacky, but not acceptable solution is

<p>
 {i18n.t('By clicking SignUp, you accept our')}
 <a href="/terms">{i18n.t('Terms of Service')}</a>
</p>

It is not acceptable in a long term since in other languages phrases are often arranged differently, so when you do translation you need to move <a> around the sentence to not sound silly.

@bholloway
Copy link

bholloway commented Feb 8, 2017

An investigation into tagged template literals.

Consider a pagination component where More and Less anchors are separately translated.

<span className={`${css.moreAndLess} ${className}`}>
  tgettext`Show
    ${<More key="more" {...{numMore, onMore}} />} or
    ${<Less key="less" {...{numLess, onLess}} />}`
</span>

Where the substitutions are non-strings we want the interpolation to return an Array. So long as the substitutions all have a key then React is happy.

Referring to this implementation:

export const tgettext = ng => (strings, ...values) => {
  const DELIMITER = '____';

  const untranslated = strings
    .map((v, i) => ((i < values.length) ? `${v}${DELIMITER}` : v))
    .join('');

  const translated = ng.gettext(untranslated)
    .split(new RegExp(`(${DELIMITER})`))
    .map((v, i) => ((i % 2) ? values[(i - 1) / 2] : v));

  const isText = translated
    .every(v => (typeof v === 'string'));

  return isText ? translated.join('') : translated;
};

The required translation is not very descriptive.

Show ____ or ____

We would be looking for those fields to be named. However I cannot see how that can be achieved conventionally with tagged template literals.

However we are lucky that we expect our substitutions to be React components. Each substitution must already have a key so ideally we would introspect this value and use it as the placeholder.

Meaning we translate:

Show __more__ or __less__

Magically that actually works. Here is the full solution:

export const tgettext = ng => (strings, ...substitutions) => {
  const keys = substitutions
    .map((v) => {
      const key = v && (typeof v === 'object') && (typeof v.key === 'string') && v.key;
      return key ? `__${key}__` : '____';
    });

  const untranslated = strings
    .map((v, i) => ((i < keys.length) ? `${v}${keys[i]}` : v))
    .join('')
    .replace(/\s{2,}/g, ' ');

  const translated = ng.gettext(untranslated);

  const substituted = keys
    .reduce((reduced, key, i) => {
      const completed = reduced.slice(0, -1);
      const split = reduced[reduced.length - 1].split(key);
      const unsplit = split.slice(1).join(key);
      return [...completed, split[0], i, unsplit];
    }, [translated])
    .map((v, i) => ((i % 2) ? substitutions[(i - 1) / 2] : v));

  const isText = substituted
    .every(v => (typeof v === 'string'));

  return isText ? substituted.join('') : substituted;
};

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