React i18n Build Size Optimization 2: Splitting Translations by Page
This continues my earlier post, React i18n Build Size Optimization 1.
In the previous post, I moved the translation files that had been kept under src into the public directory and used i18next-http-backend to load only the needed language at runtime, which improved the JS bundle size and initial loading performance.
The next step was to split the translation data managed in a Google Sheet by page, and below is how I optimized it so each page loads only the translations it really needs.
Key design background: why I chose the namespace.key structure in advance
This optimization was not done by suddenly changing the structure. From the time I first designed the translations, I had designed the key structure with a later per-page split in mind.
Below is the structure of the Google Sheet for managing translations that is actually in use for the Autowini service.

I wrote the translation keys by these rules.
- Words likely to be shared across several pages are declared as a single key, like search
- Text that belongs only to a specific page or UI area, like myWini.saved, shippingSchedule.title, userSideMenu.username, is
clearly separated in namespace.key form
Google Sheet → JSON split by namespace
Instead of generating one translation file per language, I extended the logic to generate per-page JSON files by splitting namespaces based on the key. This took the structure one step further, so translation resources can be managed per page (feature).
It works like this.
- Fetch the translation data from the Google Sheet
- If a key contains a . → the part before the . is treated as the namespace
- A key without a . → treated as a shared translation and put in the common namespace
- Generate a JSON file for each language and namespace
The whole logic is fairly simple. The main part looks like this.
const keyParts = key.split('.');
const namespace = keyParts.length > 1 ? keyParts[0] : 'common';
const actualKey = keyParts.length > 1 ? keyParts.slice(1).join('.') : key;With this structure, files like the following are generated.
- /locales/ko/common.json
- /locales/ko/myWini.json
- /locales/ko/shippingSchedule.json
- /locales/ko/userSideMenu.json

i18n .ts setup after splitting namespaces
i18n
.use(HttpBackend)
.use(initReactI18next)
.init({
supportedLngs: supportLanguages,
fallbackLng: false,
ns: ['common', 'seo'],
lng: supportLanguages.includes(getCookieValue(PROPERTY_LANGUAGE) ?? '')
? getCookieValue(PROPERTY_LANGUAGE)!
: fallbackLng,
debug: false,
backend: {
loadPath: '/w/react-route/locales/{{lng}}/{{ns}}.json',
},
interpolation: {
escapeValue: false,
},
});
export default i18n;I changed loadPath to match the language + namespace structure, and set the common and seo namespaces, which every page uses, to load together on the initial load.
With this setup, the shared translations a page always needs on entry are available right away, and the remaining namespaces each page needs are loaded only later, when they are needed.
Looking back
Before, I had optimized it to load only the translation .json file for the selected language. This time I went one step further and optimized the structure to load only the translation .json files for the page that needs them. Thanks to this, even when all the legacy JSP pages are later moved to React, I think I was able to address problems like build size growth or slower initial rendering in advance.
- #Autowini
- #i18n
- #WiniLogis
- #Localization
- #Rendering Optimization
- #Per-Page Translation Splitting