The age of globalization has opened the door for a diverse range of internet content, especially for those that is not English. This creates a trend where websites become adaptible to multiple languages, serving appropriate contents according to user's preferences. These websites are known to have been internationalized.
In my research of making such sites in React, a popular framework for web development, I have followed many tutorials of many existing libraries to implement the features, yet failed to make the site serving the correct content. This frustrated me, since I think internationalization should accomplish something as simple as a change of text. Thus, I have to change my perspective, by looking at the foundation of those libraries, which is React's Context.
Prerequisites
Below are what I think you should have known before continuing with this document:
- Basic understanding of React, more specifically: Function component, component's props, state and effect
- Javascript's ES6 syntax and JSON file format.
- An IDE, a software that supports your coding.
If you are confident with the knowledge above, feel free to take a look at this demo for reference. Or, you can create a sample React app repository to follow along, using these commands in a folder opened in your chosen IDE:
- For NPM users:
npx create-react-app
- For Yarn users:
yarn create react-app
The content JSON files
These files are what I used to store translatable texts, any piece of content that can be switched between languages. Here is the English content of the demo, which I named en.json:
{
"en": "English",
"vi": "Vietnamese",
"edit": "Edit",
"saveToReload": "and save to reload",
"learnReact": "Learn React"
}
Indeed, the texts above come from the generated React repository. I simply translate those to my native language Vietnamese and create a new file, named vi.json. Remember, it's important to standardize these content files. They should always have the same keys but different values.
Using those content files in a context is a bit tricky, since you would need to import the files asynchronously, read and return them.
const dictionaries = {
en: () => import("../contents/en.json").then((module) => module.default),
vi: () => import("../contents/vi.json").then((module) => module.default),
};
const getDictionary = async (locale) => dictionaries[locale]?.() ?? dictionaries.en();
The constant dictionaries stands for the collection of read JSON contents.
The function getDictionary is how we would get the correct content for usage in the context.
The context and its provider
React's context is a convinient method to manage states that are meant to be used in multiple components. In the demo, these values would be:
- locale: the current language of the website, should be either 'en' or 'vi' for the sake of simplicity.
- setLocale: the method to set the website to another language, its only parameter.
- dictionary: the content corresponding to the current language.
Here is how the context is defined with those values:
const LocaleContext = createContext({
locale: defaultLocale,
setLocale: (lang) => { },
dictionary: {},
});
In order to used that context in any component, a provider must be defined. It's a wrapper component that takes the children props, which means it would be the parent of other components within the same tree. Because of this, we turn those values into states that can affect the children or wrapped components.
function LocaleProvider({ children }) {
const [lang, setLang] = useState(defaultLocale);
const [dictionary, setDictionary] = useState({});
useEffect(() => {
(async () => {
const json = await getDictionary(lang);
setDictionary(json);
})()
}, [lang]);
return (
<LocaleContext.Provider value={{ locale: lang, setLocale: setLang, dictionary: dictionary }}>
{children}
</LocaleContext.Provider>
)
}
As you can see, the hook useEffect is for listening to the changes of the state that I named lang. So, whenever lang is set to a different value, a nameless async function would be called to get the content corresponding to that lang. This is how the website's content is served.
After all definitions are done, the context can now be used in React's main component by wrapping other components:
function App() {
return (
<div className="App">
<header className="App-header">
<LocaleProvider>
<img src={logo} className="App-logo" alt="logo" />
<AppContent />
</LocaleProvider>
</header>
</div>
);
}
How to use context and change its states
The common usage of the mentioned context is displaying content. Here is how it's done in the component named AppContent, which is wrapped by the context provider. In a sense, it would be similar to destructuring an object:
function AppContent() {
const { dictionary } = useContext(LocaleContext)
return (
<div>
<p>{dictionary.edit} <code>src/App.js</code> {dictionary.saveToReload}.</p>
<div style={{ display: "flex", justifyItems: 'center', alignItems: 'center' }}>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
style={{marginRight: 20}}
>
{dictionary.learnReact}</a>
<LocaleSwitcher />
</div>
</div>
)
}
You might have noticed another component that is named LocaleSwitcher. This one is a simple switch to control the current language of the website, using the defined state setter named setLocale:
function LocaleSwitcher() {
const { dictionary, setLocale } = useContext(LocaleContext)
const switchLanguage = (value) => {
if (['en', 'vi'].includes(value)) {
setLocale(value)
} else {
setLocale(defaultLocale)
}
}
return (
<select defaultValue={defaultLocale} onChange={(event) => switchLanguage(event.target.value)}>
<option value="en">{dictionary.en}</option>
<option value="vi">{dictionary.vi}</option>
</select>
)
}
Basically, it's a select tag with a handle for when its option is selected. The option's values should match the acceptable values of the locale state, as the handler function switchLanguage would set that state to any of those. So by selecting any of those options, you should be able to change the content in the demo.
Everything in Typescript
Personally, I prefer Typescript to Javascript because of type safety. A mishandling value would result in the application ended up in layers of exception. Besides, who doesn't want to have a clear vision on the inputs and outputs of their system? Well, that is if you can handle the nuisance of having to define types in almost every piece of code.
Let's begin with the types (and interfaces, which are object types) used with the context states:
type Locale = 'en' | 'vi'
type Dictionary = Awaited<ReturnType<typeof getDictionary>>
interface ILanguageContext {
locale: Locale;
setLocale: (newLang: Locale) => void;
dictionary: Dictionary;
}
As you can see, the Locale type represents all supporting languages in the demo. The Dictionary one stands for the type of the read JSON content files, which is an object type with clear defined keys. Finally, the context's interface to combine those 2 types. Here is how the types are used in context, under the Typescript syntax that is called generic:
const LanguageContext = createContext<ILanguageContext>({ locale: 'en', setLocale: (lang) => { }, contents: {} });
function LanguageProvider({ children }: PropsWithChildren) {
const [lang, setLang] = useState<Locale>('en');
const [dictionary, setDictionary] = useState<Dictionary>({});
useEffect(() => {
(async () => {
const json = await getDictionary(lang);
setDictionary(json);
})()
}, [lang]);
return (
<LanguageContext.Provider value={{ locale: lang, setLocale: setLang, contents: dictionary }}>
{children}
</LanguageContext.Provider>
)
}
Other than above changes, nothing else really need to change in the demo's main code.
Final thought
I admit that internationalization is more than just changing texts. There are things like time zone(s), currency, number format that need to be changed, as well, and they are based on location, not selected language.
The demo, at the moment, fails to detect that as I hard-coded the default language to be English ('en') without taking into account of the user's location.
Another disadvantage of this text-change context is its dependence to static files, the JSON ones. Thus, a roburst website with dynamic content would require a CMS (content management system) to handle its content.
The context in the demo should be usable in the React ecology, meaning all React-based frameworks. I used an instance of it in my NextJS project since all I need is a simple text change.
I hope you enjoy this tutorial. Let me know if you have any questions by: Github, LinkedIn or Email.
Thank you for your time.
Top comments (0)