We recently had a bit of a challenge trying to set up a semi-efficient way to bring translations into React from our server.
We had the idea to create a useTranslations()
hook, and to utilise the order in which components mount in order to collate the translation tags into an array - Then we can use this array to send out a single request to the server for each page to get all the required translations, and cache them in localStorage. To illustrate, usage will look like this;
App.jsx
const App = () => (
<TranslationProvider>
<MyPage />
</TranslationProvider>
)
MyPage.jsx
const MyPage = () => {
const { translate, fetchTranslations } = useTranslations();
// fetch translations on mount
useEffect(fetchTranslations, []);
return (
<>
<h1>{ translate('hello_world') }</h1>
<MyComponent />
</>
);
}
MyComponent.jsx
const MyComponent = () => {
const { translate } = useTranslations();
return (
<p>{ translate('description_1') }</p>
);
}
This came with a few challenges along the way, but we got there in the end and learned a ton, so I thought I'd share our solution for anyone needing anything similar.
Setting up our boilerplate
When making hooks that require a context/state, I like to use this little template. It's simple, keeps everything in one place and is really easy to extend.
TranslationState.jsx
const TranslationContext = createContext({});
export const TranslationProvider = ({ children }) => {
const state = {
// ...
};
return (
<TranslationContext.Provider value={state}>
{children}
</TranslationContext.Provider>
);
}
export const useTranslations = () => {
const context = useContext(TranslationContext);
if (!context) {
throw new Error('useTranslationState must be used within the TranslationProvider');
}
return context;
}
Fetching Translations
To begin with, we're going to create a state for the translations themselves. The format we're going to store them in through this example is { 'tag_name':'Translation value.' }
- but you can tailor this however you like.
TranslationState.jsx
//...
export const TranslationProvider = ({ children }) => {
// You could pull the default value from localStorage
// if you plan to cache the result on the client.
const [translations, setTranslations] = useState({});
const state = {
translations,
};
// ...
We're also going to create the function that fetches the translations from the server. We'll call this fetchTranslations()
and insert this into the state object so we can access it from the hook.
I've created a mocky endpoint, with some dummy-data to play with;
https://run.mocky.io/v3/28656dd4-871b-48fb-8a86-464cca940c76
TranslationState.jsx
//...
const [translations, setTranslations] = useState({});
const fetchTranslations = () => {
fetch('https://run.mocky.io/v3/28656dd4-871b-48fb-8a86-464cca940c76', {
method: 'POST',
//TODO: body should be an array of required translations,
}).then(
(data) => data.json()
).then(
// we update the state of translations with the
// new translations recieved from the server.
setTranslations({
...translations,
...newTranslations,
});
// You could also store these translations in
// localStorage here.
});
};
const state = {
translations,
fetchTranslations,
}
//...
Collating Translations and Rendering
This was the tricky part!
The Problem:
Our initial idea was to add the tags to the translations state, with the value of false, and send those tags to the server in fetchTranslations - however this messes up the order of operations! When we update the state, it means the parent will re-mount every time the children require a translation, meaning if we have 6 translations, we're going to send out 6 requests.
The Solution:
React has the useRef()
hook! This hook creates an object that will not re-mount components when it updates! Solving our order of operations issue!
TranslationState.jsx
//...
const translationsRef = useRef({})
const [translations, setTranslations] = useState(translationsRef.current);
const fetchTranslations = () => {
fetch('https://run.mocky.io/v3/28656dd4-871b-48fb-8a86-464cca940c76', {
method: 'POST',
body: Object.entries(translationsRef.current)
.filter(([, value]) => value === false)
.map(([tag]) => tag)
})
.then((data) => data.json())
.then((newTranslations) => {
translationsRef.current = {
...translationsRef.current,
...newTranslations,
};
setTranslations(translationsRef.current);
});
};
const translate = (tag, defaultValue = 'Loading...') => {
if (typeof translationsRef.current[tag] === 'undefined') {
translationsRef.current[tag] = false;
}
// You could return something fancy like;
// https://material-ui.com/components/skeleton/#animations
return translationsRef.current[tag] || defaultValue;
};
const state = {
translations,
fetchTranslations,
translate,
}
//...
And there we go!
If you found the walkthrough a little hard to understand, sorry about that! This is my first Dev.to post, I'm sure I'll improve. You can try out the hook here:
Please let me know what you think in the comments, and if you have any better solutions to the problem, do let me know!
Top comments (0)