كيفية إعداد التدويل في React من البداية إلى النهاية
ستستخدم هذه المشاركة react-intl
لمساعدتك في الانتقال من create-react-app
إعداد الإطار إلى تطبيق ويب مترجم مكتمل!
لقد ارتكبت رمزًا كما كتبت هذا المنشور ، لذلك ستتمكن من إلقاء نظرة على سجل الالتزام الخاص بي لترى بسهولة كيف تطور الكود الخاص بي من البداية إلى النهاية.

ما هو التدويل؟
نظرًا لأنك قررت النقر فوق الرابط المؤدي إلى هذا المنشور ، فمن المحتمل أن يكون لديك فكرة على الأقل عن ماهية التدويل (i18n). مأخوذ مباشرة من موقع W3:
"التدويل هو تصميم وتطوير منتج أو تطبيق أو محتوى مستند يتيح سهولة أقلمة الجماهير المستهدفة التي تختلف في الثقافة أو المنطقة أو اللغة."بصفتك مطورًا ، فأنت تريد أن يكون المحتوى الخاص بك سهل القراءة والاستخدام من قبل جميع أنواع الأشخاص في جميع أنحاء العالم. أعتقد أن الجميع يوافق على ذلك. لكني أعرف ما تفكر فيه:
"يعد تطوير تطبيق ويب للأشخاص من ثقافتي / منطقتي / لغتي أمرًا صعبًا بما يكفي! ليس لدي الوقت أو الجهد من أجل i18n! "
لديك بالفعل لغة أسفل ، أرى. نأمل أن يساعدك هذا المنشور على إدراك أن إعداد i18n لمشروعك ليس صعبًا أو مستهلكًا للوقت كما يبدو.
ما هو رد فعل 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؟ حسنًا ، لديك هذه الأجزاء والأجزاء من المحتوى في جميع أنحاء تطبيق الويب الخاص بك. وتريد أن تتم ترجمة كل ذلك بسهولة. إذا كنت ستقدم المحتوى الخاص بك إلى مترجم ، فلن تمنحه الرمز الخاص بك وتقول "حظًا سعيدًا ، ابدأ العمل".
قد ترغب في العثور على طريقة لوضع كل المحتوى الخاص بك في ملف واحد ، ومن ثم منحهم هذا الملف. سيترجمونها إلى لغة أخرى ، لنقل من الإنجليزية إلى الإسبانية ، ويعطونك ملفًا واحدًا يحتوي على كل المحتوى الإسباني.
حسنا عظيم. لقد فعلت ذلك ، ولكن عليك الآن أن تأخذ المحتوى الأسباني في هذا الملف الواحد وتعيد توزيعه مرة أخرى في موقعه الأصلي. كيف ستفعل ذلك برمجيًا؟ ربما تقوم بتعيين معرفات لكل جزء من المحتوى ، حتى لا تفقد الموقع الأصلي لكل جزء من المحتوى.
وهذا كل شيء إلى حد كبير!
الخطوة الأولى هي لف التطبيق الخاص بك في ملف
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
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
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
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
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
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.