React-i18next: Lack of documentation

Created on 16 Sep 2018  路  19Comments  路  Source: i18next/react-i18next

Hi,

I'm struggling with i18n for several weeks now and I'm getting mad because I spend my whole time trying packages to finally discover that they don't do what I expect from an i18n package. So as I'm still deadlocked on this issue, I've taken the time to write what I'd expect from an i18n package :

  • It should have pretty urls with custom names and prefixes (/en/about, /fr/a-propos, and so forth) for nice SEO.
  • If no prefix is given, it would take the default language of the browser used and redirect to the good language.
  • If the person has already been on the website and has chosen, for instance, French, then the next time he comes on the website and goes to an url without prefix, he would be redirected to /fr/ no matter what his browser's language is.
  • In next.js, we should have an HOC that fetches both the _translations_ (.json, for instance) and the _current language_ according to the previous rules.
  • This HOC would supply a simple function t() that has two arguments : the _key_ and the _optional language_. By default, if no language is provided, it would get the current language. For instance: <p>{t('title')}</p> would echo the title in the provided json (that would look like {"title": "How are you?"}).
  • To create a link to a page, we would simply use <Link href="about"><a>t('go2AboutPage')</a></Link> where about is the name of the template in next.js (and not the route). Via a custom Link component, the name would be converted to the right route (/fr/a-propos or /en/about).
  • If we give the lang attribute to this Link, it would force the locale : <Link href="about" lang="fr"> would give /fr/a-propos.
  • We would finally need a _switch component_ that keeps the current page but change the locale. For instance <SwitchLang to="fr" />.

As I'm pretty new to next.js, I'm not totally aware of how client routing and server routing work. So there may be even more parameters to take into account in this listing, for instance the isomorphic routing or how the languages are fetched.

I'm aware that react-i18next already does the most of all these features, but as there is a lack of documentation on how to implement them (we only have examples, but no explanation), I only get errors with it.
The weirdest thing is that react-i18next doesn't natively implement prefixed routes!
How can you have a good SEO without different routes?
I mean, the only reason why I use next.js instead of React is for SEO.

So I tried other solutions:

But they all have their own lacks.
I'm still astonished to see that there is so few solutions for i18n with next.js, regarding how wonderful and practical next is.
I thought next was more used than that. Or are we pioneers?

Anyway, thanks for taking the time to read me :)

Most helpful comment

For anyone searching for the same config as me, here it is.

And here's the website built with this technique: https://hivency.com

Next.js, with JSON translations and custom routes

鈿狅笍 There is one JSON file per language. You'll need to change the config for a folder-per-language system.

My files tree:

  • components/

    • WhateverComponent/

    • index.js

    • Link

    • index.js

  • pages/

    • _document.js

    • index.js

    • pricing.js

  • lang/

    • fr.json

    • en.json

  • lib/

    • withI18next.js

  • i18n.js
  • languages.js
  • next.config.js
  • routes.js
  • server.js

Usage in components

components/WhateverComponent/index.js

import {translate} from 'react-i18next';
import Link from '../Link';

const Whatever = ({t, lang}) => (
  <div>
    <Link href="/pricing">
      <a>
        {t('global.whatever')}
      </a>
    </Link>
    <p>
      And the language is: {lang === 'fr' ? 'French' : 'English'}
    </p>
  </div>
);

export default translate()(Whatever);

components/Link/index.js

import {translate} from 'react-i18next';
import Link from 'next/link';
import * as routes from '../../routes';

const CustomLink = ({lng, href, anchor, children}) => {
  const route = routes.find(r => r.page === href) || {path: '/'};
  lng = lng.split('-')[0];
  return (
    <Link href={'/' + lng + route.path + (anchor ? '#' + anchor : '')}>
      {children}
    </Link>
  );
};

export default translate()(CustomLink);

Usage in pages

pages/_document.js

import {withI18next} from '../lib/withI18next';
import Document, {Head, Main, NextScript} from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <html lang={this.props.lng}>
        <Head>
          <link rel="stylesheet" href="/_next/static/style.css" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    );
  }
}

export default withI18next()(MyDocument);

pages/index.js

import {withI18next} from '../lib/withI18next';
import Whatever from '../components/Whatever';

const Homepage = ({t}) => (
  <main>
    <p>{t('home.title')}</p>
    <Whatever />
  </main>
);

export default withI18next()(Homepage);

pages/pricing.js

import {withI18next} from '../lib/withI18next';
import Whatever from '../components/Whatever';

const Pricing = ({t}) => (
  <main>
    <p>{t('pricing.title')}</p>
    <Whatever />
  </main>
);

export default withI18next()(Pricing);

JSON files

lang/fr.json

{
  "home": {
    "title": "Page d'accueil"
  },
  "pricing": {
    "title": "Prix"
  }
}

lang/en.json

{
  "home": {
    "title": "Home"
  },
  "pricing": {
    "title": "Pricing"
  }
}

HOC withI18next (only in pages)

lib/withI18next.js

import {translate} from 'react-i18next';
import i18n from '../i18n';

export const withI18next = () => ComposedComponent => {
  const Extended = translate([], {i18n, wait: process.browser})(
    ComposedComponent,
  );

  Extended.getInitialProps = async ctx => {
    const composedInitialProps = ComposedComponent.getInitialProps
      ? await ComposedComponent.getInitialProps(ctx)
      : {};
    const i18nInitialProps = ctx.req ? i18n.getInitialProps(ctx.req) : {};
    const hostname = ctx.req && ctx.req.headers.host;
    return {
      ...composedInitialProps,
      ...i18nInitialProps,
      hostname,
    };
  };

  return Extended;
};

Config

i18n.js

const i18n = require('i18next');
const XHR = require('i18next-xhr-backend');
const LanguageDetector = require('i18next-browser-languagedetector');
const path = require('path');
const langs = require('./languages');

const options = {
  whitelist: langs,
  fallbackLng: ['en', 'fr'],
  load: 'languageOnly',
  debug: false,
  saveMissing: false,
  backend: {
    loadPath: path.join(__dirname, '/lang/{{lng}}.json'),
  },
  interpolation: {
    escapeValue: false, // not needed for react!!
  },
  react: {
    wait: true,
  },
};

// for browser use xhr backend to load translations and browser lng detector
if (process.browser) {
  i18n.use(XHR)
    // .use(Cache)
    .use(LanguageDetector);
}

// initialize if not already initialized
if (!i18n.isInitialized) i18n.init(options);

// a simple helper to getInitialProps passed on loaded i18n data
i18n.getInitialProps = req => {
  req.i18n.toJSON = () => null; // do not serialize i18next instance and send to client

  const initialI18nStore = {};
  req.i18n.languages.forEach(l => {
    initialI18nStore[l] = req.i18n.services.resourceStore.data[l] || {};
  });

  return {
    i18n: req.i18n, // use the instance on req - fixed language on request (avoid issues in race conditions with lngs of different users)
    initialI18nStore,
    initialLanguage: req.i18n.language,
  };
};

module.exports = i18n;

languages.js

module.exports = ['fr', 'en', 'de', 'it', 'ru', 'es', 'pt', 'zh'];

routes.js

module.exports = [
  {
    path: '/', // path in the url
    page: '/index', // page used in the 'pages' folder
  },
  {
    path: '/pricing',
    page: '/pricing',
  },
];
/*
I've split path and page because sometimes you would like to have the page name in the language of your choice.  
For instance, you could name your 'pricing' page: 'prix.js'.  
In this case, it would be {path: '/pricing', page: '/prix'},  
and in your components: <Link href="/prix">...
*/

server.js

const express = require('express');
const path = require('path');
const next = require('next');
const compression = require('compression');
const i18n = require('./i18n');
const i18nextMiddleware = require('i18next-express-middleware');
const Backend = require('i18next-node-fs-backend');
const {parse} = require('url');
const routes = require('./routes');
const redirects = require('./redirects');

const dev = process.env.NODE_ENV !== 'production';
const PORT = process.env.PORT || 3000;
const app = next({dev});
const handle = app.getRequestHandler();
const langs = require('./languages');

// Express init
const server = express();
server.use(compression());
server.use('/lang', express.static(path.join(__dirname, '/lang')));

// i18next serverside
const i18nextOptions = {
  whitelist: langs,
  fallbackLng: ['en', 'fr'],
  load: 'languageOnly',
  preload: langs,
  backend: {
    loadPath: path.join(__dirname, '/lang/{{lng}}.json'),
  },
  detection: {
    order: ['path', 'session', 'querystring', 'cookie', 'header'],
  },
};

i18n.use(Backend)
  .use(i18nextMiddleware.LanguageDetector)
  .init(i18nextOptions, _ => {
    // init next.js
    app.prepare().then(() => {
      server.use(i18nextMiddleware.handle(i18n)); // what is this?

      redirects.forEach(({old, page}) => {
        server.get(old, (req, res) => {
          const url = req.params.lang
            ? '/' + req.params.lang + page
            : page;
          res.redirect(301, url);
        });
      });

      routes.forEach(({path, page}) => {
        i18nextMiddleware.addRoute(
          i18n,
          '/:lng' + path,
          langs,
          server,
          'get',
          (req, res) => app.render(req, res, page, {}),
        );
      });

      server.get('*', (req, res) => {
        const currentLang = req.i18n.languages[0];
        const route = routes.find(r => {
          return r.page === req.url || r.path === req.url;
        });
        if (route) {
          res.redirect('/' + currentLang + route.path);
        }
        handle(req, res);
      });
      server.listen(PORT, err => {
        if (err) throw err;
        console.log(`> Ready on http://localhost:${PORT}`);
      });
    });
  });

Ok, it's not very clean but it was very painful to get to this point.

And concerning the client-side routing issue I encountered, it has nothing to see with i18n but with custom routing.
Here's the link for those who can't have client-side routing with next: https://github.com/zeit/next.js/issues/2833#issuecomment-414919347

All 19 comments

Hi @nagman the problem here is:

next.js is a framework to do serverside rendering of react apps - it does this well - but it does not care for multilanguage sites out of the box (ah...blame the americans for this - the always forget that beside US multilingual stuff is regulary used ;) )

react-i18next is a i18n framework for react, doing so it cares (not like other i18n frameworks!!!!) to get the current language from the location.href path, it cares for persisting selected languages using either cookies or localstorage, ... it cares for using a default language if non was discovered by route, takes care it also works on serverside, without loading full translations for every request, while keeping requests save from interfering each others influence on set language (async nature of a requests).

The i18next express middleware is also capable of creating seo friendly routes like: https://github.com/i18next/i18next-express-middleware#add-localized-routes (that is used here in the react-i18next sample for next.js): https://github.com/i18next/react-i18next/blob/master/example/nextjs_withAppJS/server.js#L9

But react-i18next does not have any specifics for knowing your routing framework or if your using next.js or are on electron, reactnative, expo.js

So while i understand your frustrations - i hope you also understand that i got not the time to glue all this together. I would say the parts from i18next are complete to get it working - but i personally have not the time to look into every framework to cover everycase - i already invested a lot of time into the next.js sample - so i hope the community (next.js users knowing more about the internals) can cover the last bits to make the SEO routes happen (honestly that should be covered by the next.js team - but this is just my opinion).

important note for docs...non react specific stuff is on https://i18next.com which covers setting options on i18n.init or options for translations (plural, ...).

WOw. That is the fastest and most complete answer I've ever seen.
First a big thank. I totally understand you can't spend time on every case.

I'll look at these links you provided. When I resolve my issue, I'll post it in this thread!

PS: Yeah you're right, those americans...

Ok. I'm back after a few days. Things have a lot changed and I'm close to finish with i18n.
BUT! I can't fetch the current lang inside my components, so I can't make a language switch (neither can I do proper Links that work both client and server side).

Where can I find the current locale (for instance 'fr') BOTH client and server side?
AND inside components as well as pages :)

on i18n.languages[0] or req.i18next.languages[0] or newly every translate HOC passes lng into the component as a prop.

Yet the lng property of the HOC returns undefined on server side. This is why I'm asking. And the i18n.languages[0] or req.i18next.languages[0] can only be fetched from a page.

https://github.com/i18next/react-i18next/blob/master/src/I18n.js#L127

hm...takes language from the i18n object that gets passed to it...which in this case is not the one from the request: https://github.com/i18next/react-i18next/blob/master/example/nextjs_withAppJS/lib/withI18next.js#L5 but just the global instance which does not get lng set to avoid async request mismatch.

but isn't there the initialLanguage that gets set in this withI18next.js?!?: https://github.com/i18next/react-i18next/blob/master/example/nextjs_withAppJS/i18n.js#L56

oh man...all this SSR is a lot complexer than it should be

big respect to you for going through all this.

Yeah, thanks for the encouragement.

As the withI18next HOC adds a getInitialProps to the components passed to it, it will add the current lang taken from the request.
But getInitialProps works only on pages. So for now the only solution I've found is to get the current language from the page and pass it down to every child that needs to know the current language of the request.

Not the best solution, I know.

And concerning the initialLanguage, it is also set in the getInitialProps method of the i18n instance.

So now I can fetch the current lang from the server on pages but not on components... HELP!!!

Maybe it is somewhere in a context? I don't know! This is not documented!

nope, currently context only passes i18n instance and t function...but guess we will improve that situation on the next major version where we start using the new context API of react

Here's where I'm stuck:

import {withI18next} from '../../lib/withI18next';
import Link from 'next/link';

const CustomLink = props => {
    console.log(props);
    return <Link href={'/' + props.lng + props.href}>{props.children}</Link>;
};

export default withI18next()(CustomLink);

On the client side, the console.log works great:

lng: "fr"

But on the server console, I have lng: undefined (last line).
The full log:

{ tReady: false,
  href: '/notre-expertise',
  children:
   { '$$typeof': Symbol(react.element),
     type: 'a',
     key: null,
     ref: null,
     props: { className: '', title: undefined, children: 'Formation' },
     _owner: null,
     _store: {} },
  i18n:
   I18n {
     observers: {},
     options:
      { debug: false,
        initImmediate: true,
        ns: [Array],
        defaultNS: [Array],
        fallbackLng: [Array],
        fallbackNS: false,
        whitelist: [Array],
        nonExplicitWhitelist: false,
        load: 'languageOnly',
        preload: [Array],
        simplifyPluralSuffix: true,
        keySeparator: '.',
        nsSeparator: ':',
        pluralSeparator: '_',
        contextSeparator: '_',
        saveMissing: false,
        updateMissing: false,
        saveMissingTo: 'fallback',
        saveMissingPlurals: true,
        missingKeyHandler: false,
        missingInterpolationHandler: false,
        postProcess: false,
        returnNull: true,
        returnEmptyString: true,
        returnObjects: false,
        joinArrays: false,
        returnedObjectHandler: [Function: returnedObjectHandler],
        parseMissingKeyHandler: false,
        appendNamespaceToMissingKey: false,
        appendNamespaceToCIMode: false,
        overloadTranslationOptionHandler: [Function: handle],
        interpolation: [Object],
        backend: [Object],
        detection: [Object] },
     services:
      { logger: [Object],
        resourceStore: [Object],
        languageUtils: [Object],
        pluralResolver: [Object],
        interpolator: [Object],
        backendConnector: [Object],
        languageDetector: [Object] },
     logger:
      Logger {
        prefix: 'i18next:',
        logger: [Object],
        options: [Object],
        debug: false },
     modules: { external: [], backend: [Object], languageDetector: [Object] },
     format: undefined,
     store: ResourceStore { observers: {}, data: [Object], options: [Object] },
     translator:
      Translator {
        observers: [Object],
        resourceStore: [Object],
        languageUtils: [Object],
        pluralResolver: [Object],
        interpolator: [Object],
        backendConnector: [Object],
        options: [Object],
        logger: [Object] },
     getResource: [Function],
     addResource: [Function],
     addResources: [Function],
     addResourceBundle: [Function],
     removeResourceBundle: [Function],
     hasResourceBundle: [Function],
     getResourceBundle: [Function],
     getDataByLanguage: [Function],
     getInitialProps: [Function],
     isInitialized: true },
  t: { [Function: fixedT] lngs: null, ns: undefined },
  lng: undefined }

It is important to note that CustomLink is a __component__ and not a __page__.

So I could keep things like that but my client and my server wouldn't be synchronized because in the first case I would have /fr/page-name and in the second case /undefined/page-name.

guessing that I18n instance is the global one...not the one from the request.

In CustomLink do you use withI18next or the normal translateHOC coming with react-i18next? Change to using the regular HOC might help...but not sure.

I tried:
import {translate} from 'react-i18next';
export default translate()(CustomLink);
but I got exactly the same result.

Woops, I unintentionally closed the issue.

I just figured the solution!

It is not specified in the docs that you need to use withI18next for the pages, and translate for the components!

__And now it works!__

When I tried yesterday, it wasn't working certainly because I was using withi18next on one of the parents of the component.

But now that I changed it in all the components, it works like a charm :)

Strangely enough, the client routing doesn't work anymore. It is replaced by server routing.
But I don't care because I've spent more than 30 hours on this, so f* up the client routing!
This is a bonus that's not needed for my immediate purposes.

But this react-i18next example needs improvment and __documentation__.
I'll do a pull request when I'm done with my website.

Awesome...thanks for putting all the time into this. Looking forward to your contributions to sample and docs 馃憤

For anyone searching for the same config as me, here it is.

And here's the website built with this technique: https://hivency.com

Next.js, with JSON translations and custom routes

鈿狅笍 There is one JSON file per language. You'll need to change the config for a folder-per-language system.

My files tree:

  • components/

    • WhateverComponent/

    • index.js

    • Link

    • index.js

  • pages/

    • _document.js

    • index.js

    • pricing.js

  • lang/

    • fr.json

    • en.json

  • lib/

    • withI18next.js

  • i18n.js
  • languages.js
  • next.config.js
  • routes.js
  • server.js

Usage in components

components/WhateverComponent/index.js

import {translate} from 'react-i18next';
import Link from '../Link';

const Whatever = ({t, lang}) => (
  <div>
    <Link href="/pricing">
      <a>
        {t('global.whatever')}
      </a>
    </Link>
    <p>
      And the language is: {lang === 'fr' ? 'French' : 'English'}
    </p>
  </div>
);

export default translate()(Whatever);

components/Link/index.js

import {translate} from 'react-i18next';
import Link from 'next/link';
import * as routes from '../../routes';

const CustomLink = ({lng, href, anchor, children}) => {
  const route = routes.find(r => r.page === href) || {path: '/'};
  lng = lng.split('-')[0];
  return (
    <Link href={'/' + lng + route.path + (anchor ? '#' + anchor : '')}>
      {children}
    </Link>
  );
};

export default translate()(CustomLink);

Usage in pages

pages/_document.js

import {withI18next} from '../lib/withI18next';
import Document, {Head, Main, NextScript} from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <html lang={this.props.lng}>
        <Head>
          <link rel="stylesheet" href="/_next/static/style.css" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    );
  }
}

export default withI18next()(MyDocument);

pages/index.js

import {withI18next} from '../lib/withI18next';
import Whatever from '../components/Whatever';

const Homepage = ({t}) => (
  <main>
    <p>{t('home.title')}</p>
    <Whatever />
  </main>
);

export default withI18next()(Homepage);

pages/pricing.js

import {withI18next} from '../lib/withI18next';
import Whatever from '../components/Whatever';

const Pricing = ({t}) => (
  <main>
    <p>{t('pricing.title')}</p>
    <Whatever />
  </main>
);

export default withI18next()(Pricing);

JSON files

lang/fr.json

{
  "home": {
    "title": "Page d'accueil"
  },
  "pricing": {
    "title": "Prix"
  }
}

lang/en.json

{
  "home": {
    "title": "Home"
  },
  "pricing": {
    "title": "Pricing"
  }
}

HOC withI18next (only in pages)

lib/withI18next.js

import {translate} from 'react-i18next';
import i18n from '../i18n';

export const withI18next = () => ComposedComponent => {
  const Extended = translate([], {i18n, wait: process.browser})(
    ComposedComponent,
  );

  Extended.getInitialProps = async ctx => {
    const composedInitialProps = ComposedComponent.getInitialProps
      ? await ComposedComponent.getInitialProps(ctx)
      : {};
    const i18nInitialProps = ctx.req ? i18n.getInitialProps(ctx.req) : {};
    const hostname = ctx.req && ctx.req.headers.host;
    return {
      ...composedInitialProps,
      ...i18nInitialProps,
      hostname,
    };
  };

  return Extended;
};

Config

i18n.js

const i18n = require('i18next');
const XHR = require('i18next-xhr-backend');
const LanguageDetector = require('i18next-browser-languagedetector');
const path = require('path');
const langs = require('./languages');

const options = {
  whitelist: langs,
  fallbackLng: ['en', 'fr'],
  load: 'languageOnly',
  debug: false,
  saveMissing: false,
  backend: {
    loadPath: path.join(__dirname, '/lang/{{lng}}.json'),
  },
  interpolation: {
    escapeValue: false, // not needed for react!!
  },
  react: {
    wait: true,
  },
};

// for browser use xhr backend to load translations and browser lng detector
if (process.browser) {
  i18n.use(XHR)
    // .use(Cache)
    .use(LanguageDetector);
}

// initialize if not already initialized
if (!i18n.isInitialized) i18n.init(options);

// a simple helper to getInitialProps passed on loaded i18n data
i18n.getInitialProps = req => {
  req.i18n.toJSON = () => null; // do not serialize i18next instance and send to client

  const initialI18nStore = {};
  req.i18n.languages.forEach(l => {
    initialI18nStore[l] = req.i18n.services.resourceStore.data[l] || {};
  });

  return {
    i18n: req.i18n, // use the instance on req - fixed language on request (avoid issues in race conditions with lngs of different users)
    initialI18nStore,
    initialLanguage: req.i18n.language,
  };
};

module.exports = i18n;

languages.js

module.exports = ['fr', 'en', 'de', 'it', 'ru', 'es', 'pt', 'zh'];

routes.js

module.exports = [
  {
    path: '/', // path in the url
    page: '/index', // page used in the 'pages' folder
  },
  {
    path: '/pricing',
    page: '/pricing',
  },
];
/*
I've split path and page because sometimes you would like to have the page name in the language of your choice.  
For instance, you could name your 'pricing' page: 'prix.js'.  
In this case, it would be {path: '/pricing', page: '/prix'},  
and in your components: <Link href="/prix">...
*/

server.js

const express = require('express');
const path = require('path');
const next = require('next');
const compression = require('compression');
const i18n = require('./i18n');
const i18nextMiddleware = require('i18next-express-middleware');
const Backend = require('i18next-node-fs-backend');
const {parse} = require('url');
const routes = require('./routes');
const redirects = require('./redirects');

const dev = process.env.NODE_ENV !== 'production';
const PORT = process.env.PORT || 3000;
const app = next({dev});
const handle = app.getRequestHandler();
const langs = require('./languages');

// Express init
const server = express();
server.use(compression());
server.use('/lang', express.static(path.join(__dirname, '/lang')));

// i18next serverside
const i18nextOptions = {
  whitelist: langs,
  fallbackLng: ['en', 'fr'],
  load: 'languageOnly',
  preload: langs,
  backend: {
    loadPath: path.join(__dirname, '/lang/{{lng}}.json'),
  },
  detection: {
    order: ['path', 'session', 'querystring', 'cookie', 'header'],
  },
};

i18n.use(Backend)
  .use(i18nextMiddleware.LanguageDetector)
  .init(i18nextOptions, _ => {
    // init next.js
    app.prepare().then(() => {
      server.use(i18nextMiddleware.handle(i18n)); // what is this?

      redirects.forEach(({old, page}) => {
        server.get(old, (req, res) => {
          const url = req.params.lang
            ? '/' + req.params.lang + page
            : page;
          res.redirect(301, url);
        });
      });

      routes.forEach(({path, page}) => {
        i18nextMiddleware.addRoute(
          i18n,
          '/:lng' + path,
          langs,
          server,
          'get',
          (req, res) => app.render(req, res, page, {}),
        );
      });

      server.get('*', (req, res) => {
        const currentLang = req.i18n.languages[0];
        const route = routes.find(r => {
          return r.page === req.url || r.path === req.url;
        });
        if (route) {
          res.redirect('/' + currentLang + route.path);
        }
        handle(req, res);
      });
      server.listen(PORT, err => {
        if (err) throw err;
        console.log(`> Ready on http://localhost:${PORT}`);
      });
    });
  });

Ok, it's not very clean but it was very painful to get to this point.

And concerning the client-side routing issue I encountered, it has nothing to see with i18n but with custom routing.
Here's the link for those who can't have client-side routing with next: https://github.com/zeit/next.js/issues/2833#issuecomment-414919347

Hi @nagman, did you see our new example?

Also, if you're interested in a more hands-off next-i18next package which would allow easy installation into any NextJs project, you can feel free to help us define the core requirements here.

Yes, I've seen the example, but it doesn't provide any solution for custom routes, so it is totally unusable because we use NextJS for SEO, and routes like /?lng=de are horrible for SEO.

However, this next-i18next package seems a lot more interesting!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

oyeanuj picture oyeanuj  路  3Comments

flq picture flq  路  4Comments

Flo-Slv picture Flo-Slv  路  4Comments

a-barbieri picture a-barbieri  路  4Comments

tankpower1 picture tankpower1  路  3Comments