Skip to content
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

Help wanted Using react-intl #1227

Open
folivi opened this issue Dec 10, 2016 · 43 comments
Open

Help wanted Using react-intl #1227

folivi opened this issue Dec 10, 2016 · 43 comments

Comments

@folivi
Copy link

folivi commented Dec 10, 2016

Note from Maintainers

Please try the solution in #1227 (comment) and let us know if it works well for you.


Hi all,

I'm following this tutorial https://medium.freecodecamp.com/internationalization-in-react-7264738274a0#.pgfd03878 to use internationalization in my app.
Apparently the integreation is done by modifying the .babelrc file but to do so I have to eject, and I prefer not to.
Is there any way to use React-Intl without ejecting create-react-app?

@robcaldecott
Copy link

You only need the Babel plugin to generate the translate files which you can do as part of your production build without ejecting although it's a little messy. I'll post how later.

@EnoahNetzach
Copy link
Contributor

EnoahNetzach commented Dec 10, 2016

You can just install the required dependencies, add your own .babelrc, and add your "script": "translation" to your package.json.

E.g.:

npm install --save react-intl
npm install --save-dev babel-cli babel-preset-react-app babel-plugin-react-intl

add to the root folder:

{
  "presets": ["react-app"],
  "plugins": [
    [
      "react-intl", {
        "messagesDir": "translations/messages",
        "enforceDescriptions": false
      }
    ]
  ]
}

then add to your package.json's "scripts" this line:

"translate": "NODE_ENV=production babel ./src >/dev/null"

You should be able to run both npm start/npm run build for running/building the project, and npm run translate for generating the translations.

@firaskrichi
Copy link

@EnoahNetzach thanks for the answer!

How does : "NODE_ENV=production babel ./src >/dev/null" generate the translation? How does it work exactly?

Thank you 🙏

@arshmakker
Copy link

hi,

Internationlization is a pretty important feature for mobile apps these days. Could we have react-intl inbuilt in the create-react flow?

Regards
Arsh

@EnoahNetzach
Copy link
Contributor

@firaskrichi it works as intended by react-intl, by using babel's AST to catch translations and replace them (I suggest you to look it up, it's really interesting, if you like to know how exactly it - and in general babel plugins - works!).
The > /dev/null part is there simply to discharge the babel parsed output.

Moreover what I suggested is simply what the official doc tells to do ;)

@arshmakker being that it's easily pluggable, not everybody wants to use i18n, and the specific tool (format.js) is not that reliable if you want to do substantial i18n (@see), I'm not sure at all it would be possible.

@draperd
Copy link

draperd commented Feb 10, 2017

This is a really useful discussion and the information provided by @EnoahNetzach has really helped get react-intl into my create-react-app application. I think it would be exceptionally valuable to have i18n working out-of-the-box with create-react-app (or at least some good documentation explaining how to use it).

The main thing that I don't quite understand is how this works with a production build. I've followed the various scripts to combine individual component message files into a single JSON file for the default locale, I've then added a different locale (with translated messages) and updated the IntlProvider locale to be the new locale.

When you use npm run build the new locale translations are lost (probably because the translate scripts provided by the blogs combine the component messages into locale files in the build directory).

If anyone can shed any light on the correct approach to combining the information provided so far in a production story I'd be very grateful.

@EnoahNetzach
Copy link
Contributor

@draperd, as a non English native speaker, I find that unfortunately no i18n instrument in js is a "catch all" solution.
I've used a couple and always ended up tweaking them to my needs (e.g. subject/direct object gender and numerosity, probably @gaearon could say something about Russian desinences, I don't know if there is someone from Iceland here, and so on and so forth..).
Whichever translation package we could add, it's going to solve only a limited set of cases and needs, so I'm against adding them in altogether, specially when the integration is not "that" painful.

@draperd
Copy link

draperd commented Feb 10, 2017

@EnoahNetzach Thanks for your response... I wasn't suggesting that there is a one-size fits all solution, but it would seem that React-Intl is a pretty good fit. I can understand the rationale of not wanting to tie "create-react-app" to a single solution, however a solution of some kind is almost going to be required. Any serious application is going to want to support i18n so I'm simply suggesting that either something is provided by default, or that documentation is provided to assist people in achieving a solution.

The information you have previously provided has been invaluable but only as far as the development experience goes. My point is that when I'm ready to ship my finished application I need translation to work in production for multiple locales and I've not been able to find that information anywhere so far.

I'll continue to experiment and hopefully find a working solution which I'll then write up, I was just hoping that someone may have already encountered and solved this problem using react-intl.

@draperd
Copy link

draperd commented Feb 13, 2017

Just to follow up my previous comment... what I actually need is a per component solution which I've now managed to achieve using the higher order component pattern. I will write this solution up as soon as I can for anyone that comes across this thread as it might be useful.

@EnoahNetzach
Copy link
Contributor

@draperd good!

@draperd
Copy link

draperd commented Feb 13, 2017

As promised... hopefully this might be of use to some people: https://medium.com/@dave.draper20/i18n-per-component-in-react-using-the-higher-order-component-pattern-b04f32e7cd6a#.ewrplesd7

@sthuber90
Copy link

For everyone that the suggestion from @EnoahNetzach is not working out of the box I had to put following in my package.json, probably as I'm working on windows

"translate": "cross-env NODE_ENV=production ./node_modules/.bin/babel ./src > NUL",

@EnoahNetzach
Copy link
Contributor

Yep, env var definition and the output indirection to /dev/null work only in Linux/Mac.

cross-env solves the first part, for the second sincerely I have no idea how to accomplish that on Windows..

@cwalv
Copy link

cwalv commented Mar 18, 2017

If you don't want to have to npm install --save-dev babel-cli or create the babel.rc file, or worry about issues on Windows, you can use the babel API:

// scripts/translate.js

process.env.NODE_ENV = 'development';

const fs = require('fs');
const path = require('path');
const globSync = require('glob').sync;
const mkdirpSync = require('mkdirp').sync;
const transformFileSync = require('babel-core').transformFileSync;

const paths = require('../config/paths');

//////////////////////////////
// Add to `/config/paths.js`:
//  appBuildMessages: .. where to output the messages
//////////////////////////////

globSync(path.join(paths.appSrc, '/**/*.js'))
    .map((filename) => {
        const result = transformFileSync(filename, {
            plugins: ['react-intl']
        });
        const messages = result.metadata["react-intl"]["messages"];
        if (messages.length > 0) {
            const relFn = path.relative(paths.appSrc, filename);
            const outFilename = path.join(
                paths.appBuildMessages,
                path.dirname(relFn), `${path.basename(relFn, ".js")}.json`);
            mkdirpSync(path.dirname(outFilename));
            const outFd = fs.openSync(outFilename, 'w');
            fs.write(outFd, JSON.stringify(messages));
        }
    });
...
  "scripts": {
    ...,
    "translate": "node scripts/translate.js",
   }

@arshmakker
Copy link

I tried the .bablerc solution, but it does not work unless and untill i do an eject, is that just me?

@bmueller-sykes
Copy link

@EnoahNetzach I tried your solution of using a translate script, but I wound up with a "missing script: translate" error. I don't know if this means your solution is no longer valid with the latest version of CRA, or if I have missed a detail.

In my package.json file, I have:

  "scripts": {
    "translate": "NODE_ENV=production babel ./src >/dev/null"
  },

...and am running react 15.5.4, react-scripts 0.9.5, babel-cli 6.24.1, babel-plugin-react-intl 2.3.1, and babel-preset-react-app 2.2.0. My .babelrc sits at the root of my project folder, and is verbatim what you posted above:

{
  "presets": ["react-app"],
  "plugins": [
    [
      "react-intl", {
       "messagesDir": "translations/messages",
       "enforceDescriptions": false
      }
    ]
  ]
}

...and then here's the CLI:
screen shot 2017-04-17 at 4 08 33 pm

Have I missed something obvious?

@bmueller-sykes
Copy link

@cwalv question about your solution: it appears that you're outputting a separate .json file for each .js file in which you find translatable content (if I'm reading your code right). Is there a reason for doing that as opposed to putting all translatable content into a single .json file? I'm just getting into i18n, so I don't know if you're following an accepted practice, or if that's just a personal preference on your part.

Thanks!

@cwalv
Copy link

cwalv commented Apr 21, 2017

@bmueller-sykes, you're right, I only put in half the script. I adapted it from https://github.com/yahoo/react-intl/blob/b82a899a59c762fcc590e1fa824b87da6ad409d9/examples/translations/scripts/translate.js, which adds this to the .babelrc file:

    "plugins": [
        ["react-intl", {
            "messagesDir": "./build/messages/"
        }]

which creates the .json files when babel runs, similar to how my snippet above does, and then the translate script has a second pass that aggregates separate files into one:

// Aggregates the default messages that were extracted from the example app's
// React components via the React Intl Babel plugin. An error will be thrown if
// there are messages in different components that use the same `id`. The result
// is a flat collection of `id: message` pairs for the app's default locale.
const messagesGlob = path.join(paths.appBuildMessages, '**', '*.json');
let defaultMessages = globSync(messagesGlob)
    .map((filename) => fs.readFileSync(filename, 'utf8'))
    .map((file) => JSON.parse(file))
    .reduce((collection, descriptors) => {
        descriptors.forEach(({ id, defaultMessage }) => {
            if (collection.hasOwnProperty(id)) {
                throw new Error(`Duplicate message id: ${id}`);
            }
            collection[id] = defaultMessage;
        });
        return collection;
    }, {});

// For the purpose of this example a fake locale: `en-UPPER` is created and
// the app's default messages are "translated" into this new "locale" by simply
// UPPERCASING all of the message text. For a real translation this would mean some
// offline process to get the app's messages translated by machine or
// professional translators.
let uppercaseTranslator = new Translator((text) => text.toUpperCase());
let uppercaseMessages = Object.keys(defaultMessages)
    .map((id) => [id, defaultMessages[id]])
    .reduce((collection, [id, defaultMessage]) => {
        collection[id] = uppercaseTranslator.translate(defaultMessage);
        return collection;
    }, {});

mkdirpSync(LANG_DIR);
fs.writeFileSync(LANG_DIR + 'en-US.json', JSON.stringify(defaultMessages, null, 2));
fs.writeFileSync(LANG_DIR + 'en-UPPER.json', JSON.stringify(uppercaseMessages, null, 2));

@evenchange4
Copy link
Contributor

evenchange4 commented Apr 27, 2017

I released a workaround solution to NPM react-intl-cra, so that you can simply extract messages of CRA-project from the command line.

$ yarn add react-intl-cra --dev
$ react-intl-cra './src/**/*.js' -o messages.json

DEMO

@arshmakker
Copy link

Thanks @evenchange4 , that npm package really helped my case.

I was struggling with intl after shifting to react-scripts.

REgards
ARsh

@gaearon
Copy link
Contributor

gaearon commented May 25, 2017

This looks really great! Perhaps we should put it into docs?

@bmueller-sykes
Copy link

FWIW, I wound up writing a script for this as well, based on the work from above. It runs at the command line, and trolls through everything in the src folder and outputs all translatable instances into a single JSON file (currently hardwired to en-for-translation.json). I don't know if there's a reason to split up translations across multiple files, and at least for my purposes, I didn't need to. If this is useful to people, please use it!

// This file needs to be at the root of the React project
process.env.NODE_ENV = 'development';

const path = require('path');
const globSync = require('glob').sync;
const fs = require('fs');
const mkdirpSync = require('mkdirp').sync;
const babel = require('babel-core');
const jsonFormat = require('json-format');
const plugin = require('babel-plugin-react-intl');
const _ = require('lodash');
const colors = require('colors');

const paths = {
  root: `${process.env.PWD}/src/`,
  output: `${process.env.PWD}/localization/`
}

const all = []
var totalNodes = 0;

globSync(path.join(paths.root, '**/*.js')).map ( (filename) => {
  const result = babel.transformFileSync(filename, {
    presets: ['react'],
    plugins: ['transform-object-rest-spread','transform-class-properties',plugin.default],
  });
  const meta = result.metadata['react-intl'];

  if (meta.messages.length > 0) {
    meta.messages.map ( (message) => {
      const existing = _.find( all, (m) => m.id === message.id );
      if (existing) {
        console.log(`*** ERROR: Duplicate ID found: ${message.id} ***`.bold.red)
        console.log(`Look in file ${filename} to find duplicate`.red)
      } else {
        totalNodes++
        all.push(message);
      }
      return true;
    })
  }
  return true;
})

const outputFileName = `${paths.output}en-for-translation.json`;
mkdirpSync(path.dirname(outputFileName));
fs.writeFile(outputFileName, jsonFormat(all, {type: 'tab', size: 1}), (error) => {
  if (error) {
    console.log('*** ERROR WRITING FILE'.bold.red.underline);
    console.log('ERROR', error);
  } else {
    console.log(`*** DONE`.bold.green);
    console.log(`${totalNodes} messages ready for translation`.green);
    console.log(`File written ${outputFileName}`.green);
  }
});

@evenchange4
Copy link
Contributor

evenchange4 commented May 26, 2017

@arshmakker, thank you for trying. If there is any problem please feel free to report it to here currently. Maybe I will move it out to a stand-alone repository later.

@gaearon I am happy to send a PR for I18n document improvement. 😀


Update (2017-12-14): Moved it to https://github.com/evenchange4/react-intl-cra

@johnrasku
Copy link

Thank you @bmueller-sykes. Works like a charm.

@stereobooster
Copy link
Contributor

stereobooster commented Dec 12, 2017

To sum up: it would be nice to have standalone CLI (or API to write one) to extract all translation keys to JSON file. react-intl-cra is a nice try, except I would prefer standalone repository for this package (it seems to be embedded into mcs-lite).

See no reason why React Intl can not provide official solution for this issue. Can we have this as standalone CLI instead of Babel plugin?

@evenchange4
Copy link
Contributor

@stereobooster Thanks for suggestions! I have received some issues in the monorepo, so I moved it to standalone repository https://github.com/evenchange4/react-intl-cra for better issue tracking (after v0.2.12). 🙌

@gaearon
Copy link
Contributor

gaearon commented Jan 8, 2018

@evenchange4 How well does it work in practice? Should we start pointing people towards it?

@evenchange4
Copy link
Contributor

evenchange4 commented Jan 10, 2018

@gaearon I use the react-intl-cra CLI in all my CRA projects (include an OSS one), but I still mark it as a workaround solution.

I notice that babel-plugin-macros have been merged today. I will take some time to try it with this babel-related problem.

@gaearon
Copy link
Contributor

gaearon commented Jan 10, 2018

Nice, thanks.

@arshmakker
Copy link

Currently I am using the react-intl-cra in production at our end and have not face any issues so far.

@evenchange4
Copy link
Contributor

evenchange4 commented Jan 25, 2018

Here is the macro solution POC:

Your code

// Component.js
-import {defineMessages} from 'react-Intl';
+import { defineMessages } from 'react-intl.macro';

const messages = defineMessages({
  'Component.greet': {
    id: 'Component.greet',
    defaultMessage: 'Hello, {name}!',
    description: 'Greeting to welcome the user to the app',
  },
});

Extract messages

// package.json
"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
+ "extract": "MESSAGE_DIR='./.messages' react-scripts build"
},

Pros and Cons

The react-intl-cra CLI is pretty easy to use, but it only works with CRA project.
The react-intl.macro is more flexible even working with others (e.g.,Next.js).

References

GitHub: https://github.com/evenchange4/react-intl.macro
Example: https://github.com/evenchange4/react-intl.macro-example
Demo: https://react-intlmacro.netlify.com

@dachinat
Copy link

dachinat commented Mar 31, 2018

May I know why does react-intl-cra and other examples here generate in

[
  {
    "id": "...",
    "defaultMessage": "..."
  }
]

format?

If you do import translations from './i18n/messages.json'; and <IntlProvider locale="en" messages={translations}> it won't work. It accepts key value paris like

{
  "...": "..."
}

Thanks.

@kopax
Copy link

kopax commented Apr 9, 2018

@EnoahNetzach I have tried with your solution and

Error: Couldn't find preset "react-app" relative to directory 
"/home/dka/workspace/gitlab.example.com/dev-tools/test"

Should I install anything else in my project ? What plugin can I reuse and where are they stored ?

Did anybody made a workaround ? I dont' get why react-intl-cra './src/**/*.js' -o messages.json generated me this file :

[
  {
    "id": "test.action.resetViews",
    "defaultMessage": "Reset views",
    "filepath": "./src/messages.js"
  }
]

While I expect

{
  "test.action.resetViews": "Reset views"
}

I want to use react-intl. As @dachinat pointed oput, we need key:value pair.

How can I do without ejecting?

@noliiva
Copy link

noliiva commented May 7, 2018

Personnaly, I use the internal script extract-intl from react-boilerplate.
No need of babel-plugin-react-intl or react-intl-cra, just some adaptations according the project, like changing paths and install needed modules (mainly babel-cli) and voila. :D

"extract-intl": "babel-node --presets env,stage-0 -- ./internals/scripts/extract-intl.js"

@kopax
Copy link

kopax commented May 7, 2018

@noliiva I have placed this react-boilerplate script in a program that you can run through npx and it work without any dependency :

You must have in your package.json

+  "declinationId": "intl"

Then run

$ npx rollup-umd-scripts install fr en de ch vi --default=en

This will create in your package.json

+  "translation": {
+    "locale": "en",
+    "locales": [
+      "en",
+      "fr",
+      "de",
+      "ch",
+      "vi"
+    ]
  },

You can now extract what's in src directory with :

$ NODE_ENV=production npx rollup-umd-scripts intl extract

You can add the following scripts to your package.json to keep the snippet for any project:

+  "extract-intl": "NODE_ENV=production npx rollup-umd-scripts intl extract --target=src/i18n/translation",

--target is for changing the destination path for the generated json files.

It's basically the same as evenchange4 except with the standard format for json and without any install.

@arnaudriegert
Copy link

I'm following up on the questions asked by @dachinat and @kopax

From what I understand, there are two steps:

  1. Extract all the strings that need to be translated
  2. Generate a file that will store the translations for each language

In the gettext universe, the first step is the equivalent of generating the (single) .pot file while the second step is the generation of the (multiple) .po files.

The "messageId": "value" syntax would apply to the result of the second step, while babel-plugin-react-intl only does the first step.

I didn't think the second step would be so complicated to implement.

So far all I've found (outside of this discussion) is this software that provides a GUI to do it. I don't know what it is based on.
I was expecting the guys behind react-intl would provide a command such as translations-update -i build/**/*.json -o src/messages/fr.json.

Did I miss something here or is it really that complicated to have a translations file for each language?

@sunknudsen
Copy link

Looking for a way to extract messages from components localized using react-intl and manage translations effortlessly?

The following worked for me: formatjs/babel-plugin-react-intl#108 (comment)

@jamieallen59
Copy link

I was using the same tutorial and couldn't find a tutorial from start => finish without ejecting so wrote one here: https://medium.com/@jamieallen59/using-react-intl-with-create-react-app-6667ee3e19f3. Hope it helps.

@shsunmoonlee
Copy link

@noliiva I have placed this react-boilerplate script in a program that you can run through npx and it work without any dependency :

$ npx rollup-umd-scripts install fr en de ch vi --default=en

This will create in your package.json

+  "translation": {
+    "locale": "en",
+    "locales": [
+      "en",
+      "fr",
+      "de",
+      "ch",
+      "vi"
+    ]
  },

You can now extract what's in src directory with :

$ NODE_ENV=production npx rollup-umd-scripts intl extract

You can add the following scripts to your package.json to keep the snippet for any project:

+  "extract-intl": "NODE_ENV=production npx rollup-umd-scripts intl extract --target=src/i18n/translation",

--target is for changing the destination path for the generated json files.

It's basically the same as evenchange4 except with the standard format for json and without any install.

Hey I get error saying ' You must use a intl declination to use this command!'

@kopax
Copy link

kopax commented Apr 26, 2019

@SeunghunSunmoonLee sorry I did a change in that script, you must have in your package.json

+  "declinationId": "intl"

It wont break anymore, otherwise you can fix to an older version.

@erikhansen
Copy link

@jamieallen59 Your tutorial worked great for me. Thanks.

@mihanizm56
Copy link

mihanizm56 commented Aug 11, 2019

@jamieallen59
You use react-intl v2 version in your guide. Nowadays in v3 there are a lot of APIs have breaking changes

@lucasbasquerotto
Copy link

In my case I'm able to use react-intl with create-react-app by just including 2 simple scripts to extract and compile the translation files:

scripts/i18n-extract.sh:

#!/bin/bash
lang='en'

npm run i18n:formatjs:extract \
	-- 'src/**/*.ts*' --ignore 'src/**/*.d.ts*' \
	--out-file src/src-lang/"$lang".json \
	--id-interpolation-pattern '[sha512:contenthash:base64:6]'

* I define the extension as ts* because I'm using typescript, if you are using javascript just change it to js*

scripts/i18n-compile.sh:

#!/bin/bash
langs=( 'en' 'fr' 'ar' )

if [ "${1:-}" = 'default' ]; then
    langs=( 'en' )
fi

for lang in "${langs[@]}"; do
    src="src/src-lang/$lang.json"
    dest="src/lang/$lang.json"

    if [ ! -f "$src" ]; then
        echo "[warn] src file not found ($src)"
    else
	    npm run i18n:formatjs:compile -- "$src" --ast --out-file "$dest"
    fi
done

* In this case I'm considering the languages en, fr and ar (en being the default), you can change it to your use case.

And in package.json:

"scripts": {
    "start": "HTTPS=true react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "i18n:dev": "echo '{}' > src/lang/en.json",
    "i18n:extract": "bash ./scripts/i18n-extract.sh",
    "i18n:compile": "bash ./scripts/i18n-compile.sh",
    "i18n:formatjs:extract": "formatjs extract",
    "i18n:formatjs:compile": "formatjs compile"
}

I only need bash to be installed and everything works fine so far. I can run npm run i18n:extract in a CI environment to generate the file with the translations in the default language, and in another CI process, when deploying to staging/production I can retrieve the translation files (from wherever they are), put them at src/src-lang/ and call npm run i18n:compile to generate the final (compiled) translation files at src/lang/ (the files in these folders aren't versioned).

In smaller projects you can run npm run i18n:extract locally, put the generated file (default language) in a versioned folder and apply the translation on the other files in the folder based on the changes in the default language. Then when deploying in a CI environment you can just move the files from the versioned folder to src/src-lang/ and run npm run i18n:compile to generate the compiled translation files at src/lang/.

In development I just run npm run i18n:dev to create an empty json file ({}) so as to not receive errors when trying to import the file (the default language messages are already in the code, so the file doesn't need to have translations in the default language).

The only thing I haven't achieved was to be able to generate ids (https://formatjs.io/docs/guides/bundler-plugins), and use the generated ids in development (I don't know if this is possible without ejecting and stop using create-react-app, because it would need to transpile the source files to generate the ids). So, for now, I'm using static ids (if there is the same id for different messages I receive an error, so it's ok).

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

No branches or pull requests