This is a simple POC repo for setting up a "main" React 17 app (react-main-container
) which consumes a React 17 microfrontend (react-mfe
) using Webpack 5 module federation. The main purpose of this POC is to investigate internationalization (react-i18next
) with this type of setup for the best compromise between colocated MFE translations, TypeScript support, and ideal testing.
For a history of different approaches and tradeoffs, see commit history. Ultimately the current state of the repo represents an architecture where MFEs own their own i18n instances for more flexibility and independence (rather than pass down the i18n instance from the host to the MFE to be shared). The MFE also is able to remain in sync with the host app instance on language changes and can also access some of its translations if necessary (at the cost of less typesafety for those translations and having to mock those translations in tests).
- Run the microfrontend (runs on
localhost:5000
)
cd react-mfe
yarn
yarn start
- Since
react-mfe
leverages thei18n
instance fromreact-main-container
, it cannot run independently. Solving this is not within the scope of this POC.
- Run the main app (runs on
localhost:4000
)
- cd
react-main-container
yarn
yarn start
- Go to
localhost:4000
& click on the button to toggle between English & French locales.
yarn lint
yarn lint:i18n
for ensuring translation files in different locales for that app have identical keys, are sorted alphabetically, and follow a proper format.
- The main application,
react-main-container
, defines inwebpack.config.js
what microfrontends it will consume. - The MFE,
react-mfe
, exposesMicrofrontend.tsx
inwebpack.config.js
so that it can be imported byreact-main-container
. react-main-container
initializes two namespaces:common
andapp
react-mfe
initializes three namespaces:mfe
,error
, andhost-common
(a namespace loaded from host into MFE)
Sharing assets between MFE & Host and more:
- module-federation/module-federation-examples#697
- manfredsteyer/module-federation-plugin-example#21
- https://stackoverflow.com/questions/71087541/angular-module-federation-how-can-i-make-static-files-assets-i18n-available
- https://stackoverflow.com/questions/67633345/serving-styles-and-assets-with-webpack-5-module-federation
- https://stackoverflow.com/questions/69192229/angular-mfe-webpack5-module-federation-image-path-issue
- https://itsnotbugitsfeature.com/2022/03/13/sharing-common-assets-in-angulars-module-federation/
- https://dev.to/waldronmatt/tutorial-a-guide-to-module-federation-for-enterprise-n5
- https://scriptedalchemy.medium.com/micro-fe-architecture-webpack-5-module-federation-and-custom-startup-code-9cb3fcd066c
- module-federation/module-federation-examples#102
In order to typically (integrate TypeScript)[https://react.i18next.com/latest/typescript] for react-i18next
internationalization projects, a i18next.d.ts
file is added which imports project locale files for type safety and augmented intellisense. Now, when using the t
translate function, you'll get errors on invalid keys & see available keys! One caveat though, is when using the useTranslation
hook you must pass in the namespaces being used otherwise the types will not work as expected.
This works fine for typical monolithic frontend applications however, in an architecture where an app consumes multiple i18n instances, TypeScript is a lot more difficult to configure. Based on comments in the repo, there does not appear to be official support for this type of situation - www.github.com/i18next/react-i18next/issues/726#issuecomment-1499882853
.
As mentioned above although typescript integration is straightforward in the host app since there is only one i18n
instance, the MFE app accesses both its own translations and the host app ones. When configuring typescript for the MFE i18n instance with i18next.d.ts
, due to its global namespace, it also affects types for the host app instance (and when we import certain APIs like the Trans
component, it'll automatically use types generated by the MFE i18next.d.ts
making it difficult to use for host app translations). I can make it work by typing the host app instance with an overridden t
field (i.e. typed as any) so it won't error when using host translation keys but you do lose intellisense. Alternatively, using types like TFunction
gives it more intellisense but also makes it more annoying to work with for the Trans
component - overall it's very hacky playing with the types like this.
Since the goal of this POC is to also assume that MFEs are in their own repo, it's difficult to enable full type safety for host app translations (i.e. if it was a monorepo for example, it could have access to directly import those types/translation files). There are some tools/approaches like https://github.com/module-federation/universe/tree/main/packages/typescript, npm packages, and https://spin.atomicobject.com/2022/07/19/typescript-federated-modules/ for sharing assets across federated modules but this does add complexity and isn't guaranteed to work due to how TypeScript works with i18-next
(where you augment types in a single .d.ts
file) + us using multiple i18n instances. This requires us to maybe rethink our approach of having multiple i18n instances.
Assuming we want the MFE to have access to host app translations, there are mainly two approaches. The first approach assumes you want to keep separate i18n instances (the host app i18n instance is passed to MFE and consumed with React Context) in which case it may be better to forego typescript integration for the microfrontend to avoid running into edge cases and annoyances with typing host app translations. Alternatively, we have a single i18n
instance in the MFE that dynamically loads in host translations and then we manually type namespaces for the host translations. For simplicity we can type the namespace so that for that namespace any strings can be accepted (so it doesn't have full type safety but errors if strings aren't passed in the t
function for example) - this is done in the current POC. Alternatively, since host translations get dynamically loaded into the MFE instance at run-time, the only way to have full type safety for host translations is for the MFE to have a "synced copy" of the host app locale files whether this is through an npm package
or some process to pull in those files from the host app copy.
Assuming (react-testing-library)[https://testing-library.com/docs/react-testing-library/setup/] and Jest is being used, some relevant docs are:
- https://react.i18next.com/misc/testing
- https://jestjs.io/docs/configuration
- https://codesandbox.io/s/kentcdoddsreact-testing-library-examples-pl10s?file=/src/__tests__/i18next.js
- https://github.com/i18next/react-i18next/tree/master/example/test-jest
Ultimately there are two different approaches we can go with for setting up our tests to work with i18next
. We can either choose to:
- Mock i18n and related functionalities
- If we decide to mock i18n, then in the testing DOM any text will just appear as
i18n
translation keys. A test assertion may look likescreen.getByText('GREETING')
- Implementation wise, assuming
jest
, create areact-i18next.js
in a__mocks__
folder and fill it with the following content: https://github.com/i18next/react-i18next/blob/master/example/test-jest/src/mocks/react-i18next.js.
- Do not mock i18n and translate text in tests
- Assertions on text will look like:
screen.getByText('Welcome to the application!')
orscreen.getByText(ENCommonTranslations.GREETING)
(if you directly import the JSON translations file)
This project uses option 2 as it gives more confidence that things are working (the translation key actually has an associated translation & i18n is set up correctly).In addition, where possible, importing the JSON translations file to use in assertions keeps things concise, is synced to the translation so if you update the translation the test still works fine, and you can easily ctrl-click
to go directly to that translation (nice DX). However for more complex translations with interpolation (i.e. "Welcome {{ name }} to the app") you cannot rely on just importing this translation from the JSON file in your test and will need to explicitly write out the string.