Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SSR support #79

Open
lesmo opened this issue Dec 17, 2019 · 12 comments
Open

Add SSR support #79

lesmo opened this issue Dec 17, 2019 · 12 comments
Labels
feature request New feature or request help wanted Extra attention is needed

Comments

@lesmo
Copy link

lesmo commented Dec 17, 2019

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

X-All-The-Y
Because localize all the things everywhere!

Possible implementation

I noticed the calls are to navigatorand 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.

@zoontek zoontek added help wanted Extra attention is needed feature request New feature or request labels Dec 17, 2019
@zoontek
Copy link
Owner

zoontek commented Dec 17, 2019

@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.

@lesmo
Copy link
Author

lesmo commented Dec 17, 2019

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.

@zoontek
Copy link
Owner

zoontek commented Jan 5, 2020

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.

@lesmo
Copy link
Author

lesmo commented Jan 13, 2020

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. 😀

@zoontek
Copy link
Owner

zoontek commented Jan 26, 2020

@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)!

@lesmo
Copy link
Author

lesmo commented Jan 28, 2020

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? 😅

@zoontek
Copy link
Owner

zoontek commented Jan 29, 2020

@lesmo Totally in the v2 TODO list 🙂

@sanderlooijenga
Copy link

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!

@lesmo
Copy link
Author

lesmo commented Jun 23, 2020

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.

@zoontek
Copy link
Owner

zoontek commented Jun 24, 2020

FormatJS offers a full set of polyfills: https://formatjs.io/docs/polyfills
I see no mentions of NodeJS support, but it might be compatible.

@lesmo
Copy link
Author

lesmo commented Jun 24, 2020

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!

@longlho
Copy link

longlho commented Aug 17, 2020

We (formatjs) do support Node, although Node 14+ has almost everything you need (sans the bugs that we fixed)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

4 participants