Πώς να ρυθμίσετε τη διεθνοποίηση στο React από την αρχή έως το τέλος

Αυτή η ανάρτηση θα react-intlσας βοηθήσει να μεταβείτε από create-react-appτη ρύθμιση του πλαισίου σε μια ολοκληρωμένη, μεταφρασμένη εφαρμογή ιστού!

Δεσμεύτηκα κώδικα καθώς έγραψα αυτήν την ανάρτηση, οπότε θα μπορείτε να δείτε το ιστορικό δεσμεύσεων για να δείτε εύκολα πώς εξελίχθηκε ο κώδικάς μου από την αρχή έως το τέλος.

Τι είναι η Διεθνοποίηση;

Δεδομένου ότι αποφασίσατε να κάνετε κλικ στον σύνδεσμο αυτής της ανάρτησης, είναι πιθανό τουλάχιστον να έχετε ιδέα τι είναι η διεθνοποίηση (i18n). Αφαιρέθηκε απευθείας από τον ιστότοπο του W3:

«Η διεθνοποίηση είναι ο σχεδιασμός και η ανάπτυξη περιεχομένου προϊόντος, εφαρμογής ή εγγράφου που επιτρέπει την εύκολη τοπική προσαρμογή για κοινό-στόχο που ποικίλλει σε πολιτισμό, περιοχή ή γλώσσα.»

Ως προγραμματιστής, θέλετε το περιεχόμενό σας να είναι ευανάγνωστο και χρησιμοποιήσιμο από όλα τα είδη ανθρώπων σε όλο τον κόσμο. Νομίζω ότι όλοι συμφωνούν με αυτό. Αλλά ξέρω τι σκέφτεστε:

«Η ανάπτυξη μιας εφαρμογής ιστού για άτομα της δικής μου κουλτούρας / περιοχής / γλώσσας είναι ήδη αρκετά δύσκολη! Δεν έχω τον χρόνο ή την προσπάθεια για το i18n! "

Βλέπετε ήδη το lingo κάτω. Ας ελπίσουμε ότι αυτή η ανάρτηση θα σας βοηθήσει να συνειδητοποιήσετε ότι η ρύθμιση του i18n για το έργο σας δεν είναι τόσο δύσκολη ή χρονοβόρα όσο φαίνεται.

Τι κάνει και δεν κάνει το react-intl

Εάν είστε νέοι στο i18n, μπορεί να έχετε κάποιες σκέψεις για το τι πιστεύετε ότι μια βιβλιοθήκη όπως react-intlθα έπρεπε και τι δεν πρέπει να κάνει.

Κάνει:

  • Βοηθήστε σας να συγκεντρώσετε όλο το διάσπαρτο περιεχόμενό σας, έτσι ώστε να μπορεί εύκολα να μεταφραστεί αργότερα
  • Θα σας βοηθήσουμε να μεταφράσετε κείμενο εκτός από ημερομηνίες, αριθμούς και ούτω καθεξής
  • Παρέχετε έναν εύκολο τρόπο εισαγωγής μεταφράσεων στην εφαρμογή σας

Δεν ειναι:

  • Μεταφράστε το περιεχόμενό σας για εσάς
  • Πείτε σας πώς να μάθετε ποιες τοπικές ρυθμίσεις θέλει ο χρήστης
  • Διορθώστε το άσχετο σφάλμα με το οποίο αντιμετωπίζετε τις τελευταίες δύο ώρες (αλήθεια, σωστά;)

Εντάξει, ας πάμε αμέσως!

Ρύθμιση του παραδείγματος έργου

$ npx create-react-app i18n-example

Πρόκειται να προσθέσω τον αντιδραστήρα αντίδρασης για να δείξω πώς react-intlλειτουργεί με πολλές σελίδες.

$ cd i18n-example && npm install react-router-dom

Η εφαρμογή μου για παράδειγμα θα έχει τρία στοιχεία React: μία κύρια σελίδα, μία υποσελίδα και ένα στοιχείο που εισάγεται στην υποσελίδα. Δείτε τη δομή αρχείων και τις παρακάτω σελίδες:

/src /components Weather.js /pages Home.js Day.js

Η κατάσταση του έργου μέχρι αυτό το σημείο μπορεί να βρεθεί εδώ.

Εγκαθιστώ react-intl

Τώρα, η διασκέδαση ξεκινά. Θα εγκαταστήσουμε react-intlκαι θα δουλέψουμε!

$ npm install react-intl

Ο κύριος στόχος πίσω react-intlείναι να επιτρέψετε την υποστήριξη για το i18n, ελαχιστοποιώντας παράλληλα την επίδραση στην κανονική ροή κωδικοποίησης. Σίγουρα, έχετε περιεχόμενο σε πολλά μέρη σε όλη την εφαρμογή ιστού σας. Έχετε κείμενο, αριθμούς και ημερομηνίες σε παραγράφους, πίνακες και κεφαλίδες.

Τι θα κάνατε αν έπρεπε να χτίσετε μια βιβλιοθήκη i18n; Λοιπόν, έχετε αυτά τα κομμάτια περιεχομένου σε όλη την εφαρμογή ιστού σας. Και θέλετε όλα να μεταφραστούν εύκολα. Εάν επρόκειτο να δώσετε το περιεχόμενό σας σε έναν μεταφραστή, δεν θα τους δώσατε τον κωδικό σας και θα πείτε «καλή τύχη, ξεκινήστε τη δουλειά».

Θα θέλατε να βρείτε έναν τρόπο να τοποθετήσετε όλο το περιεχόμενό σας σε ένα αρχείο και, στη συνέχεια, να τους δώσετε ένα αρχείο. Θα το μετέφραζαν σε άλλη γλώσσα, θα έλεγαν από Αγγλικά σε Ισπανικά και θα σας έδιναν ένα αρχείο με όλο το ισπανικό περιεχόμενο.

Εντάξει μια χαρά. Έτσι το κάνατε, αλλά τώρα πρέπει να πάρετε το ισπανικό περιεχόμενο σε αυτό το αρχείο και να το διανείμετε ξανά στην αρχική του θέση. Πώς θα το κάνατε μέσω προγραμματισμού; Ίσως θα εκχωρούσατε αναγνωριστικά σε κάθε κομμάτι περιεχομένου, ώστε να μην χάσετε την αρχική τοποθεσία κάθε κομμάτι περιεχομένου.

Και αυτό είναι λίγο πολύ!

Το πρώτο βήμα είναι να ολοκληρώσετε την εφαρμογή σας στο er> component:

Now, you need to identify the content for react-intl that will eventually be translated. On the home page of my app, I have the following paragraph:

It is a beautiful day outside.

I need to tell react-intl that this is content that I want to translate and give it an id, so that it can keep track of this content and its original location:

By default, the text will be outputted in a an> , so we will need to wrap this in the original

I will now do this for all the content in my web app.

The state of the project up until now can be found here.

Adding babel-plugin-react-intl

Now that we have everything set up, you might be wondering how we can easily aggregate all of that content into one file. However, for debugging purposes, it could be helpful to have individual JSON files for each React component. Guess what, there’s a babel plugin for that!

$ npm install babel-plugin-react-intl

This plugin will make a copy of your src directory, but instead of having your React component files, it will have json files with the message content and id. One for each component file in your src directory. It will do this when you run npm run build .

Now we need to eject from create-react-app, so that we can add our new plugin into our babel configuration. Make sure to commit any changes and then execute:

$ npm run eject

Now, we will need to add a .babelrc file in our project root with the following contents:

{ "presets":["react-app"], "plugins": [ ["react-intl", { "messagesDir": "./public/messages/" }] ] }

Now that babel can use our fancy new plugin that we just added, we can move onto our next step: generating those JSON files.

$ npm run build

Once you run this, you should notice that you have a public/messages/src directory that appears to be a clone of your original src directory, except all your component files are actually JSON files.

/messages /src /components Weather.json /pages Home.json Day.json

Now, let’s see the contents of one of them, Home.json:

[ { "id": "Home.header", "defaultMessage": "Hello, world!" }, { "id": "Home.dayMessage", "defaultMessage": "It's a beautiful day outside." }, { "id": "Home.dayLink", "defaultMessage": "Click here to find out why!" } ]

The state of the project up until now can be found here.

Combining the JSON files

It did just what we thought it would. It can be helpful to have our content organized in this structure, but ultimately we will want it to be in one file, and we need it to include any translations that we will make.

Now we need to make a script that does this for us. Thankfully, the folks at react-intl gave us a good starting point with this script.

import * as fs from "fs"; import { sync as globSync } from "glob"; import { sync as mkdirpSync } from "mkdirp"; import last from "lodash/last"; const MESSAGES_PATTERN = "./public/messages/**/*.json"; const LANG_DIR = "./public/locales/"; const LANG_PATTERN = "./public/locales/*.json"; // Try to delete current json files from public/locales try { fs.unlinkSync("./public/locales/data.json"); } catch (error) { console.log(error); } // Merge translated json files (es.json, fr.json, etc) into one object // so that they can be merged with the eggregated "en" object below const mergedTranslations = globSync(LANG_PATTERN) .map(filename => { const locale = last(filename.split("/")).split(".json")[0]; return { [locale]: JSON.parse(fs.readFileSync(filename, "utf8")) }; }) .reduce((acc, localeObj) => { return { ...acc, ...localeObj }; }, {}); // 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 defaultMessages = globSync(MESSAGES_PATTERN) .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; }, {}); // Create a new directory that we want to write the aggregate messages to mkdirpSync(LANG_DIR); // Merge aggregated default messages with the translated json files and // write the messages to this directory fs.writeFileSync( `${LANG_DIR}data.json`, JSON.stringify({ en: defaultMessages, ...mergedTranslations }, null, 2) );

We will need to modify it a little bit because, as it stands, that script will generate a fake translation. We don’t want this because it is not practical.

We are better than that! We want it to read a real translation!

The script we will use to do this is below:

We will need to save this file in our scripts directory and then edit package.json so that it actually runs the script.

Before we do that, we will need to do a couple things, so that our ESNext code can be understood. First we will need to add babel-cli to make sure that the script gets transpiled.

$ npm install --save-dev babel-cli

Next, we need to add the env preset to our .babelrc so that it looks like this:

{ "presets":["react-app", "env"], "plugins": [ ["react-intl", { "messagesDir": "./public/messages/" }] ] }

Lastly, we need to edit our package.json so that it runs our script:

{... "scripts": { "build:langs": "NODE_ENV='production' babel-node scripts/mergeMessages.js", "build": "npm run build:langs && node scripts/build.js", ... }, ... }

Note that we are running the mergeMessages script before npm run build . This is because we want to generate our final data.json file in the /public directory before our build script copies it over to /build .

Alright, now when we run npm run build we should see build/locales/data.json which combines all of our JSON files into one.

The state of the project up until now can be found here.

Time to start translating

Now that we have made a script that will aggregate our default messages and our translations into one file, let’s make some translations! For this example, we will translate to Spanish. Our script that we just created will read all *.json files from /public/locales so we will need to name our new translation file /public/locales/es.json and add the content below:

{ "Weather.message": "¡Porque es soleado!", "Day.homeLink": "Regresar a inicio", "Home.header": "¡Hola Mundo!", "Home.dayMessage": "Es un hermoso día afuera.", "Home.dayLink": "¡Haz clic aquí para averiguar por qué!" }

Now when we run npm run build, our mergeMessages script will create a data.json file in /public/locales , and then it will be copied over to /build/locales. Our final data.json file will look like this:

{ "en": { "Weather.message": "Because it is sunny!", "Day.homeLink": "Go back home", "Home.header": "Hello, world!", "Home.dayMessage": "It's a beautiful day outside.", "Home.dayLink": "Click here to find out why!" }, "es": { "Weather.message": "¡Porque es soleado!", "Day.homeLink": "Regresar a inicio", "Home.header": "¡Hola Mundo!", "Home.dayMessage": "Es un hermoso día afuera.", "Home.dayLink": "¡Haz clic aquí para averiguar por qué!" } }

We’re almost there! The last step is to dynamically load the Spanish version of the text if the user’s browser settings are Spanish. We need to edit index.js to read the browser language settings and then give that information along with the correct translations to /> and ultimately our app.

Our final index.js looks like this:

import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import registerServiceWorker from "./registerServiceWorker"; import { BrowserRouter } from "react-router-dom"; import { IntlProvider, addLocaleData } from "react-intl"; import en from "react-intl/locale-data/en"; import es from "react-intl/locale-data/es"; import localeData from "./../build/locales/data.json"; addLocaleData([...en, ...es]); // Define user's language. Different browsers have the user locale defined // on different fields on the `navigator` object, so we make sure to account // for these different by checking all of them const language = (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage; // Split locales with a region code const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0]; // Try full locale, try locale without region code, fallback to 'en' const messages = localeData[languageWithoutRegionCode] || localeData[language] || localeData.en; ReactDOM.render( , document.getElementById("root") ); registerServiceWorker();

(Heavily copied code from Preethi Kasireddy’s gist here)

One other small thing we need to do is edit our webpack configs to allow imports outside of src and node_modules .

Now, if we change our browser settings to Spanish, we should see our content translated into Spanish!

The final state of the project can be found here.