I've got this big legacy web app based on Codeigniter and jQuery.
The plan is to phase out jQuery and start using Vuejs instead. We are replacing certain parts of the website step by step.
I have just installed Nuxtjs and got the file structure the way i like it inside the legacy project.
Now to the question. How can i access a Vue component from one of my legacy javascript files?
App.vue
<template>
<div id="app">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</div>
</template>
<script lang="ts">
import Vue from "vue";
import HelloWorld from "./components/HelloWorld.vue";
export default Vue.extend({
name: "App",
components: {
HelloWorld
}
});
</script>
main.ts
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
Vue.config.productionTip = false;
new Vue({
store,
render: h => h(App)
}).$mount("#app");
I want to display my App.vue inside an ordinary php/html view.
I am doing something similar right now, the project was originally written using Thymeleaf and jQuery and now we are switching to Vue.
You can communicate between vue components and non-vue components in a few ways, none of them are "pretty".
Communication
Classic JavaScript events
Pretty straightforward
// legacy code
document.dispatchEvent(new CustomEvent('legacy-vue.something-happened', { detail: payload }))
// vue component
created () {
document.addEventListener('legacy-vue.something-happened', this.onSomethingHappened)
},
destroyed () { // don't forget to remove the listener!
document.removeEventListener('legacy-vue.something-happened', this.onSomethingHappened)
}
Exposing EventHub
Similar to the previous one, but you are using vue events instead. This is what i would recommend, because it's the Vue way of handling events and your goal is to vuetify your app.
// initialization
const hub = new Vue()
Vue.prototype.$eventHub = hub
window.$vueEventHub = hub
// legacy code
$vueEventHub.$emit('something-happened', payload)
// vue component
created () {
this.$eventHub.$on('something-happened', this.onSomethingHappened)
},
destroyed () {
this.$eventHub.$off('something-happened', this.onSomethingHappened)
}
Exposing whole components
The most flexible way, but it's hard to see what's going on where. Event based approaches the lesser evil in my opinion (it's easy to track events).
// vue component
created () {
window.vueTableComponent = this
}
// legacy component
vueTableComponent.fetchNextPage()
vueTableComponent.registerOnPageFetchedCallback(callback);
Summary
No matter which approach you pick, i recommend doing something like this:
Let's say that you have TableComponent. TableComponent has few props like apiUrl, emits row-clicked event, etc. It would be best to design the component without thinking about your legacy code at all, and then create it's legacy wrapper because at one point it will be used both with vue-only screens and with mixed-screens (with both legacy components and vue components). An example:
LegacyTableComponentWrapper.vue
<template>
<table-component
:api-path="apiPath"
#row-clicked="onRowClicked"
ref="table-component"
/>
</template>
export default {
data: () => ({
apiPath: null
}),
methods: {
onRowClicked (row) {
this.$eventHub.$emit('table-row-clicked', row) // notify legacy code
},
onApiPathDefined (payload) {
this.apiPath = payload
}
},
mounted () {
// legacy code might require the TableComponent to act differently
// and if you don't want the TableComponent to know whenever it's legacy or not
// you can always override one or more of it's methods.
this.$refs['table-component'] = this.overriddenImplementationOfXYZ
},
created () {
this.$eventHub.$on('define-api-path', this.onApiPathDefined)
},
destroyed () {
this.$eventHub.$off('define-api-path', this.onApiPathDefined)
}
}
It's sure more work at first, but will save you a headache later, when you will be working on your first view which is fully in vue and all that legacy communication stuff is in the way.
I'm working on a NUXT project and I find myself copying the same actions into multiple store/modules. So I have extracted the actions to a separate file and I now import it into the module. That works great but I occasionally need an additional action that is not boilerplate. How do I import the boilerplate actions and also have a custom action in the module?
Tags module:
import Vue from "vue";
import globalActions from "../utils/Actions.js";
export const state = () => ({ /* removed */ })
export const actions = globalActions;
//Need actions to be a bit more flexible to include non-boilerplate actions
I'm not sure it matters but here is utils/Actions.js It is just the standard "export default {}" that would typically be in the module.
export default {
all({ commit }, all) {
all.data.forEach(item => {
commit("add", item);
});
},
async list({ commit, state, getters, dispatch }) {
/* flush resets the state */
commit("flush");
/*Makes the api call using the repository setup */
let params = getters.params;
const results = await this.$repositories[state.type].index(params);
const ids = results.data.map(item => item.id);
let page = state.page;
dispatch("all", results);
/*Adds a encyclopedia that maps pages to index */
commit("SET_REFERENCE", { page, ids });
commit("totalItems", results.meta.total);
},
}
Ideally, I think the module actions would look something like this:
export const actions = {
list(){ return globalActions.list }
nonBoilerPlateAction({commit})
}
I am pretty sure I will need to change how I import the globalActions and that my "ideal" actions syntax is wrong but I not sure what I need to adjust.
To merge the imported actions and the custom actions, you can merge the two actions in this manner:
export const actions = {
...globalActions,
nonBoilerPlateAction({commit}) {
commit('something')
}
}
Although the way you are using to re-use your module works fine, I'll recommend using namespacing instead. This way all your created module can be easily re-used without having to import them to any other file. You can have access to other modules from another module easily.
I'm building an application, where I need to preload people and planet data (it's likely that in the future more preload requirements may be added) on launch of the application. I want to have value in the store that represents the global state of the app as loaded: <boolean>. The value would be true only then when the preload requirements people.loaded: true and planet.loaded: true are true. The store would look something like this:
Store
├── loaded: <Boolean>
├── people:
│ ├── loaded: <Boolean>
│ └── items: []
├── planets:
│ ├── loaded: <Boolean>
│ └── items: []
Separate action creators make the needed async requests and dispatch actions which are handled by the People and Planets reducers. As shown below (uses redux-thunk):
actions/index.js
import * as types from '../constants/action-types';
import {getPeople, getPlanets} from '../util/swapi';
export function loadPeople () {
return (dispatch) => {
return getPeople()
.then((people) => dispatch(addPeople(people)));
};
}
export function loadPlanets () {
return (dispatch) => {
return getPlanets()
.then((planets) => dispatch(addPeople(planets)));
};
}
export function addPeople (people) {
return {type: types.ADD_PEOPLE, people};
}
export function addPlanets (planets) {
return {type: types.ADD_PLANETS, planets};
}
export function initApp () {
return (dispatch) => {
loadPeople()(dispatch);
loadPlanets()(dispatch);
};
}
../util/swapi handles fetching people and planet data either from LocalStorage or making a request.
initApp() action creator calls other action creators within site.js just before rendering to DOM as shown below:
site.js
import React from 'react';
import {render} from 'react-dom';
import Root from './containers/root';
import configureStore from './store/configure-store';
import {initApp} from './actions';
const store = configureStore();
// preload data
store.dispatch(initApp());
render(
<Root store={store} />,
document.querySelector('#root')
);
1. What are the best practices for managing global preload state of the application in Redux?
2. Is having a global loaded state in the store necessary?
3. What would be a scalable way of checking app loaded state in multiple React components? It doesn't seem right to include People and Planet state for containers that just needs to know the global app state and doesn't handle rendering of People or Planets. Also that would be painful to manage when the global loaded state would be needed in multiple containers.
Quoting part of Dan's answer from Redux - multiple stores, why not? question.
Using reducer composition makes it easy to implement "dependent updates" a la waitFor in Flux by writing a reducer manually calling other reducers with additional information and in a specific order.
4. Does Dan by calling other reducers mean calling nested reducers?
First, let me correct your example.
Instead of
export function initApp () {
return (dispatch) => {
loadPeople()(dispatch);
loadPlanets()(dispatch);
};
}
you can (and should) write
export function initApp () {
return (dispatch) => {
dispatch(loadPeople());
dispatch(loadPlanets());
};
}
You don’t need to pass dispatch as an argument—thunk middleware takes care of this.
Of course technically your code is valid, but I think my suggestion reads easier.
What are the best practices for managing global preload state of the application in Redux?
What you’re doing seems correct. There are no specific best practices.
Is having a global loaded state in the store necessary?
No. As David notes in his answer, you’re better off storing only necessary state.
What would be a scalable way of checking app loaded state in multiple React components? It doesn't seem right to include People and Planet state for containers that just needs to know the global app state and doesn't handle rendering of People or Planets. Also that would be painful to manage when the global loaded state would be needed in multiple containers.
If you’re concerned about duplication, create a “selector” function and place it alongside your reducers:
// Reducer is the default export
export default combineReducers({
planets,
people
});
// Selectors are named exports
export function isAllLoaded(state) {
return state.people.loaded && state.planets.loaded;
}
Now you can import selectors from your components and use them in mapStateToProps function inside any component:
import { isAllLoaded } from '../reducers';
function mapStateToProps(state) {
return {
loaded: isAllLoaded(state),
people: state.people.items,
planets: state.planet.items
};
}
Does Dan by calling other reducers mean calling nested reducers?
Yes, reducer composition usually means calling nested reducers.
Please refer to my free Egghead tutorial videos on this topic:
Reducer Composition with Arrays
Reducer Composition with Objects
Reducer Composition with combineReducers()
Implementing combineReducers() from Scratch
A loaded flag in your store state makes perfect sense.
However you've got too many of them. You've got state.people.loaded, state.planets.loaded as well as state.loaded. The latter is derived from the first two. Your store really shouldn't contain derived state. Either have just the first two or just the latter.
My recommendation would be to keep the first two, i.e. state.people.loaded and state.planets.loaded. Then your connected component can derive an ultimate loaded state. e.g.
function mapStateToProps(state) {
return {
loaded: state.people.loaded && state.planets.loaded,
people: state.people.items,
planets: state.planet.items
};
}
I'm building an app that will need to be available in multiple languages and locales.
My question is not purely technical, but rather about the architecture, and the patterns that people are actually using in production to solve this problem.
I couldn't find anywhere any "cookbook" for that, so I'm turning to my favourite Q/A website :)
Here are my requirements (they are really "standard"):
The user can choose the language (trivial)
Upon changing the language, the interface should translate automatically to the new selected language
I'm not too worried about formatting numbers, dates etc. at the moment, I want a simple solution to just translate strings
Here are the possible solutions I could think off:
Each component deal with translation in isolation
This means that each component have for example a set of en.json, fr.json etc. files alongside it with the translated strings. And a helper function to help reading the values from those depending on the selected language.
Pro: more respectful of the React philosophy, each component is "standalone"
Cons: you can't centralize all the translations in a file (to have someone else add a new language for example)
Cons: you still need to pass the current language as a prop, in every bloody component and their children
Each component receives the translations via the props
So they are not aware of the current language, they just take a list of strings as props which happen to match the current language
Pro: since those strings are coming "from the top", they can be centralized somewhere
Cons: Each component is now tied into the translation system, you can't just re-use one, you need to specify the correct strings every time
You bypass the props a bit and possibly use the context thingy to pass down the current language
Pro: it's mostly transparent, don't have to pass the current language and/or translations via props all the time
Cons: it looks cumbersome to use
If you have any other idea, please do say!
How do you do it?
After trying quite a few solutions, I think I found one that works well and should be an idiomatic solution for React 0.14 (i.e. it doesn't use mixins, but Higher Order Components) (edit: also perfectly fine with React 15 of course!).
So here the solution, starting by the bottom (the individual components):
The Component
The only thing your component would need (by convention), is a strings props.
It should be an object containing the various strings your Component needs, but really the shape of it is up to you.
It does contain the default translations, so you can use the component somewhere else without the need to provide any translation (it would work out of the box with the default language, english in this example)
import { default as React, PropTypes } from 'react';
import translate from './translate';
class MyComponent extends React.Component {
render() {
return (
<div>
{ this.props.strings.someTranslatedText }
</div>
);
}
}
MyComponent.propTypes = {
strings: PropTypes.object
};
MyComponent.defaultProps = {
strings: {
someTranslatedText: 'Hello World'
}
};
export default translate('MyComponent')(MyComponent);
The Higher Order Component
On the previous snippet, you might have noticed this on the last line:
translate('MyComponent')(MyComponent)
translate in this case is a Higher Order Component that wraps your component, and provide some extra functionality (this construction replaces the mixins of previous versions of React).
The first argument is a key that will be used to lookup the translations in the translation file (I used the name of the component here, but it could be anything). The second one (notice that the function is curryed, to allow ES7 decorators) is the Component itself to wrap.
Here is the code for the translate component:
import { default as React } from 'react';
import en from '../i18n/en';
import fr from '../i18n/fr';
const languages = {
en,
fr
};
export default function translate(key) {
return Component => {
class TranslationComponent extends React.Component {
render() {
console.log('current language: ', this.context.currentLanguage);
var strings = languages[this.context.currentLanguage][key];
return <Component {...this.props} {...this.state} strings={strings} />;
}
}
TranslationComponent.contextTypes = {
currentLanguage: React.PropTypes.string
};
return TranslationComponent;
};
}
It's not magic: it will just read the current language from the context (and that context doesn't bleed all over the code base, just used here in this wrapper), and then get the relevant strings object from loaded files. This piece of logic is quite naïve in this example, could be done the way you want really.
The important piece is that it takes the current language from the context and convert that into strings, given the key provided.
At the very top of the hierarchy
On the root component, you just need to set the current language from your current state. The following example is using Redux as the Flux-like implementation, but it can easily be converted using any other framework/pattern/library.
import { default as React, PropTypes } from 'react';
import Menu from '../components/Menu';
import { connect } from 'react-redux';
import { changeLanguage } from '../state/lang';
class App extends React.Component {
render() {
return (
<div>
<Menu onLanguageChange={this.props.changeLanguage}/>
<div className="">
{this.props.children}
</div>
</div>
);
}
getChildContext() {
return {
currentLanguage: this.props.currentLanguage
};
}
}
App.propTypes = {
children: PropTypes.object.isRequired,
};
App.childContextTypes = {
currentLanguage: PropTypes.string.isRequired
};
function select(state){
return {user: state.auth.user, currentLanguage: state.lang.current};
}
function mapDispatchToProps(dispatch){
return {
changeLanguage: (lang) => dispatch(changeLanguage(lang))
};
}
export default connect(select, mapDispatchToProps)(App);
And to finish, the translation files:
Translation Files
// en.js
export default {
MyComponent: {
someTranslatedText: 'Hello World'
},
SomeOtherComponent: {
foo: 'bar'
}
};
// fr.js
export default {
MyComponent: {
someTranslatedText: 'Salut le monde'
},
SomeOtherComponent: {
foo: 'bar mais en français'
}
};
What do you guys think?
I think is solves all the problem I was trying to avoid in my question: the translation logic doesn't bleed all over the source code, it is quite isolated and allows reusing the components without it.
For example, MyComponent doesn't need to be wrapped by translate() and could be separate, allowing it's reuse by anyone else wishing to provide the strings by their own mean.
[Edit: 31/03/2016]: I recently worked on a Retrospective Board (for Agile Retrospectives), built with React & Redux, and is multilingual.
Since quite a lot of people asked for a real-life example in the comments, here it is:
You can find the code here: https://github.com/antoinejaussoin/retro-board/tree/master
From my experience the best approach is to create an i18n redux state and use it, for many reasons:
1- This will allow you to pass the initial value from the database, local file or even from a template engine such as EJS or jade
2- When the user changes the language you can change the whole application language without even refreshing the UI.
3- When the user changes the language this will also allow you to retrieve the new language from API, local file or even from constants
4- You can also save other important things with the strings such as timezone, currency, direction (RTL/LTR) and list of available languages
5- You can define the change language as a normal redux action
6- You can have your backend and front end strings in one place, for example in my case I use i18n-node for localization and when the user changes the UI language I just do a normal API call and in the backend, I just return i18n.getCatalog(req) this will return all the user strings only for the current language
My suggestion for the i18n initial state is:
{
"language":"ar",
"availableLanguages":[
{"code":"en","name": "English"},
{"code":"ar","name":"عربي"}
],
"catalog":[
"Hello":"مرحباً",
"Thank You":"شكراً",
"You have {count} new messages":"لديك {count} رسائل جديدة"
],
"timezone":"",
"currency":"",
"direction":"rtl",
}
Extra useful modules for i18n:
1- string-template this will allow you to inject values in between your catalog strings for example:
import template from "string-template";
const count = 7;
//....
template(i18n.catalog["You have {count} new messages"],{count}) // لديك ٧ رسائل جديدة
2- human-format this module will allow you to converts a number to/from a human readable string, for example:
import humanFormat from "human-format";
//...
humanFormat(1337); // => '1.34 k'
// you can pass your own translated scale, e.g: humanFormat(1337,MyScale)
3- momentjs the most famous dates and times npm library, you can translate moment but it already has a built-in translation just you need to pass the current state language for example:
import moment from "moment";
const umoment = moment().locale(i18n.language);
umoment.format('MMMM Do YYYY, h:mm:ss a'); // أيار مايو ٢ ٢٠١٧، ٥:١٩:٥٥ م
Update (14/06/2019)
Currently, there are many frameworks implement the same concept using react context API (without redux), I personally recommended I18next
Antoine's solution works fine, but have some caveats :
It uses the React context directly, which I tend to avoid when already using Redux
It imports directly phrases from a file, which can be problematic if you want to fetch needed language at runtime, client-side
It does not use any i18n library, which is lightweight, but doesn't give you access to handy translation functionalities like pluralization and interpolation
That's why we built redux-polyglot on top of both Redux and AirBNB's Polyglot.
(I'm one of the authors)
It provides :
a reducer to store language and corresponding messages in your Redux store. You can supply both by either :
a middleware that you can configure to catch specific action, deduct current language and get/fetch associated messages.
direct dispatch of setLanguage(lang, messages)
a getP(state) selector that retrieves a P object that exposes 4 methods :
t(key): original polyglot T function
tc(key): capitalized translation
tu(key): upper-cased translation
tm(morphism)(key): custom morphed translation
a getLocale(state)selector to get current language
a translate higher order component to enhance your React components by injecting the p object in props
Simple usage example :
dispatch new language :
import setLanguage from 'redux-polyglot/setLanguage';
store.dispatch(setLanguage('en', {
common: { hello_world: 'Hello world' } } }
}));
in component :
import React, { PropTypes } from 'react';
import translate from 'redux-polyglot/translate';
const MyComponent = props => (
<div className='someId'>
{props.p.t('common.hello_world')}
</div>
);
MyComponent.propTypes = {
p: PropTypes.shape({t: PropTypes.func.isRequired}).isRequired,
}
export default translate(MyComponent);
Please tell me if you have any question/suggestion !
Yet another (light) proposal implemented in Typescript and based on ES6 & Redux & Hooks & JSON with no 3rd party dependencies.
Since the selected language is loaded in the redux state, changing the language becomes very fast without the need of rendering all pages, but just the affected texts.
Part 1: Redux setup:
/src/shared/Types.tsx
export type Language = 'EN' | 'CA';
/src/store/actions/actionTypes.tsx
export const SET_LANGUAGE = 'SET_LANGUAGE';
/src/store/actions/language.tsx:
import * as actionTypes from './actionTypes';
import { Language } from '../../shared/Types';
export const setLanguage = (language: Language) => ({
type: actionTypes.SET_LANGUAGE,
language: language,
});
/src/store/reducers/language.tsx:
import * as actionTypes from '../action/actionTypes';
import { Language } from '../../shared/Types';
import { RootState } from './reducer';
import dataEN from '../../locales/en/translation.json';
import dataCA from '../../locales/ca/translation.json';
type rootState = RootState['language'];
interface State extends rootState { }
interface Action extends rootState {
type: string,
}
const initialState = {
language: 'EN' as Language,
data: dataEN,
};
const setLanguage = (state: State, action: Action) => {
let data;
switch (action.language) {
case 'EN':
data = dataEN;
break;
case 'CA':
data = dataCA;
break;
default:
break;
}
return {
...state,
...{ language: action.language,
data: data,
}
};
};
const reducer = (state = initialState, action: Action) => {
switch (action.type) {
case actionTypes.SET_LANGUAGE: return setLanguage(state, action);
default: return state;
}
};
export default reducer;
/src/store/reducers/reducer.tsx
import { useSelector, TypedUseSelectorHook } from 'react-redux';
import { Language } from '../../shared/Types';
export interface RootState {
language: {
language: Language,
data: any,
}
};
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
/src/App.tsx
import React from 'react';
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import languageReducer from './store/reducers/language';
import styles from './App.module.css';
// Set global state variables through Redux
const rootReducer = combineReducers({
language: languageReducer,
});
const store = createStore(rootReducer);
const App = () => {
return (
<Provider store={store}>
<div className={styles.App}>
// Your components
</div>
</Provider>
);
}
export default App;
Part 2: Dropdown menu with languages. In my case, I put this component within the navigation bar to be able to change the language from any screen:
/src/components/Navigation/Language.tsx
import React from 'react';
import { useDispatch } from 'react-redux';
import { setLanguage } from '../../store/action/language';
import { useTypedSelector } from '../../store/reducers/reducer';
import { Language as Lang } from '../../shared/Types';
import styles from './Language.module.css';
const Language = () => {
const dispatch = useDispatch();
const language = useTypedSelector(state => state.language.language);
return (
<div>
<select
className={styles.Select}
value={language}
onChange={e => dispatch(setLanguage(e.currentTarget.value as Lang))}>
<option value="EN">EN</option>
<option value="CA">CA</option>
</select>
</div>
);
};
export default Language;
Part 3: JSON files. In this example, just a test value with a couple of languages:
/src/locales/en/translation.json
{
"message": "Welcome"
}
/src/locales/ca/translation.json
{
"message": "Benvinguts"
}
Part 4: Now, at any screen, you can show the text in the selected language from the redux setup:
import React from 'react';
import { useTypedSelector } from '../../store/reducers/reducer';
const Test = () => {
const t = useTypedSelector(state => state.language.data);
return (
<div> {t.message} </div>
)
}
export default Test;
Sorry for the post extension, but I tried to show the complete setup to clarify all doubts. Once this is done, it is very quick and flexible to add languages and use descriptions anywhere.
From my research into this there appears to be two main approaches being used to i18n in JavaScript, ICU and gettext.
I've only ever used gettext, so I'm biased.
What amazes me is how poor the support is. I come from the PHP world, either CakePHP or WordPress. In both of those situations, it's a basic standard that all strings are simply surrounded by __(''), then further down the line you get translations using PO files very easily.
gettext
You get the familiarity of sprintf for formatting strings and PO files will be translated easily by thousands of different agencies.
There's two popular options:
i18next, with usage described by this arkency.com blog post
Jed, with usage described by the sentry.io post and this React+Redux post,
Both have gettext style support, sprintf style formatting of strings and import / export to PO files.
i18next has a React extension developed by themselves. Jed doesn't. Sentry.io appear to use a custom integration of Jed with React. The React+Redux post, suggests using
Tools: jed + po2json + jsxgettext
However Jed seems like a more gettext focussed implementation - that is it's expressed intention, where as i18next just has it as an option.
ICU
This has more support for the edge cases around translations, e.g. for dealing with gender. I think you will see the benefits from this if you have more complex languages to translate into.
A popular option for this is messageformat.js. Discussed briefly in this sentry.io blog tutorial. messageformat.js is actually developed by the same person that wrote Jed. He makes quite stong claims for using ICU:
Jed is feature complete in my opinion. I am happy to fix bugs, but generally am not interested in adding more to the library.
I also maintain messageformat.js. If you don't specifically need a gettext implementation, I might suggest using MessageFormat instead, as it has better support for plurals/gender and has built-in locale data.
Rough comparison
gettext with sprintf:
i18next.t('Hello world!');
i18next.t(
'The first 4 letters of the english alphabet are: %s, %s, %s and %s',
{ postProcess: 'sprintf', sprintf: ['a', 'b', 'c', 'd'] }
);
messageformat.js (my best guess from reading the guide):
mf.compile('Hello world!')();
mf.compile(
'The first 4 letters of the english alphabet are: {s1}, {s2}, {s3} and {s4}'
)({ s1: 'a', s2: 'b', s3: 'c', s4: 'd' });
If not yet done having a look at https://react.i18next.com/ might be a good advice. It is based on i18next: learn once - translate everywhere.
Your code will look something like:
<div>{t('simpleContent')}</div>
<Trans i18nKey="userMessagesUnread" count={count}>
Hello <strong title={t('nameTitle')}>{{name}}</strong>, you have {{count}} unread message. <Link to="/msgs">Go to messages</Link>.
</Trans>
Comes with samples for:
webpack
cra
expo.js
next.js
storybook integration
razzle
dat
...
https://github.com/i18next/react-i18next/tree/master/example
Beside that you should also consider workflow during development and later for your translators -> https://www.youtube.com/watch?v=9NOzJhgmyQE
I would like to propose a simple solution using create-react-app.
The application will be built for every language separately, therefore whole translation logic will be moved out of the application.
The web server will serve the correct language automatically, depending on Accept-Language header, or manually by setting a cookie.
Mostly, we do not change language more than once, if ever at all)
Translation data put inside same component file, that uses it, along styles, html and code.
And here we have fully independent component that responsible for its own state, view, translation:
import React from 'react';
import {withStyles} from 'material-ui/styles';
import {languageForm} from './common-language';
const {REACT_APP_LANGUAGE: LANGUAGE} = process.env;
export let language; // define and export language if you wish
class Component extends React.Component {
render() {
return (
<div className={this.props.classes.someStyle}>
<h2>{language.title}</h2>
<p>{language.description}</p>
<p>{language.amount}</p>
<button>{languageForm.save}</button>
</div>
);
}
}
const styles = theme => ({
someStyle: {padding: 10},
});
export default withStyles(styles)(Component);
// sets laguage at build time
language = (
LANGUAGE === 'ru' ? { // Russian
title: 'Транзакции',
description: 'Описание',
amount: 'Сумма',
} :
LANGUAGE === 'ee' ? { // Estonian
title: 'Tehingud',
description: 'Kirjeldus',
amount: 'Summa',
} :
{ // default language // English
title: 'Transactions',
description: 'Description',
amount: 'Sum',
}
);
Add language environment variable to your package.json
"start": "REACT_APP_LANGUAGE=ru npm-run-all -p watch-css start-js",
"build": "REACT_APP_LANGUAGE=ru npm-run-all build-css build-js",
That is it!
Also my original answer included more monolithic approach with single json file for each translation:
lang/ru.json
{"hello": "Привет"}
lib/lang.js
export default require(`../lang/${process.env.REACT_APP_LANGUAGE}.json`);
src/App.jsx
import lang from '../lib/lang.js';
console.log(lang.hello);