react-native-localize
react-native-localize copied to clipboard
Add SSR support
Feature Request
Support for SSR. Current index.web.js is directly calling browser-only objects, which is not possible while being rendered on the server.
Why it is needed
Because localize all the things everywhere!
Possible implementation
I noticed the calls are to navigator
and window
objects. Those statements are called as soon as any code imports react-native-localize
, so it would be necessary to "delay" that to a later stage, or call them lazily or even conditionally. I don't know how this module's internals work... yet 😏
Code sample
This is a conditional example that would render properly on SSR:
export let constants: LocalizationConstants = generateConstants(
(navigator && navigator.languages) || [],
);
window && window.addEventListener("languagechange", () => {
constants = generateConstants(navigator.languages);
handlers.forEach(handler => handler());
});
The only complication is how to make constants
somehow wait or be populated until the browser is ready without breaking anything. This could work:
/* Server Side would need to be populated some othery way... */
export let constants: LocalizationConstants; // Would be undefined until...
document && document.addEventListener("DOMContentLoaded", function(event) {
constants = navigator.languages; // We're in business
});
But I'm not sure if an undefined constants
would break something, and there would of course need to be a way to know from the client which locale to use. I'll get back if I come up with something.
@lesmo Hi 👋
I'm not a big user of SSR but it should be do-able. The main pain point is currently to parse and send the Accept-Language
header to the module to have valid data at startup.
Imagine:
// on server
import { parseAcceptLanguageHeader } from "react-native-localize/server"
// …
render(<App languages={parseAcceptLanguageHeader(header)} />)
In your app:
generateConstants(languages)
But could it be enough? getNumberFormatSettings
, getTimeZone
, uses24HourClock
depends on browser Intl
API.
Another solution could be to switch to lazy, synchronous getters.
Oh I like that idea! Passing Accept-Language
should be an easy task from most of the major SSR libraries (I'm trying to get this to work with razzle btw).
The browser API could be polyfilled for SSR... 🥁 drums for dramatic effect... 🥁 with intl perhaps? Using it as an optional dependency would allow for this magic to happen.
Intl.js
seems unmaintained 😞
An easy start could be lazy evaluation: stop generating constants at start, but instead only when requested. It would make SSR related future work easier.
I've looked into that and while it hasn't been updated, I wouldn't say it's unmaintained... just dusty. 😜
While researching for some bugs on my Android build, I found some mentions of that polyfill as a solution to the missing Intl
object and some problems due to missing implementations. As far as I could tell, I think those missing implementations wouldn't make much of a difference... but still, after some thought even if that polyfill was maintained, I don't think it's a good idea to have it be a dependency to this lib. It's easier to say "wanna use it for SSR? make sure you have this or this", which brings me to my next point:
I think a reasonable solution would be to warn and maybe even have users of react-native-localize
use a Node environment with support for internationalization when pretending to use it for SSR. Node has built-in support for the required stuff, although some builds might not have it built-in... but that's for the implementor to solve (maybe even with the polyfill).
I believe that's a nice solution. Just rewrite the web stuff to be lazy, and document that it'll need to be run on Node with proper support.
I'll see if I can put together a PR. 😀
@lesmo I just published a new version with lazy getters on the web version: https://github.com/react-native-community/react-native-localize/releases/tag/1.3.3
Now we are free of code that calls navigator
or window
objects at module init. 😌
It should be easier to work on SSR support (we still need to find a way to parse the accept-language
header and pass it down to the functions)!
Thanks! I'll try this out!
I've been thinking about the Accept-Language
thing, and it's quite difficult to solve with the current API. A solution I thought was making the RNLocalize
an instantiable object that can be given an override param. Borrowing from react-navigation
example for SSR (and actually quite similar to my solution), I imagined:
expressApp.get("/*", (req, res) => {
const { path, query } = req;
const runtimeLocales = parser.parse(req.get('Accept-Language');
const localize = new RNLocalize({ runtimeLocales });
const { navigation, title, options } = handleServerRequest(
AppNavigator.router,
path,
query
);
// register the app
AppRegistry.registerComponent('App', () => App);
// prerender the app
const { element, getStyleElement } = AppRegistry.getApplication('App', {
initialProps: { navigation, localize }, // now <App> has localize prop
});
const markup = renderToString(<AppNavigator navigation={navigation} />);
res.send(
`<!doctype html>
<html lang="">
<head>
<title>${title}</title>
<script src="main.js"></script>
</head>
<body>
<div id="root">${markup}</div>
</body>
</html>`
);
});
This way we can do:
// App.js
export default App = ({ localize }) => {
const lang = localize.findBestAvailableLanguage(['en', 'en-GB', 'fr', 'pr']);
return <AnAwesomeApp />
}
Or fancier, putting it inside a react context one could use hooks too:
// App.js
export default App = ({ runtimeLocales }) => {
return (
// Directly "override" the platform available languages with
// the ones from express
<LocalizeProvider platformLanguages={runtimeLocales}>
<AnAwesomeApp />
</LocalizeProvider>
)
}
// Somewhere.js
export const Somewhere = () => {
const localize = useLocalize();
return (
<Text>{localize.getCountry()}</Text>
);
};
This way there's no direct dependency on global variables and it's SSR friendly... but it's a massive refactor, and I'm not sure it's a good idea (yet) or if could benefit other use cases at all. Something for like v2 maybe? 😅
@lesmo Totally in the v2 TODO list 🙂
Hi guys, I would be interested in this as well. Do you have some roadmap (for the v2) on when this will be implemented?
Cheers!
Just a heads up, Intl.js will no longer be maintained so... I guess the best route forward would be to consider having implementers make sure they run Node with Intl compiled into the final binary.
FormatJS offers a full set of polyfills: https://formatjs.io/docs/polyfills I see no mentions of NodeJS support, but it might be compatible.
FormatJS offers a full set of polyfills: https://formatjs.io/docs/polyfills I see no mentions of NodeJS support, but it might be compatible.
Actually, the home page says it does so... that's the one!
We (formatjs) do support Node, although Node 14+ has almost everything you need (sans the bugs that we fixed)