Next.js: DELETE_ME

Created on 21 Feb 2019  ·  20Comments  ·  Source: vercel/next.js

good first issue

Most helpful comment

Also experiencing an issue with this - are there any alternatives for i18n that works with serverless next?

All 20 comments

@jlsbi I modified a little the original example with-intl to make this example which works with @now/next and react-intl .
You can try to fork and deploy to see !

Also experiencing an issue with this - are there any alternatives for i18n that works with serverless next?

+1 on this, still searching for a solution

+1, also looking to do this.

+1

+1 - team I'm working on may look into solving this as we'd love to use this library for our next project.

I have the beginnings of a workaround!

Some observations: (apologies in advance if these things are obvious -- I've only been doing Next.js for like 2 days so give me a break)

  1. Looks like Now doesn't run server.js, which is the heart of all the i18n code in that example.
  2. The goal of the example is to get the locale and messages to _app.js and to load a formatjs script for the locale into _document.js.

I verified that server.js doesn't run by throwing an exception in some server.js main code, which blows up locally 👍, but not when deployed to Now:

/// server.js
/// (comes from the example code https://github.com/zeit/next.js/blob/master/examples/with-react-intl)
...
app.prepare().then(() => {
  createServer((req, res) => {
    const accept = accepts(req);
    const locale = accept.language(supportedLanguages) || 'en';
    req.locale = locale;
    req.localeDataScript = getLocaleDataScript(locale);
    req.messages = dev ? {} : getMessages(locale);
    throw 'should fail'; // <------------------------------------ (doesn't fail 🎉)
    handle(req, res);
  }).listen(port, err => {
    if (err) throw err;
    console.log(`> Ready on http://localhost:${port}`);
  });
});

For my workaround, I was able to get my translations to "work" by slapping some crap into _app.js and _document.js:

/// pages/_app.js
/// (also comes from the example code)

  render() {
    let { Component, pageProps, locale, messages } = this.props;
    const strings = {
          'en': {
            "nav-home": "Home",
            "nav-zeit": "ZEIT",
            "nav-github": "GitHub"
          },
          'fr': {
            "title": "React Intl Next.js Exemple",
            "nav-home": "Hoame",
            "nav-zeit": "ZEIT",
            "nav-github": "GeetHoob",
            "description": "Un exemple d'application intégrant React Intl avec Next.js",
            "greeting": "Bonjour le monde!"
          }
        };

    locale = locale || 'fr';
    messages = messages || strings[locale]; // <---------- hardcoded fallbacks 🎉

    const intl = createIntl(
      {
        locale,
        messages
      },
      cache
    );

    return (
      <RawIntlProvider value={intl}>
        <Component {...pageProps} />
      </RawIntlProvider>
    );
  }

.
.
.

/// pages/_document.js
/// (also comes from the example code)

  render() {
    // Polyfill Intl API for older browsers
    const polyfill = `https://cdn.polyfill.io/v3/polyfill.min.js?features=Intl.~locale.${this.props.locale}`;

    return (
      <html>
        <Head />
        <body>
          <Main />
          <script src={polyfill} />
          <script
            dangerouslySetInnerHTML={{
              __html: this.props.localeDataScript ||
`if (Intl.RelativeTimeFormat && typeof Intl.RelativeTimeFormat.__addLocaleData === 'function') {
  Intl.RelativeTimeFormat.__addLocaleData({"data":{"fr-CA":{"quarter":{"0":"ce trimestre-ci","1":"le trimestre prochain","future":{"one":"dans {0} trimestre","other":"dans {0} trimestres"},"past":{"one":"il y a {0} trimestre","other":"il y a {0} trimestres"},"-1":"le trimestre dernier"},"quarter-short":{"0":"ce trim.","1":"trim. prochain","future":{"one":"dans {0} trim.","other":"dans {0} trim."},"past":{"one":"il y a {0} trim.","other":"il y a {0} trim."},"-1":"trim. dernier"},"quarter-narrow":{"0":"ce trim.","1":"trim.prochain","future":{"one":"+{0} trim.","other":"+{0} trim."},"past":{"one":"-{0} trim.","other":"-{0} trim."},"-1":"trim. dernier"},"second-narrow":{"0":"maintenant","future":{"one":"+ {0} s","other":"+{0} s"},"past":{"one":"-{0} s","other":"-{0} s"}}},"fr":{"nu":["latn"],"year":{"0":"cette année","1":"l’année prochaine","future":{"one":"dans {0} an","other":"dans {0} ans"},"past":{"one":"il y a {0} an","other":"il y a {0} ans"},"-1":"l’année dernière"},"year-short":{"0":"cette année","1":"l’année prochaine","future":{"one":"dans {0} a","other":"dans {0} a"},"past":{"one":"il y a {0} a","other":"il y a {0} a"},"-1":"l’année dernière"},"year-narrow":{"0":"cette année","1":"l’année prochaine","future":{"one":"+{0} a","other":"+{0} a"},"past":{"one":"-{0} a","other":"-{0} a"},"-1":"l’année dernière"},"quarter":{"0":"ce trimestre","1":"le trimestre prochain","future":{"one":"dans {0} trimestre","other":"dans {0} trimestres"},"past":{"one":"il y a {0} trimestre","other":"il y a {0} trimestres"},"-1":"le trimestre dernier"},"quarter-short":{"0":"ce trimestre","1":"le trimestre prochain","future":{"one":"dans {0} trim.","other":"dans {0} trim."},"past":{"one":"il y a {0} trim.","other":"il y a {0} trim."},"-1":"le trimestre dernier"},"quarter-narrow":{"0":"ce trimestre","1":"le trimestre prochain","future":{"one":"+{0} trim.","other":"+{0} trim."},"past":{"one":"-{0} trim.","other":"-{0} trim."},"-1":"le trimestre dernier"},"month":{"0":"ce mois-ci","1":"le mois prochain","future":{"one":"dans {0} mois","other":"dans {0} mois"},"past":{"one":"il y a {0} mois","other":"il y a {0} mois"},"-1":"le mois dernier"},"month-short":{"0":"ce mois-ci","1":"le mois prochain","future":{"one":"dans {0} m.","other":"dans {0} m."},"past":{"one":"il y a {0} m.","other":"il y a {0} m."},"-1":"le mois dernier"},"month-narrow":{"0":"ce mois-ci","1":"le mois prochain","future":{"one":"+{0} m.","other":"+{0} m."},"past":{"one":"-{0} m.","other":"-{0} m."},"-1":"le mois dernier"},"week":{"0":"cette semaine","1":"la semaine prochaine","future":{"one":"dans {0} semaine","other":"dans {0} semaines"},"past":{"one":"il y a {0} semaine","other":"il y a {0} semaines"},"-1":"la semaine dernière"},"week-short":{"0":"cette semaine","1":"la semaine prochaine","future":{"one":"dans {0} sem.","other":"dans {0} sem."},"past":{"one":"il y a {0} sem.","other":"il y a {0} sem."},"-1":"la semaine dernière"},"week-narrow":{"0":"cette semaine","1":"la semaine prochaine","future":{"one":"+{0} sem.","other":"+{0} sem."},"past":{"one":"-{0} sem.","other":"-{0} sem."},"-1":"la semaine dernière"},"day":{"0":"aujourd’hui","1":"demain","2":"après-demain","future":{"one":"dans {0} jour","other":"dans {0} jours"},"past":{"one":"il y a {0} jour","other":"il y a {0} jours"},"-2":"avant-hier","-1":"hier"},"day-short":{"0":"aujourd’hui","1":"demain","2":"après-demain","future":{"one":"dans {0} j","other":"dans {0} j"},"past":{"one":"il y a {0} j","other":"il y a {0} j"},"-2":"avant-hier","-1":"hier"},"day-narrow":{"0":"aujourd’hui","1":"demain","2":"après-demain","future":{"one":"+{0} j","other":"+{0} j"},"past":{"one":"-{0} j","other":"-{0} j"},"-2":"avant-hier","-1":"hier"},"hour":{"0":"cette heure-ci","future":{"one":"dans {0} heure","other":"dans {0} heures"},"past":{"one":"il y a {0} heure","other":"il y a {0} heures"}},"hour-short":{"0":"cette heure-ci","future":{"one":"dans {0} h","other":"dans {0} h"},"past":{"one":"il y a {0} h","other":"il y a {0} h"}},"hour-narrow":{"0":"cette heure-ci","future":{"one":"+{0} h","other":"+{0} h"},"past":{"one":"-{0} h","other":"-{0} h"}},"minute":{"0":"cette minute-ci","future":{"one":"dans {0} minute","other":"dans {0} minutes"},"past":{"one":"il y a {0} minute","other":"il y a {0} minutes"}},"minute-short":{"0":"cette minute-ci","future":{"one":"dans {0} min","other":"dans {0} min"},"past":{"one":"il y a {0} min","other":"il y a {0} min"}},"minute-narrow":{"0":"cette minute-ci","future":{"one":"+{0} min","other":"+{0} min"},"past":{"one":"-{0} min","other":"-{0} min"}},"second":{"0":"maintenant","future":{"one":"dans {0} seconde","other":"dans {0} secondes"},"past":{"one":"il y a {0} seconde","other":"il y a {0} secondes"}},"second-short":{"0":"maintenant","future":{"one":"dans {0} s","other":"dans {0} s"},"past":{"one":"il y a {0} s","other":"il y a {0} s"}},"second-narrow":{"0":"maintenant","future":{"one":"+{0} s","other":"+{0} s"},"past":{"one":"-{0} s","other":"-{0} s"}}}},"availableLocales":["fr-BE","fr-BF","fr-BI","fr-BJ","fr-BL","fr-CA","fr-CD","fr-CF","fr-CG","fr-CH","fr-CI","fr-CM","fr-DJ","fr-DZ","fr-GA","fr-GF","fr-GN","fr-GP","fr-GQ","fr-HT","fr-KM","fr-LU","fr-MA","fr-MC","fr-MF","fr-MG","fr-ML","fr-MQ","fr-MR","fr-MU","fr-NC","fr-NE","fr-PF","fr-PM","fr-RE","fr-RW","fr-SC","fr-SN","fr-SY","fr-TD","fr-TG","fr-TN","fr-VU","fr-WF","fr-YT","fr"],"aliases":{},"parentLocales":{}})
}`
            }} // ^---- yanked from node_modules/@formatjs/intl-relativetimeformat/dist/locale-data/fr.js 🎉
          />
          <NextScript />
        </body>
      </html>
    );
  }
}

Hopefully that helps somebody figure out something better. I might post a final solution later, but I might not because this has been a big waste of time good luck everybody else ❤

Gahh couldn't leave it.

  1. Made sure client and server know the locale in _app/_document.js
  2. Made a script to build a js file with all the locale strings that gets dynamically imported as a fallback
  3. Added a fallback for the locale script from FormatJs isn't defined
  4. Left the rest of the example code as-is

_app.js

import React from 'react';
import App from 'next/app';
import { createIntl, createIntlCache, RawIntlProvider } from 'react-intl';
import { getLocale } from '@lib/i18n';

// This is optional but highly recommended
// since it prevents memory leak
const cache = createIntlCache();

export default class $App extends App {

  static async getInitialProps({ Component, ctx }) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    // Get the `locale` and `messages` from the request object on the server.
    // In the browser, use the same values that the server serialized.
    const { req } = ctx;
    const { locale, messages } = req || window.__NEXT_DATA__.props;

    const props = { pageProps, locale, messages };
    if (!props.locale) {
      props.locale = getLocale(req);
    }

    if (!props.messages) {
      const strings = (await import('@lang/strings')).default;
      props.messages = strings[props.locale];
    }

    return props;
  }

  render() {
    const { Component, pageProps, locale, messages } = this.props;

    const intl = createIntl(
      {
        locale,
        messages
      },
      cache
    );

    return (
      <RawIntlProvider value={intl}>
        <Component {...pageProps} />
      </RawIntlProvider>
    );
  }
}

_document.js

import React from 'react';
import Document, { Head, Main, NextScript } from 'next/document';
import { getServerLocale } from '@lib/i18n';

// The document (which is SSR-only) needs to be customized to expose the locale
// data for the user's locale for React Intl to work in the browser.
export default class $Document extends Document {

  static async getInitialProps(context) {
    const pageProps = await super.getInitialProps(context);

    const { req } = context;

    const props = {
      ...pageProps,
      locale: req.locale,
      localeDataScript: req.localeDataScript
    };

    if (!props.locale) {
      props.locale = getServerLocale(req);
    }

    return props;
  }

  render() {
    // Polyfill Intl API for older browsers
    const polyfill = `https://cdn.polyfill.io/v3/polyfill.min.js?features=Intl.~locale.${this.props.locale}`;
    const fallbackLocaleDataScript = `https://cdn.jsdelivr.net/npm/@formatjs/intl-relativetimeformat/dist/locale-data/${this.props.locale}.js`;

    return (
      <html lang={this.props.locale}>
        <Head />
        <body>
          <Main />
          <script src={polyfill} />
          {this.props.localeDataScript ?
            <script
              dangerouslySetInnerHTML={{
                __html: this.props.localeDataScript
              }}
            />
              :
            <script src={fallbackLocaleDataScript} />}
          <NextScript />
        </body>
      </html>
    );
  }
}

scripts/generate-strings.js

const { readFileSync, writeFileSync } = require('fs');
const { basename, resolve } = require('path');
const glob = require('glob');

const languageFilenames = glob.sync('./lang/*.json');

// build a structure like
// {
//   "en": {
//     "id": "string"
//   },
//   "fr": {
//     "id": "string"
//   },
//   ...
// }
let data = {};
for (let filename of languageFilenames) {
  let locale = basename(filename, '.json');
  let file = readFileSync(filename, 'utf8');
  let strings = JSON.parse(file);
  data[locale] = strings;
}

let fileContents = `export default ${JSON.stringify(data)}`;

writeFileSync('./lang/strings.js', fileContents);
console.log(`> Wrote strings to: "${resolve('./lang/strings.js')}"`);

locale-utilities.js

// Combines the other two utility functions
export function getLocale(req, defaultLocale) {
  if (req) {
    return getServerLocale(req, defaultLocale);
  } else {
    return getBrowserLocale(defaultLocale);
  }
}

export function getServerLocale(req, defaultLocale) {
  const accepts = require('accepts');
  const accept = accepts(req);
  return accept.languages()[0] || defaultLocale || 'en';
}

export function getBrowserLocale(defaultLocale) {
  if (navigator.languages != undefined) {
    return navigator.languages[0] || defaultLocale || 'en';
  } else {
    return navigator.language || defaultLocale || 'en';
  }
}

package.json scripts

{
  "scripts": {
    "dev": "node --icu-data-dir=node_modules/full-icu server.js",
    "build": "next build && node ./scripts/default-lang && node ./scripts/generate-strings",
    "start": "NODE_ENV=production node --icu-data-dir=node_modules/full-icu server.js"
  },
  ...
}

🥔 🎉 ¯\_(ツ)_/¯

Hey @FreedCapybara, could you share a link to your repository?

Sure, here you go https://github.com/FreedCapybara/nextjs-template

Note that there's a bunch of other stuff in there, and some of the i18n stuff might be slightly different or rearranged from what I wrote above (same idea, though).

@FreedCapybara It's working on dev but not on prod when deployed to ZEIT now :/

@smakosh Poop, sorry -- fixed it 👍

Here's a deployment https://nextjs-template-bw7c2tmwk.now.sh/ which works/worked at the time of this writing. If you flip your browser language to French (or English) it should work properly after a reload. (Note that you may have to actually click the refresh button, even if Chrome had you relaunch the browser after switching languages)

tl;dr

  1. Loosened up the default-lang script to overwrite duplicate keys instead of throwing errors (which is good enough for me)
  2. Commented-out the line to load the user context in _app.js, since the template isn't hooked up to a backend API so it just causes an error

I did the same as well, thanks @FreedCapybara

@FreedCapybara but isn't your repo using a custom server? Why is it working with Zeit Now?

That's the trick, I guess -- I left all the server stuff the same as the example, with fallbacks in files like _app.js and _document.js, which do run in Now.

Some links:

  1. The dynamic strings import in _app.js https://github.com/FreedCapybara/nextjs-template/blob/master/pages/_app.js#L71
  2. Loading the locale data script from a CDN in _document.js https://github.com/FreedCapybara/nextjs-template/blob/master/pages/_document.js#L56

It's all a little bit unfortunate. I think it's as efficient as the original example when running server.js (it's supposed to be, but I'm only 70% confident about that), but the extra loading in a Now deployment might be a bit of a bear depending on how many strings you have. ¯_(ツ)_/¯

@BjoernRave that file is totally ignored by ZEIT now.

That's the trick, I guess -- I left all the server stuff the same as the example, with fallbacks in files like _app.js and _document.js, which do run in Now.

Some links:

  1. The dynamic strings import in _app.js https://github.com/FreedCapybara/nextjs-template/blob/master/pages/_app.js#L71
  2. Loading the locale data script from a CDN in _document.js https://github.com/FreedCapybara/nextjs-template/blob/master/pages/_document.js#L56

It's all a little bit unfortunate. I think it's as efficient as the original example when running server.js (it's supposed to be, but I'm only 70% confident about that), but the extra loading in a Now deployment might be a bit of a bear depending on how many strings you have. ¯_(ツ)_/¯

Have you tried converting the content within server.js to a serverless function?

@smakosh No, because I'm not that slick -- don't know how to make that work and it sounds like some hours of swearing for me. I'm good with where I'm at, but you could do it 👍

The goal was to have it working wherever I decided to deploy it. I'm liking Zeit Now because it's super easy ❤️ but I might change my mind if I find it isn't going to work for me -- so I left the option in there.

If you use my boilerplate you can always delete one approach or the other (deleting code is my favorite btw).

My team is working on an approach to serverless A/B testing we suspect will work for i18n. Not all of it is public as we're still working out the API, but here is a spike of our proposed surface area to Next.js: https://github.com/Sheertex/next.js/pull/1

High-level, we:

In our case, we also use a pre-build step to compute all possible A/B(/C...) combinations by inspecting the JSX AST for <Experiment> and <Variant> components, but I suspect this won't be necessary for i18n. We currently do this project-wide, but hope to tidy this up by inspecting per-page output before further transpilation.

Would something like this be helpful for y'alls?

Closing as OP has removed all content:
image

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Timer picture Timer  ·  90Comments

timneutkens picture timneutkens  ·  250Comments

nvartolomei picture nvartolomei  ·  78Comments

baldurh picture baldurh  ·  74Comments

robinvdvleuten picture robinvdvleuten  ·  74Comments