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.

@nkbt
Copy link
Author

nkbt commented Apr 6, 2016

Very far from perfect, super verbose but working solution I came up with so far:

const TermsOfService = () => {
  const template = i18n.t('By clicking \'CREATE ACCOUNT\', you accept our __TermsOfServiceUrl__');
  const [start, end] = template.split('__TermsOfServiceUrl__');

  return (
    <span>
      {start}<a href="/terms-of-service">{i18n.t('Terms of Service')}</a>{end}
    </span>
  );
};

I could probably abstract it and have some wrapper to use in the app.

@roddeh
Copy link

roddeh commented Apr 6, 2016

Could you make it generic with something like?

<PhraseWithLink phrase={i18n('By clicking SignUp, you accept our <linkstart>Terms of Service</linkend>)} link="/terms-of-service">

@nkbt
Copy link
Author

nkbt commented Apr 6, 2016

@roddeh Yeah, looks like it asks for some basic abstraction/automation. I updated my comment above to avoid splitting by translated string and now basically manage i18n-templating manually. Was curious if there is any common solution to the problem, since quick googling did not give me what I want

@roddeh
Copy link

roddeh commented Apr 6, 2016

Definitely prefer your updated solution... The previous solution just felt too fuzzy. If people happened to translate the standalone translation of the string you are splitting on differently to the inline version then it all breaks down.

@nkbt
Copy link
Author

nkbt commented Apr 6, 2016

Looks like KhanAcademy has a more generic solution for this: https://github.com/Khan/khan-exercises/blob/master/local-only/i18n.js#L87-L134

@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