{"id":28098024,"url":"https://github.com/wavelop/i18n-only-with-react-hooks","last_synced_at":"2026-04-17T13:32:56.958Z","repository":{"id":53804893,"uuid":"232087290","full_name":"Wavelop/i18n-only-with-react-hooks","owner":"Wavelop","description":"🌍 Set up a translation/i18n internalization system with only React hooks","archived":false,"fork":false,"pushed_at":"2021-03-12T14:06:04.000Z","size":708,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-10-09T07:07:18.933Z","etag":null,"topics":["i18n","react","react-hooks","translations"],"latest_commit_sha":null,"homepage":"https://wavelop.com","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Wavelop.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-01-06T11:30:56.000Z","updated_at":"2023-08-30T13:38:34.000Z","dependencies_parsed_at":"2022-08-22T05:01:12.012Z","dependency_job_id":null,"html_url":"https://github.com/Wavelop/i18n-only-with-react-hooks","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Wavelop/i18n-only-with-react-hooks","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wavelop%2Fi18n-only-with-react-hooks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wavelop%2Fi18n-only-with-react-hooks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wavelop%2Fi18n-only-with-react-hooks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wavelop%2Fi18n-only-with-react-hooks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Wavelop","download_url":"https://codeload.github.com/Wavelop/i18n-only-with-react-hooks/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Wavelop%2Fi18n-only-with-react-hooks/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31931434,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-17T12:37:54.787Z","status":"ssl_error","status_checked_at":"2026-04-17T12:37:25.095Z","response_time":62,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["i18n","react","react-hooks","translations"],"created_at":"2025-05-13T17:47:32.061Z","updated_at":"2026-04-17T13:32:56.930Z","avatar_url":"https://github.com/Wavelop.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# i18n internalization with only React hooks - translate\n\nIn this tutorial, we are going to explain how to set up a translation/i18n internalization system with only React hooks. \nYou can read the article [here](https://wavelop.com/en/story/i18n-only-with-react-hooks/).\n\nThe first article of the series _How To Implement a Translation System Without Any Library_ was about **React Native** and can be read [here](https://wavelop.com/en/story/implementing-multi-language-without-any-library-in-react-native/).\n\nYou can find the code of this tutorial [here](https://github.com/Wavelop/i18n-only-with-react-hooks).\nYou can take a look at a demo [here](https://wavelop-i18n-react-hooks.firebaseapp.com/).\n\n\u003e Keep in mind to adapt the code to your best practice and code styling.\n\n## Environment setup \n\nExecute the following commands:\n\n```\nnpx create-react-app i18n-only-with-react-hooks\ncd i18n-only-with-react-hooks\nnpm run eject\n```\n\nTo the below question say yes:\n\n```\n? Are you sure you want to eject? This action is permanent. \n````\n\nYou will have the following structure: \n\n```\ni18n-only-with-react-hooks\n├── README.md\n├── node_modules\n├── package.json\n├── package-lock.json\n├── .gitignore\n├── config\n│   ├── webpack.config.js\n│   ├── ...\n│   └── Other folder and files\n├── scripts\n│   ├── build.js\n│   ├── start.js\n│   └── test.js\n├── public\n│   ├── favicon.ico\n│   ├── index.html\n│   ├── logo192.png\n│   ├── logo512.png\n│   ├── manifest.json\n│   └── robots.txt\n└── src\n    ├── App.css\n    ├── App.js\n    ├── App.test.js\n    ├── index.css\n    ├── index.js\n    ├── logo.svg\n    ├── serviceWorker.js\n    └── setupTests.js\n```\n\nExecute then: \n\n```\nnpm i\n```\n\nCreate the following folders inside the **src**one: \n- **assets**;\n- **components**;\n- **screens**;\n- **translate**.\n\nand inside of all of these folders, create an **index.js** file. Inside of every index.js file we are going to export the contained sub-folders. The sintax that we are going to use will be:\n\n```javascript\nexport { default as CompontentName/ServiceName/etc } from \"./CompontentNameFolder/ServiceNameFolder/etc\";\n```\n\nAdd to **config/webpack.config.js** file - in particular in the **resolve.alias** path of the return object - the following lines: \n```javascript\n'Assets': path.resolve(__dirname, '../src/assets/'), \n'Components': path.resolve(__dirname, '../src/components/'),\n'Screens': path.resolve(__dirname, '../src/screens/'),\n'Translate': path.resolve(__dirname, '../src/translate/'),\n```\n\nin this way we are able to do inside every component: \n\n```javascript\nimport { CompontentName } from 'Components';\nimport { ServiceName } from 'Services';\n...\n```\n\nand also the export for the internalization - the **Translate** module. If you prefer you can continue to use the relative path instead. The logic is the same. \n\nNow we are going to re-organize the file generated by the `npm run eject` command. \n\nStarting from the **assets** folder, we move **logo.svg** inside a new **images** folder. And inside the index file, we export the file: \n\n```\nexport { default as Logo } from './images/logo.svg';\n```\n\nNow, for components, we move the **App.css**, **App.js** and **App.test.js** inside a new folder called **App**. Then we rename them into **style.css**, **index.js** and **index.test.js**. \nInside the new **App/index.js** file we update:\n- the import line `import './App.css';` in `import './style.css';`;\n- the import line `import logo from './logo.svg';`in `import { Logo as logo } from 'Assets';`.\n\nIn the end we need to update the entry point index file as the following: \n\n`src/index.js:` \n```javascript\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\nimport \"./index.css\";\nimport { App } from \"Components\";\nimport * as serviceWorker from \"./serviceWorker\";\n\nReactDOM.render(\u003cApp /\u003e, document.getElementById(\"root\"));\n\n// If you want your app to work offline and load faster, you can change\n// unregister() to register() below. Note this comes with some pitfalls.\n// Learn more about service workers: https://bit.ly/CRA-PWA\nserviceWorker.unregister();\n\n```\n\n## Translate service\n\nNow we are going to create the Translate module with all the functions necessary to translate our application. \n\nThe main functionality of React hooks used are: \n- **createContext**;\n- **useContext**; \n- **useReducer**.\n\nInside the **translate** folder, we create two new sub-folder: \n- **Languages**;\n- **Translate**.\n\n### Languages\n\nThe new **Languages** folder will contain the **JSON** files with all the label translated:\n\n```json\n// src/translate/Languages/en.json \n\n{\n  \"Application.title\": \"Wavelop\",\n  \"Application.subTitle\": \"i18n internalization with only React hooks - translate\",\n\n  \"Application.footer\": \"Developed by Wavelop\",\n\n  \"LanguageSwitcher.used\": \"Lang selected:\",\n  \"LanguageSwitcher.it\": \"Italiano\",\n  \"LanguageSwitcher.en\": \"English\",\n  \"LanguageSwitcher.fr\": \"Français\"\n}\n```\n\n```json\n// src/translate/Languages/it.json\n\n{\n  \"Application.title\": \"Wavelop\",\n  \"Application.subTitle\": \"i18n internalizzazione with only React hooks - traduzioni\",\n\n  \"Application.footer\": \"Sviluppato da Wavelop\",\n  \"LanguageSwitcher.used\": \"Lingua selezionata: \"\n}\n```\n\n```json\n// src/translate/Languages/fr.json \n\n{\n  \"Application.title\": \"Wavelop (French translation)\",\n  \"Application.subTitle\": \"i18n internalization with only React hooks - translate (French translation)\",\n\n  \"Application.footer\": \"Developed by Wavelop (French translation)\",\n  \"LanguageSwitcher.used\": \"Lang selected: (French translation) \"\n}\n```\n\nAs you can see, there are some missing labels in the italian and french translations. This because there will be the fallback system for the missing one. \n\n### Translate\n\nThe new **Translate** folder will contain the util function used by the service for the translation. The file will look like this:\n\n```javascript\n// src/translate/Translate/index.js\n\nlet _currentLanguage = \"\";\nlet _fallbackLanguage = \"\";\nlet _languages = [];\nlet _translations = {};\n\nexport const getCurrentLanguage = () =\u003e {\n  return _currentLanguage;\n};\n\nexport const setCurrentLanguage = currentLanguage =\u003e {\n  _currentLanguage = currentLanguage;\n};\n\nexport const getFallbackLanguage = () =\u003e {\n  return _fallbackLanguage;\n};\n\nexport const setFallbackLanguage = fallbackLanguage =\u003e {\n  _fallbackLanguage = fallbackLanguage;\n};\n\nexport const getLanguages = () =\u003e {\n  return _languages;\n};\n\nexport const setLanguages = languages =\u003e {\n  _languages = languages;\n\n  _languages.forEach(language =\u003e {\n    const loadedLanguage = require(`../Languages/${language}.json`);\n    _translations[language] = loadedLanguage;\n  });\n};\n\nexport const getTranslations = () =\u003e {\n  return _translations;\n};\n\nexport const setTranslations = translations =\u003e {\n  _translations = translations;\n};\n\nexport const t = label =\u003e {\n  return _translations[_currentLanguage] \u0026\u0026\n    _translations[_currentLanguage][label]\n    ? _translations[_currentLanguage][label]\n    : _translations[_fallbackLanguage] \u0026\u0026\n      _translations[_fallbackLanguage][label]\n    ? _translations[_fallbackLanguage][label]\n    : label;\n};\n```\n\n### The Hooks integration\n\nUsing the combinantion of **createContext**,**useContext** and **useReducer** we can create a system that will update the entire application updating the label too. \n\n```javascript\n// src/translate/index.js\n\nimport React, { createContext, useContext, useReducer } from \"react\";\n\nimport {\n  getCurrentLanguage,\n  setCurrentLanguage,\n  getFallbackLanguage,\n  setFallbackLanguage,\n  getLanguages,\n  setLanguages,\n  getTranslations,\n  setTranslations,\n  t\n} from \"./Translate\";\n\n// Configuration\nconst { language, fallBacklanguage, languages } = {\n  language: \"en\",\n  fallBacklanguage: \"en\",\n  languages: [\"it\", \"fr\", \"en\"]\n};\n\n// Init language properties\n\nsetCurrentLanguage(language);\nsetFallbackLanguage(fallBacklanguage);\nsetLanguages(languages);\n\n// Contexts\nconst TranslateContext = createContext();\nconst TranslateStateContext = createContext();\nconst TranslateDispatchContext = createContext();\n\n// Reducers\nfunction translateReducer(state, action) {\n  switch (action.type) {\n    case \"CHANGE_LANGUAGE\": {\n      setCurrentLanguage(action.language);\n      return { ...state, language: action.language };\n    }\n    default: {\n      throw new Error(`Unhandled action type: ${action.type}`);\n    }\n  }\n}\n\n// Initial state\nconst initialState = {\n  language\n};\n\nexport const TranslateProvider = props =\u003e {\n  const value = {\n    getCurrentLanguage: props.getCurrentLanguage || getCurrentLanguage,\n    setCurrentLanguage: props.setCurrentLanguage || setCurrentLanguage,\n    getFallbackLanguage: props.getFallbackLanguage || getFallbackLanguage,\n    setFallbackLanguage: props.setFallbackLanguage || setFallbackLanguage,\n    getLanguages: props.getLanguages || getLanguages,\n    setLanguages: props.setLanguages || setLanguages,\n    getTranslations: props.getTranslations || getTranslations,\n    setTranslations: props.setTranslations || setTranslations,\n    t: props.t || t\n  };\n  const [state, dispatch] = useReducer(translateReducer, initialState);\n\n  return (\n    \u003cTranslateContext.Provider value={value}\u003e\n      \u003cTranslateStateContext.Provider value={state}\u003e\n        \u003cTranslateDispatchContext.Provider value={dispatch}\u003e\n          {props.children}\n        \u003c/TranslateDispatchContext.Provider\u003e\n      \u003c/TranslateStateContext.Provider\u003e\n    \u003c/TranslateContext.Provider\u003e\n  );\n};\n\nexport const useTranslate = () =\u003e {\n  // You can use the function of provider\n  const context = useContext(TranslateContext);\n  if (context === undefined) {\n    throw new Error(\"useTranslate must be used within a TranslateProvider\");\n  }\n  return context;\n};\n\nexport const useTranslateState = () =\u003e {\n  const context = useContext(TranslateStateContext);\n  if (context === undefined) {\n    throw new Error(\"useTranslateState must be used within a TranslateProvider\");\n  }\n  return context;\n};\n\nexport const useTranslateDispatch = () =\u003e {\n  const context = useContext(TranslateDispatchContext);\n  if (context === undefined) {\n    throw new Error(\"useTranslateDispatch must be used within a TranslateProvider\");\n  }\n  return context;\n};\n\n```\n\nWe create three contexts to inject in the whole application the **utils functions**, the **state** object and the **dispatch** function. The **state** object exposes the current language and the **dispatch** the way to switch language. The utils functions will be used for different purposes, the main one to get the translations. \n\n## SwitchLanguage components \n\nTo create a simple experience for switching between languages, we are going to create a component to do this. We create a new folder inside **components** one called **LanguageSwitcher**. Inside the new folder, we create two new files - **index.js** and **style.js**: \n\n```javascript\n// src/components/LanguageSwitcher/index.js\n\n// NPM dependencies\nimport React from \"react\";\n\n// Application dependencies\nimport {\n  useTranslate,\n  useTranslateDispatch,\n  useTranslateState\n} from \"Translate\";\nimport \"./style.css\";\n\nfunction LanguageSwitcher() {\n  const { language } = useTranslateState(); // we get the current language\n  const i18n = useTranslate(); // we get the utils functions\n  const { t, getLanguages } = i18n;\n  const dispatch = useTranslateDispatch();\n\n  const items = getLanguages().map(key =\u003e {\n    return key !== language ? (\n      \u003cbutton\n        key={key}\n        onClick={() =\u003e {\n          dispatch({ type: \"CHANGE_LANGUAGE\", language: key });\n        }}\n      \u003e\n        {t(`LanguageSwitcher.${key}`)}\n      \u003c/button\u003e\n    ) : (\n      \"\"\n    );\n  });\n\n  return (\n    \u003csection\u003e\n      \u003cspan\u003e{t(`LanguageSwitcher.used`)}  {t(`LanguageSwitcher.${language}`)}\u003c/span\u003e\n      \u003cspan\u003e{items}\u003c/span\u003e\n    \u003c/section\u003e\n  );\n}\n\nexport default LanguageSwitcher;\n\n```\n\nWe can leave empty the **src/components/LanguageSwitcher/style.js** file.  \n\nLeast, we add to **src/components/index.js** the following line: \n```\nexport { default as LanguageSwitcher } from \"./LanguageSwitcher\";\n```\n\n## Link all together thanks to the TranslateProvider\n\nWe need now a screen to show. We create a **HelloWorld** screen - that is a component. We create a new folder inside **screens** called **HelloWorld**. Inside the new sub-folder, we create two new files - **index.js** and **style.js**: \n\n```javascript\n// src/screens/HelloWorld/index.js\n\nimport React from \"react\";\nimport { Logo as logo } from \"Assets\";\nimport \"./style.css\";\nimport { useTranslate } from \"Translate\";\nimport { LanguageSwitcher } from \"Components\";\n\nfunction HelloWorld() {\n\n  const i18n = useTranslate();\n  const { t } = i18n;\n\n  return (\n      \u003cspan className=\"HelloWorld\"\u003e\n        \u003cheader\u003e\n          \u003ch1\u003e{t(\"Application.title\")}\u003c/h1\u003e\n          \u003ch2\u003e{t(\"Application.subTitle\")}\u003c/h2\u003e\n          \u003cimg src={logo} className=\"HelloWorld-logo\" alt=\"logo\" /\u003e\n        \u003c/header\u003e\n        \u003cmain\u003e\n          \u003cLanguageSwitcher\u003e\u003c/LanguageSwitcher\u003e\n        \u003c/main\u003e\n\n        \u003cfooter\u003e{t(\"Application.footer\")}\u003c/footer\u003e\n      \u003c/span\u003e\n  );\n}\n\nexport default HelloWorld;\n```\n\nWe can leave empty the **src/screens/HelloWorld/style.js** file.  \n\nLeast, we add to **src/screens/index.js** the following line: \n```javascript\nexport {default as HelloWorld} from './HelloWorld';\n```\n\nAt this point, we go back to **src/components/App/index.js** file and we update it in this way:\n\n```javascript\n// src/components/App/index.js\n\nimport React from \"react\";\nimport \"./style.css\";\nimport { TranslateProvider } from \"Translate\";\nimport { HelloWorld } from \"Screens\";\n\nfunction App() {\n  return (\n    \u003cTranslateProvider\u003e\n      \u003cHelloWorld /\u003e\n    \u003c/TranslateProvider\u003e\n  );\n}\n\nexport default App;\n```\n\nThe style for the **App** component is no more necessary, we can delete all the content of **style.js**. \n\nFinal project structure: \n\n```\ni18n-only-with-react-hooks\n├── README.md\n├── node_modules\n├── package.json\n├── package-lock.json\n├── .gitignore\n├── config\n│   ├── webpack.config.js\n│   ├── ...\n│   └── Other folder and files\n├── scripts\n│   ├── build.js\n│   ├── start.js\n│   └── test.js\n├── public\n│   ├── favicon.ico\n│   ├── index.html\n│   ├── logo192.png\n│   ├── logo512.png\n│   ├── manifest.json\n│   └── robots.txt\n└── src\n    ├── index.css\n    ├── index.js\n    ├── serviceWorker.js\n    ├── setupTests.js\n    ├── assets\n    │   ├── images\n    |   │   └── logo.svg   \n    │   └── index.js\n    ├── components\n    │   ├── App\n    |   │   ├── index.js   \n    |   │   └── style.css   \n    │   ├── LanguageSwitcher\n    |   │   ├── index.js   \n    |   │   └── style.css  \n    │   └── index.js  \n    ├── screens\n    │   ├── HelloWorld\n    |   │   ├── index.js   \n    |   │   └── style.css  \n    │   └── index.js\n    └── translate\n        ├── Languages\n        │   ├── en.json  \n        │   ├── fr.json   \n        │   └── it.json  \n        ├── Translate\n        │   └── index.js  \n        └── index.js\n```\n\nNow everything is working, execute `npm run start` and go to [localhost:3000](http://localhost:3000) to test it. \n\n![Demo](./demo.gif)\n\n# Reference \n\n* https://reactjs.org/docs/hooks-intro.html\n* https://medium.com/the-guild/injectable-services-in-react-de0136b6d476\n* https://spectrum.chat/react/help/how-do-i-combine-reducers-while-managing-state-with-usereducer-hook-context~842dbecd-bde0-475f-87b2-3e9ecc7bf713\n* https://kentcdodds.com/blog/how-to-use-react-context-effectively\n\n# Conclusion\n\nWith the combination of the React Hooks API is easy to create an i18n translate system for your site or application. \n\nThis tutorial is part of the series _How To Implement a Translation System Without Any Library_ and the first article was about **React Native** and can be read [here](https://wavelop.com/en/story/implementing-multi-language-without-any-library-in-react-native/).\n\nYou can find the code of this tutorial [here](https://github.com/Wavelop/i18n-only-with-react-hooks).\nYou can take a look at a demo [here](https://wavelop-i18n-react-hooks.firebaseapp.com/).\n\nIf you have questions, please write to us on the chat or an email to [info@wavelop.com](mailto:info@wavelop.com).","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwavelop%2Fi18n-only-with-react-hooks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwavelop%2Fi18n-only-with-react-hooks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwavelop%2Fi18n-only-with-react-hooks/lists"}