Closed martpie closed 3 years ago
Yep, have been waiting for it to land. We'll want to rewrite so that the main concern of next-i18next
is handling data management for SSR apps.
Would be very happy to have input from collaborators!
@isaachinman nice! Could you elaborate a bit on what you mean by data management in this context?
So, the NextJs core as of v10 intends to take all responsibility for locale routing. That means we can remove a lot of functionality from next-i18next
.
What NextJs v10 does not do, is handle any sort of actual data management for users.
In the case of full SSG apps, you can, in theory, get away with passing your localisation data as props per page, kind of like this example. However, that means you'll be sending all of your translation data down to every single page.
If i18n routing is being taken care of by NextJs internally, all that's left for a package like next-i18next
to do is handle the management of actual translation content. This is basically a harmony/synchronisation of i18next backend packages, and NextJs opinionated patterns. For example, we'll still need to hook into NextJs Router
events and load in necessary namespaces on route change, etc.
Very exciting release specifically in regards to internationalization. I am looking forward to be able to use SSG or at least gerServerSideProps
to be able to load namespacesRequired and to abandon getInitialProps
. However the features I see provided by Vercal at this time are language detection an an early intl libs support.
Few things are still not clear to me:
Basically:
If i18n routing is being taken care of by NextJs internally, all that's left for a package like
next-i18next
to do is handle the management of actual translation content. This is basically a harmony/synchronisation of i18next backend packages, and NextJs opinionated patterns. For example, we'll still need to hook into NextJsRouter
events and load in necessary namespaces on route change, etc.
Does NextJS handle localization files? If it is using react-i18n
and i18n-next
it is still possible to use their hook to handle switching languages.
NextJs only handles the routing. Handling localisation files, splitting translations across pages, etc, will be the new goal of packages like next-i18next
.
I hope to spend some hours this weekend taking a deep look at a rewrite for next-i18next
to simplify things dramatically.
After upgrade to next10, i got react hooks issue that the context of Next18n is rendering more hooks than before, it doesn't always happen but sometimes i got this annoying error saying the context of next-i18next is rendering more or fewer hooks which is kinda odd to debug Anyone experiencing the same? @isaachinman any idea of what's going on or what might be causing that?
@isaachinman Lmk if I can help. I've done some things to load translations specified with namespaces, with SSG.
https://github.com/MathiasKandelborg/TRUE-framework/tree/master/util/i18n
I have time to contribute so I might take a look myself. Would love to contribute, so let me know if there's anything I can begin with.
@MathiasKandelborg I am starting to look into this now, but unfortunately only have about an hour today.
I will write my thoughts below, to give any potential contributors a chance to work on this without my schedule blocking development. I understand that this is time-sensitive in a way, and would greatly appreciate help anyone might be able to offer.
In general, the future version of next-i18next
should be a dramatically simpler codebase to maintain and reason about.
It should be said that NextJs's i18n routing implementation has less features than next-i18next
's current implementation. Whether or not that is a bad thing is entirely subjective, it's just down to certain decisions the Vercel team has made. For example, if people want to enforce locale subpaths, they will need to write their own redirects
in next.config.js
, etc.
As I wrote above, the main purpose of next-i18next
moving forward will be the handling of namespaces, i18next-fs-backend
, I18nextProvider
, and so on.
The next-i18next
package will just export functions now, we don't have a need for any internal state, class construction, any routing logic, any i18next middlewares, etc. The _app
HOC should take the config, like so:
_app
:import { appWithTranslation } from 'next-i18next/dist/commonjs/hocs/app-with-translation'
import path from 'path'
const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />
export default appWithTranslation(MyApp, {
defaultLanguage: 'en',
otherLanguages: ['de'],
localePath: path.resolve('./public/static/locales')
})
Individual pages should continue passing namespacesRequired
up to the HOC via props, but can do so via either getStaticProps
or getServerSideProps
– either way works agnostically to next-i18next
, as it just comes up as WrappedComponent.props
:
const Homepage = ({ t }) => (
<Link href='/second-page'>
<button
type='button'
>
{t('to-second-page')}
</button>
</Link>
)
export const getStaticProps = async () => ({
props: {
namespacesRequired: ['common', 'footer'],
}
})
export default withTranslation('common')(Homepage)
The app-with-translation
HOC itself can now just consume the locale via router.locale
.
export const appWithTranslation = (WrappedComponent, userConfig) => {
const WrappedComponentWithSSR = withSSR()(WrappedComponent)
const config = createConfig(userConfig)
const { initPromise, i18n } = createClient(config)
const AppWithTranslation = (props) => {
const router = useRouter()
i18n.changeLanguage(router.locale)
return (
<I18nextProvider
i18n={i18n}
>
<WrappedComponentWithSSR
{...props}
/>
</I18nextProvider>
)
}
return hoistNonReactStatics(
AppWithTranslation, WrappedComponent,
)
}
Things I haven't figured out yet:
getStaticProps
/getServerSideProps
, to be able to await the promise? Probably so...i18next-http-middleware
. Just from an initial look, it seems like that package was doing a lot of work for us for free, in terms of configurationThat's it, really. I don't think it should be more than a few days of work to strip next-i18next
down to a much slimmer, simpler package that is agnostic of routing.
I would greatly appreciate the help of anyone who wants to work on this. If anyone wants to dedicate a good chunk of time, let me know and I will consider adding people as contributors, so that we can collaborate on a next-v10
branch, or something similar.
I'd also greatly appreciate advice and feedback from @adrai and @jamuhl, as it looks like the future of next-i18next
is basically just a thin i18next wrapper on top of NextJs.
Why a HOC? Wouldn't it better to use hooks instead?
@SalahAdDin What about react legacy apps? If it goes only for hooks, they lose support because they have no functional approach as such
Apps with React 16.8.0+ support hooks, which means that any project using React 16.0.0+ (Sep 2017) can upgrade without any breaking changes.
React 16.8.0 is a minimum requirement for next-i18next
already:
@kachkaev Okay then it would make sense
@kachkaev but still many have not migrated class components to functional components. next-i18next currently supports useTranslation
and withTranslation
already so you can choose between HOC or hook usage (but maybe its not documented).
If you mean the appWithTranslation
HOC: afaik hooks cannot insert a provider block around your app
@jansepke but useTranslation does not work, i have tried and it doesn't, so you are required to receive the TFunction. Its weird because i18 should have the loaded translations but if you try to useTranslation, the translations are empty (Right now with my hooks app i'm forced to use the T everywhere with HoC because next-i18next does not inject correctly)
@jansepke hooks are not for component wrapping. But it is still just a function. You could use any function to be a factory. However we would not use hooks in the react sense anymore. Thus we use Higher Order Components(=Factory Functions)
. However if you really need to you can return a Component from a hook and thus wrap it within a hook. For next.js => SSR does not render hooks. They are executed on the client side (see useEffect eg).
@underfisk I use useTranslation
application wide. Your configuration seems to be falsy.
Anyhow, better to open an issue and not communicate it under this topic.
@jansepke mas useTranslation não funciona, eu tentei e não funciona, então você deve receber a função TFunction. É estranho porque o i18 deve ter as traduções carregadas, mas se você tentar usar a tradução, as traduções estão vazias (agora, com meu aplicativo de ganchos, sou forçado a usar o T em todos os lugares com HoC porque next-i18next não injeta corretamente)
I use this hook and works perfectly
@mercteil Not really, i did add the HoC, context and the props but i'm using SSG, that might be the problem, not using SSR here
We'll be using a HOC, let's keep discussion in this issue on topic.
@isaachinman withTranslation hook only works if we use the custom of Next18 right? I though using i18n useTranslation would read the under lying context. Also in documentation its missing how you can skip translations in a specific page, right now i get warnings all the time even in pages i don't want locales, is there are way we can surpress locales at specific pages?
How to await the i18next init promise
What about throwing a promise in one of the HOCs and thus leveraging the Suspense mechanism? 🤔
@kachkaev That's an experimental feature, and not really relevant what we want to do. The moments where we will need to await the init promise are either during SSG or SSR. A fully initialised i18next instance is serialised and sent down to the client per req, so there is no need to await in a client side setting.
This looks promising! :tada: Let's get some things going! :partying_face:
The project's new scope is "simply" integrating i18next with Next.js, right?
Would it be fine for a first PR to try and cut as much as possible, get all the way down to the bone, and then get started with Next.js v10 and i18next?
I think a project or a milestone (on this Github repo) with some issues etc. would help organize things a bit better. It might just be me liking to organize things a bit too much, though.
There are several approaches to be made in regards to configuration and file loading. I'll spend some time today looking at different ways to approach the integration.
I found an approach - i.e., the files I've linked to before - to include the translated files on build/compile time for SSG pages using a Next.js lifecycle (haven't tested others, but the concept should be the same). This approach loads the relevant namespace files before any requests are made, so the data is included in the initial request. If it's possible to use the aforementioned method with i18next and all the different rendering approaches, that will probably be superior to any other method I can think of. I'm not sure about it, though. I'm just putting some thoughts out there.
BTW, sorry for the late response!
I've had a hectic week so far; I'm going to spend at least a few hours a day trying to solve this. I can't really progress my own project as I want to unless I use a library like this, so it's getting personal now! :grin:
Hey @MathiasKandelborg.
Would it be fine for a first PR to try and cut as much as possible, get all the way down to the bone, and then get started with Next.js v10 and i18next?
Not sure that's necessary – if you want to focus on something, let's begin with the core functionality. I have the context necessary to quickly clean up the repo once the core work is done.
I found an approach - i.e., the files I've linked to before - to include the translated files on build/compile time for SSG pages using a Next.js lifecycle
Can you explain what you mean? Loading translation files is something that next-i18next
has already solved, and I am not anticipating any changes will be needed in regards to that.
Also, I will have at least a few hours to look into this tomorrow (8 Nov), so let me know if you make any progress in the meantime.
Pretty crucial problem: _app
does not support getStaticProps
or getServerSideProps
. I'm unsure how to write a HOC that adds an i18n instance, as we need to init it during SSR/SSG, and then serialise it via pageProps
down to the client. Using getInitialProps
seems like a step backward, as these are static data requirements.
Attempting to override, or compose upon getStaticProps
or getServerSideProps
manually would be tricky or impossible, as I'm not sure we can access pathname
.
Would be grateful for any suggestions.
What if getXyzProps
returns the actual translations and not paths / namespaces? Roughly speaking,
export const getServerSideProps = async () => { // or getStaticProps
return {
props: {
translationLookup: loadI18nextTranslations(["myPage", "whatever"]), // never executed on the client
},
};
};
const MyPage = ({ translationLookup }) => {
return (
<NextI18nextProvider translationLookup={translationLookup}>
pageContents
</NextI18nextProvider>
);
};
export default MyPage;
The problem with this though is that if every page has loadI18nextTranslations(..., "common")
, then common translations would be loaded on every transition to another page. Perhaps, they could be some special trick for those via _document.tsx
? What I mean is preloading common namespaces there and thus not having to include them into the pages.
An alternative solution would involve an API endpoint and Suspence inside some sort of <NextI18nextProvider namespaces=["foo", "bar"]/>
. If the instance of i18next inside the NextI18nextProvider
does not have the expected locales loaded, it throws a promise, which resolves after the API request has been fulfilled. What I mean here is calling something like /api/next-i18next-translations?locale=ab-CD&namespaces=commons,myPage
.
In addition, each use of a not-yet-loaded namespace via t
/ Trans
can also trigger Suspense, but show a warning. The warning will help the devs predefine the locales they expect to be used at the top of each page and thus make the app faster. Still, because of how Suspense works, the app will be functional even if a requested namespace is not yet loaded in the middle of a page.
So am I understanding it correctly that this will not allow SSG for dynamic pages without getStaticPaths
, because it forces you to export a getStaticProps
function?
What about a scenario where I have a dynamic route with unknown paths, such as products/[productId]
? I would want this page to be
getStaticPaths
(unknown paths) and NextJS requires that@jan-wilhelm
So am I understanding it correctly that this will not allow SSG for dynamic pages without
getStaticPaths
, because it forces you to export agetStaticProps
function?
If the approach taken is the one suggested by @kachkaev here (return the translations as props of getXyzProps
) I think it will work the same for getStaticPaths
as you also have to return the props for every dynamic page you want to generate.
I am not using next-i18next
in my project and have been using this approach, and it works well, it is not as clean as a HOC but it works.
I currently have a server i18n
instance that has all namespaces and languages loaded (maybe not too efficient) that is used to get the needed translations for each page in getStaticProps
for example, and a fresh i18n
instance created in _app
that gets loaded with the translations from props
during build (os server rendering if using getServerSideProps
) and provided to the react-i18next
provider.
This second one can be made into a HOC for easy use with any custom _app
as it only needs to grab translations from props, wrap the whole thing with the provider and pass the context along for the actual App
. Could even be made into a hook for those that want more control over where the provider lies within the structure.
Pretty crucial problem:
_app
does not supportgetStaticProps
orgetServerSideProps
. I'm unsure how to write a HOC that adds an i18n instance, as we need to init it during SSR/SSG, and then serialise it viapageProps
down to the client. UsinggetInitialProps
seems like a step backward, as these are static data requirements.Attempting to override, or compose upon
getStaticProps
orgetServerSideProps
manually would be tricky or impossible, as I'm not sure we can accesspathname
.Would be grateful for any suggestions.
It's possible to pass additional props to the page in _document.js
by customizing the renderPage
function.
https://nextjs.org/docs/advanced-features/custom-document#customizing-renderpage
Could we pass the instance that way ?
What if getXyzProps returns the actual translations and not paths / namespaces?
Btw, that's the option next-translate
provides for loading translations https://github.com/vinissimus/next-translate/blob/master/examples/without-build-step/pages/index.js#L27
Perhaps, they could be some special trick for those via _document.tsx?
AFAIK _document.tsx
is actual only for rendering the whole page at the server-side, it won't work during page transitions within the application.
Pretty crucial problem: _app does not support getStaticProps or getServerSideProps. I'm unsure how to write a HOC that adds an i18n instance, as we need to init it during SSR/SSG, and then serialise it via pageProps down to the client. Using getInitialProps seems like a step backward, as these are static data requirements.
Another problem is that getInitialProps
can be used to serialize i18n client, but getServerSideProps
cannot, due to stricter serialization logic.
After playing w/ combination of next-i18next
and next-translate
approaches, I've collected some thoughts that might be useful:
initialI18nStore
and initialLanguage
(https://react.i18next.com/latest/ssr#passing-initial-translations-initial-language-down-to-client) in server-side methods,_app.js
at the server-side, and use singleton at the client-side (but be aware of https://github.com/i18next/react-i18next/blob/master/src/useSSR.js#L11),there is an option to use a high-order function to reduce the boilerplate of loading translations in server-side methods, e.g.
export const getServerSideProps = withTransations(["myPage", "whatever"])(async ctx => { ... });
Quick update: I have spent a few hours working on this today, and have a solution nearly ready. I'll post an update here shortly with a demo branch.
OK – the core idea can be found on the next-v10 branch.
I was able to delete the majority of next-i18next
's source code, and the core functionality now just depends on appWithTranslation
, and a serverSideTranslations
function.
Here's the idea:
next-i18next.config.js
file (at project root)const path = require('path')
module.exports = {
defaultLanguage: 'en',
otherLanguages: ['de'],
localePath: path.resolve('./public/static/locales')
}
pages/_app
:import { appWithTranslation } from 'next-i18next'
const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />
export default appWithTranslation(MyApp)
pages/index
:import { withTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import Header from '../components/Header'
const Homepage = ({ t }) => (
<>
<Header title={t('h1')} />
</>
)
export const getStaticProps = async ({ locale }) => ({
props: {
...await serverSideTranslations(locale, ['common', 'footer']),
}
})
export default withTranslation('common')(Homepage)
And that's it! For those curious about internal implementation, the two important files to check are:
I believe that this serverSideTranslations
approach will work with any of the NextJs data fetching methods, and my brief bit of local testing seems to indicate that. I believe that in doing this, we can actually side-step the issue of performing client-side translation loading, as those data dependencies will already be compiled to JSON via SSG, or will be computed server-side via SSR.
I would love if people can check out the next-v10
branch, and try to find any potential issues with this approach.
Once we come to a general consensus on this approach, next steps will be:
next-i18next@nextv10-beta
Any reason to have a i18next.config.js
when most of these variables are already available on next.config.js
?
@martpie I had considered that, but in the end, two things stopped me:
nextConfigToI18nextConfig
helper. But, for all uses cases outside of the most basic, people are going to unfortunately need to maintain two config sets regardlessnext.config.js
, because I'm not sure the Vercel team would be happy with that, and they sanitise/validate the object, if I recall correctlyEdit: On second thought, it might make sense to write a helper func that goes the other way, ie i18NextConfigToNextJsConfig
. Then people can at least import their next-i18next.config.js
file and not have config duplication. I do believe NextJs's i18n config options are a subset of next-i18next
's.
@isaachinman I think sperate config files were a good call 👍 In the case of the project I am currently working on, inside next.config.js
we use locales together with country code (nl-NL
, nl-BE
, fr-FR
, fr-BE
etc.), this allows us to serve different offer base on the country selected and display content in the right language. However, we are maintaining only one translation for Frech (fr.json
) or Dutch (nl.json
)
@TheFullResolution That's a separate point, and one I hadn't considered yet: how will locale fallbacks work, with this new NextJs v10 setup?
You can see that I am naively setting the language via router.locale
here. But probably, we will need to pass router.locale
into some i18next
detector, to get the correct fallback sequence. At the moment, it looks like if I pass locales: ['en-US', 'de-DE']
into NextJs, and locales: ['en', 'de']
into i18next, the implementation breaks.
If @adrai or @jamuhl could give some advice on that, I'd appreciate it! Not sure if that kind of fallback behaviour is already built in. It appears that there is no fallback sequence when setting lng: initialLocale
on i18next client init.
@isaachinman fallback works independently of using a language detector or directly setting the lng on i18next.init
per default the fallback will be lng-Country
-> lng
-> options.fallback
setting load options to and assuming having a lng-Country
passed in options or via detection:
'languageOnly': lng
-> options.fallback
'currentOnly': lng-Country
-> options.fallback
So as i18next does not have an options locales: ['en', 'de']
my guess this relates to preload options? idk?
Edit: load options resolution
@jamuhl Well, the fallback doesn't seem to be working in this case, for some reason. The i18next.options
object can be seen here. You can see that we have:
"supportedLngs": [
"de",
"en",
"cimode"
],
But also:
"lng": "en-US",
Any ideas?
guess it's because en-US
is not in the list of supported languages: setting "nonExplicitSupportedLngs": true
might work
@jamuhl Adding both nonExplicitSupportedLngs
and nonExplicitWhitelist
as true
to the config didn't fix the problem.
you got the https://gist.github.com/isaachinman/62b3642b591367b8951ab159a77153f8#file-next-v10-i18next-locale-fallback-L24 load option set to currentOnly
which leads to:
'currentOnly': lng-Country -> options.fallback
@jamuhl This config isn't working for en-US
either:
"defaultNS": "common",
"fallbackLng": false,
"fallbackNS": false,
"whitelist": [
"de",
"en",
"cimode"
],
"nonExplicitWhitelist": true,
"supportedLngs": [
"de",
"en",
"cimode"
],
"nonExplicitSupportedLngs": true,
"load": "all",
"preload": [
"de",
"en"
],
@isaachinman what is not working? I assume with above config you get i18next.languages = ['en-US', 'en'] which will on serverside miss en-US resources as those were not preloaded
@jamuhl I was expecting to be able to set up supportedLngs: ["en", "de"]
, and then init an i18next client with lng: "en-US"
, and have it automatically change the locale to en
, as that is our "closest" supported language. Are you saying that is not supported?
it will not change the language....but resolve translations from en-US
and en
plus fallback.json
(in that order)
https://www.i18next.com/principles/fallback#language-fallback
if you like to change en-US to only load en you will need to set load: 'languageOnly'
I'm not sure if this comment should be here but every issue i found about SSG redirects me to here, so, i add here:
since we cant use serverSideProps together with initialProps. we can make use of React FunctionComponent.defaultProps here is the workaround that works for me, and it still works with initialProps (#615 (comment))
- particular page
Page.defaultProps = { i18nNamespaces: ['home-page'] }
- main app / _app.tsx
MyApp.getInitialProps = async (appContext: AppContextType<Router>) => { const appProps = await App.getInitialProps(appContext) const defaultProps = appContext.Component.defaultProps return { ...appProps, pageProps: { namespacesRequired: [...(appProps.pageProps.namespacesRequired || []), ...(defaultProps?.i18nNamespaces || [])] } } }
Comment here.
Well, i thought it would work good, but it works with delay:
At the first loading, it does not load the skill-test
namespaces, i need to reload it to get that namespace active and the proper localization strings.
Inspecting it for the first time we get this:
After reloading, we can find the required namespace:
Both namespaces are in the option, but just the first one, defined on _app.js
by getInitialProps
, is loaded.
Why does this happen?
@TheFullResolution That's a separate point, and one I hadn't considered yet: how will locale fallbacks work, with this new NextJs v10 setup?
You can see that I am naively setting the language via
router.locale
here. But probably, we will need to passrouter.locale
into somei18next
detector, to get the correct fallback sequence. At the moment, it looks like if I passlocales: ['en-US', 'de-DE']
into NextJs, andlocales: ['en', 'de']
into i18next, the implementation breaks.If @adrai or @jamuhl could give some advice on that, I'd appreciate it! Not sure if that kind of fallback behaviour is already built in. It appears that there is no fallback sequence when setting
lng: initialLocale
on i18next client init.
Sorry, for the late reply - didn't see the question. So to give you some more information, the project is in a very early stage, so for now we have a very basic setup.
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
const resources = {
en: {
common: require('../../public/locales/en/common.json'),
home: require('../../public/locales/en/home.json'),
},
fr: {
common: require('../../public/locales/fr/common.json'),
home: require('../../public/locales/fr/home.json'),
},
nl: {
common: require('../../public/locales/nl/common.json'),
home: require('../../public/locales/nl/home.json'),
},
} as any;
type Config = string | boolean | null | string[];
export const i18nConfig: Record<string, Record<string, Config> | Config> = {
resources,
ns: ['common', 'home'],
defaultNS: 'common',
interpolation: {
escapeValue: false,
},
lng: 'en',
};
if (!i18n.isInitialized) {
i18n.use(initReactI18next).init({
...i18nConfig,
});
}
export { i18n };
And we load the language like this:
function MyApp({ Component, pageProps }: AppProps) {
const { locale } = useRouter();
useEffect(() => {
i18n.changeLanguage(locale!);
}, [locale]);
return (
<I18nextProvider i18n={i18n}>
<Component {...pageProps} />
</I18nextProvider>
);
}
export default MyApp;
Obviously, this is a very naive implementation, however, it allows us to get UI components out of the door.
Ourlocales
inside next.config.js
is en-NL
, nl-NL
, nl-BE
, fr-BE
when we load the page with locale nl-NL
and pass this to i18n.changeLanguage(locale!);
it just works and loads Dutch translations.
Just for thought: i18next's fallback functionality is going to be tricky to use with Next 10's new routing.
Next has its own locales
and defaultLocale
options and they're both required when using i18n in Next. It has 2 ways of determining the language: through the first item in the URL path (example.com/en) or through the Accept-Language header. It uses its own logic to pass the language into its router
object. If "en-uk" is passed from the client, but your app only supports "en", then Next will pass the defaultLocale
to the router. You have to somehow parse it out yourself through the request header or parse the URL (depending on which method Next is using). Then after you've parsed it, run it through i18next to determine the correct fallback.
I'm starting to go through the docs to see if there is a way to somehow intercept the request prior to routing. It seems the goal is to intercept, extract the language, run through i18next to detect the fallback, and set that as Next's language path. I'll continue researching.
Thanks @dcporter44 – if you can look into it, that'd be much appreciated. Syncing locale/fallback between NextJs and i18next is what we're currently stuck on.
Is your feature request related to a problem? Please describe.
Next.js is just out, and introduces i18n routing primitives
https://nextjs.org/docs/advanced-features/i18n-routing
Describe the solution you'd like
I think a lot of the features of
next-i18next
can be dropped, sonext-i18next
really just focuses on thei18next
integration.Describe alternatives you've considered
n/a
Additional context
n/a