- Published on
Type-Safe Localization for React App Using Flow
- Authors
- Name
- Nico Prananta
- Follow me: @2co_p
The Challenge
Switzerland has four national languages: German, French, Italian, and Romansh. The first three languages are the languages which are used by the majority of the people. For that reason most web apps I made here need to support at least those three and English in some.
When developing our first web app, I explored the popular React library for internationalization, React Intl. But I decided not to use it since it's overkill for our simple requirement: we just need to display different strings based on the selected language.
Keep it simple!
Our solution was just to use a simple Javascript object which contains the strings for each of the languages.
// @flow
// locales.js file
// you can separate the translations object into different files if you want
export const de = {
title: 'iTheorie Online-Lernen',
welcomeTo: 'Willkommen bei $title',
// and so on ...
}
export type StringsType = typeof de
export const fr: StringsType = {
title: 'Apprentissage en ligne iTheorie',
welcomeTo: 'Bienvenue chez $title',
}
To use the translation, we just need to pass the object to the components that need them.
export const TitleComponent = ({
strings = require('./locales').default,
}: {
strings: StringsType,
}) => <p>{strings.siteTitle}</p>
We use Flow to add type annotation for our translation object. By using it, not only we can prevent missing translation strings, we can also get autocompletion in editor, like Code, when using it in our React components.
To handle language switching, we can use Redux or React's built-in Context. When using Redux, we can create a reducer that will return the translations object based on the selection language.
export const defaultState = {
selected: 'de',
messages: require('./locales').de,
}
export default (
state: StringsType = defaultState,
action: {
type: 'SET_LANGUAGE_ACTION',
payload: {
selected: string,
messages: StringsType,
},
}
): LanguageState => {
switch (action.type) {
case 'SET_LANGUAGE_ACTION': {
return {
...state,
...action.payload,
}
}
default:
}
return state
}
To support formatted message, we created a small function to replace certain placeholder strings with the actual string.
const escapeRegex = (value: string) =>
value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
export const formatted = (text: string, replacement: Object): string => {
var newText = text
Object.keys(replacement).forEach(key => {
let regex = new RegExp(escapeRegex(key), 'g')
newText = newText.replace(regex, replacement[key])
})
return newText
}
For example, we have a translation string with key welcomeTo
and value Willkommen bei $title
in the translation object for German. We use the formatted
function to replace $title
with iTheorie Online-Lernen
export const Greeting = ({ strings }: { strings: StringsType }) => {
const stringToUse = formatted(strings.welcomeTo, {
$title: 'iTheorie Online-Lernen',
})
return <p>{stringToUse}</p>
}
Conclusion
This solution is satisfying for several reasons:
- No need for 3rd party dependency
- Prevent missing translation
- Auto completion in code editor.