[DISCUSSION] How to evolve to 1.0.0
aralroca opened this issue ยท 41 comments
I did a working example in the repo as an alternative to "build step", using dynamic routing.
It requires (for the moment) more configuration, but it also works for Automatic Static Optimization.
I invite you to experiment with the example that I did.
After achieving this, I'm asking myself how to evolve this library to the next major 1.0.0 release.
To be honest, I would like to remove the "build step" because it's a temporal workaround. And using dynamic routes is the natural way to achieve this in Next.js.
The problem that I see with dynamic routes is that requires three steps for each page:
- Add
lang
paths togetStaticPaths
- Add
lang
and namespaces props togetStaticProps
- Add
I18nProvider
on top of your page. (I used a HOC in the example)
And doing this for every page can be very cumbersome.
I open this issue in order to discuss if we really want to evolve simplifying this and removing the "build step", or not. And in the case that we really want to remove the "build step", how we can simplify this to avoid this cumbersome configuration.
Any feedback will be very welcome ๐
Update:
I'll remove the https://github.com/vinissimus/next-translate/tree/master/examples/with-dynamic-routes example, now there is a i18n routing support on Next.js and is a better way to evolve this library. The first step to 1.0 is the 0.19 release, adapting the i18n routing to this library.
Great! I've played with similar ideas for a while. My thoughts:
- It's possible to get lang from router and pass to provider in _app.js. Actual language can be obtained from the query or url (asPath). Something like that:
export default function App({ Component, pageProps, router }) {
const lang = getLang(router);
return (
<LangContext.Provider value={lang}>
<Component {...pageProps} />
</LangContext.Provider>
);
}
function getLang(router) {
if (router.query.lang && availableLangs.includes(router.query.lang)) {
return router.query.lang;
}
const maybeLangSlug = router.asPath.split('/')[1];
if (availableLangs.includes(maybeLangSlug)) {
return maybeLangSlug
}
return defaultLang;
}
With this approach even static routes as pages/en/about.jsx
have lang in context without any configuration.
- There are problems with
isFallback: true
in getStaticPath. Before on-demand SSG this pages do not have lang (that's one page/[lang]/item/[id]
for every lang+id), so when user visits page for first time, he gets this common page regardless actual lang in url.
So, maybe application needs "unknown" language state for this case with isFallback
...
upd: brain-dumped ideas here โ https://github.com/lastw/next-multilanguage-ssg-example (demo: https://next-multilanguage-ssg-example.now.sh/)
@lastw thank you a lot for your feedback!!
- It's possible to get lang from router and pass to provider in _app.js. Actual language can be obtained from the query or url (asPath).
That's a good idea to avoid to use a HOC in each page. I like it. So we can start simplifying using one HOC over _app.js
instead of one per page. This is good.
- There are problems with
isFallback: true
in getStaticPath. Before on-demand SSG this pages do not have lang (that's one page/[lang]/item/[id]
for every lang+id), so when user visits page for first time, he gets this common page regardless actual lang in url.So, maybe application needs "unknown" language state for this case with
isFallback
...
Right now I'm not understanding well the problem. I think tomorrow I'm going to experiment with it to reproduce this problem in order to understand it well.
As I understand the getStaticProps
is just delayed instead of being executed on the build. So it can't be solved by using a loading state? Or I'm wrong?
if (router.isFallback) {
return <div>Loading the page...</div>
}
I didn't understand what you mean with when user visits page for first time, he gets this common page regardless actual lang in url.
.
upd: brain-dumped ideas here โ https://github.com/lastw/next-multilanguage-ssg-example (demo: https://next-multilanguage-ssg-example.now.sh/)
Thanks a lot for sharing this โค๏ธ
As I understand the getStaticProps is just delayed instead of being executed on the build. So it can't be solved by using a loading state? Or I'm wrong?
For single-language website this is solution, check router.isFallback
and render page in the loading state. Loading state can have UI around that delayed data from getStaticProps โ header, footer, menu, text "loading...", etc. So, on the build phase we can build fancy and user-friendly /item/[id]
page with skeletons/spinners only where content should be.
With multiple languages on the build phase there will be generated only one page for all languages: /[lang]/item/[id]
. So, we can generate only skeletons/spinners, without text...
I didn't understand what you mean with
when user visits page for first time, he gets this common page regardless actual lang in url.
.
As for first time
I meant visiting pages (via direct link), that nobody visited before (and the generation was not triggered and cached yet). Users will get fallback page /[lang]/item/[id]
, and you need to design this page as "one fallback for all languages". Fullscreen skeleton/spinner is not so user-friendly, as it could be with specific fallback pages like /en/item/[id]
, if we had the way to make such "partial" fallbacks.
Apologies in advance if this is not the best place to ask, and congrats for all the work so far.
For 1.0.0 are you guys planning adding support for fully localized routes? eg:
[{
en: "/search"
es: "/buscador"
},
{
en: "/items/[id]"
es: "/productos/[id]"
}]
(note that absence of a locale prefix in the URL path)
The reason behind this kind of routing is often SEO, which is one of the things that attracts many to Next.js in the first place. It would be great if we found a way to achieve this that also supported SSG (and the new SPR).
Thank you!
I'd like to see an example of how dynamic routes would work with getServerSideProps
.
I have an example page using getServerSideProps
: https://github.com/justincy/next-translate/blob/dynamic-gssp/examples/with-dynamic-routes/pages/%5Blang%5D/more-examples/get-server-side-props.js
I just need to figure out how to handle unsupported languages.
I figured out how to handle unsupported languages with gSSP: justincy@2598b75#diff-bf033b67d0d5f822460e19faed094669
How are you guys going about language detection?
I've got the Tutorbook site setup such that requests to non-localized URLs (e.g. /search
instead of /en/search
) are sent to an /api/redirect
serverless function that then redirects the user to the correct locale based on their Accept-Language
headers.
In next.config.js
:
rewrites() {
return [
{
// We redirect the user to their appropriate locale directory based on
// their browser request cookies (via the `/api/redirect` endpoint).
// @see {@link https://github.com/tutorbookapp/covid-tutoring/issues/35}
source: '/',
destination: '/api/redirect',
},
{
// Don't redirect if there's a locale already in the requested URL. We
// also don't redirect if the browser's just trying to fetch favicons.
// @see {@link https://github.com/UnlyEd/next-right-now/pull/42}
// @see {@link https://github.com/pillarjs/path-to-regexp/issues/223}
source: `/:locale((?!${locales.join('|')}|favicon)[^/]+)(.*)`,
destination: '/api/redirect',
},
];
},
In /pages/api/redirect.ts
:
/**
* Redirects to a locale based on the HTTP request `Accept-Language` header.
* This API endpoint is called via the experimental Next.js rewrites feature
* (see the `next.config.js` file in the root of this repository).
*
* @example `/` => `/fr`
* @example `/search` => `/de/search`
* @example `/end` => `/en/end`
*
* @see {@link https://github.com/zeit/next.js/issues/9081}
* @see {@link https://github.com/UnlyEd/next-right-now/pull/42}
* @see {@link https://github.com/tutorbookapp/covid-tutoring/issues/35}
*/
export default function redirect(
req: NextApiRequest,
res: NextApiResponse<void>
): void {
const locale: string =
parser.pick(locales, req.headers['accept-language'] || '') || defaultLocale;
console.log(`[DEBUG] Redirecting to locale (${locale})...`);
res.statusCode = 302;
res.setHeader('Location', `/${locale}${req.url}`);
res.end();
console.log(`[DEBUG] Redirected '${req.url}' to '/${locale}${req.url}'.`);
}
Once they're redirected, all Link
and Router
calls send them to the page within their locale (e.g. <Link href='/search' />
automatically becomes <Link href='/en/search' />
).
See this issue for more info on how I got that all setup.
Another thing to think about @aralroca is how you'll provide automatic static optimization when adding the locale
to getStaticPaths
.
For example, let's say I want a dashboard to be at /[locale]/[username]/dashboard
but I want to statically pre-render a skeleton screen (i.e. when I don't know username
) and then fetch the user's data client-side once Next.js hydrates the page and triggers an update to the query
object (see this documentation).
But, by adding a getStaticPaths
, I'm now forced to either SSR that page or fallback to the default locale until Next.js hydrates the page. Once Next.js hydrates the page, I now know locale
and can manually fetch the necessary JSON translations. This is because we're forced by Next.js to provide all page routes when using getStaticPaths
(i.e. we can't just provide a locale
, we have to provide a locale
and a username
).
The best solution to that problem IMO is to add a helper that lets you use static optimization and then takes care of fetching static JSON translations and updating the I18nProvider
HOC once Next.js hydrates the page client-side.
I just opened this issue in the Next.js repository that should address the static optimization problem described above.
I just wrote a proposal to simplify i18n. Feedbacks are welcome!
Apologies in advance if this is not the best place to ask, and congrats for all the work so far.
For 1.0.0 are you guys planning adding support for fully localized routes? eg:
[{ en: "/search" es: "/buscador" }, { en: "/items/[id]" es: "/productos/[id]" }]
(note that absence of a locale prefix in the URL path)
The reason behind this kind of routing is often SEO, which is one of the things that attracts many to Next.js in the first place. It would be great if we found a way to achieve this that also supported SSG (and the new SPR).
Thank you!
I hope I understood you correctly, @javiercr . But actually including the locale prefix in the URL is better for SEO. URLs without language prefixes confuse Google and are advised against. Instead of subdirectories, you can use subdomains or TLDs, but relying only on cookies or accept-language headers is a bad idea. source: https://support.google.com/webmasters/answer/182192?hl=en
I hope I understood you correctly, @javiercr . But actually including the locale prefix in the URL is better for SEO. URLs without language prefixes confuse Google and are advised against.
We actually use different TLDs (domains) for each locale, and all of them point to the same Next.js app. In our case, it's the either the host (domain) or the path string (buscador
/ search
) what tell us what the current locale should be, but we also need to make sure the route for other locales returns a 404. Example:
ourapp.es/buscador => 200
ourapp.es/search => 404
ourapp.co.uk/search => 200
ourapp.co.uk/buscador => 404
Does that make sense? :)
Hi @aralroca and all,
I've been working on this i18n example: https://github.com/omarryhan/nextjs-i18n-example the past couple of days.
It builds on the work done in with-dynamic-routes example as well as other examples from the Next.js community. 1, 2.
You can read about the differences in the README. However, I want to highlight one particular pain point that I can't find a very good solution for. That is loading the translations for getStaticProps
(and getServerSideProps
). The current approach is to specify the translation namespaces you want to load in both the component (in useTranslations
) and the page (in getStaticProps
). Ideally, the translation namespaces should only be specified once, in the components. Having to define the namespaces in the pages as well is pretty annoying and makes your app prone to deployment bugs. For example, if you remove a namespace that is used by a component that you think your page will never mount. But this component conditionally mounts somewhere down the page tree. This will pass the next build command
but fail otherwise.
You might ask, why even load them in the getStaticProps
in the first placel; because that's the only place we can asynchronously load content for the pages. You can use dynamic translations as highlighted in the [language]/dynamic
page and in this repo's example as well. But loading all translations dynamically, isn't the best for SEO. And I assume most people who use Next.js have SEO as a top priority.
As a temporary workaround I wrote a function that loads all the translation JSON files found in the translations (locale) folder and passes them as props to the page. But this will significantly increase the bundle size of your app very quickly because it loads all the translations of all pages. Needless to say, it's not a very sustainable solution. I would love to hear your thoughts on that or any potential solutions. I mentioned this here, because I think this is a discussion that must be discussed for this library as well. Also, I'd prefer to use a lightweight library like this one rather than roll my own i18n logic from scratch.
I just had an idea regarding the pain point I mentioned in my comment above.
Instead of either having to:
- load all the namespaces.
- manually type in all the namespaces that a page and its components are going to need.
We can export a neededTranslations
array of strings in every component, and keep exporting them until they reach all the pages that are going to render them.
e.g.
// pages/index.jsx
import FooComponent, { neededTranslations as neededTranslationsFooComponent } from './components/FooComponent/';
export default = () => (
<FooComponent />
)
export const getServerSideProps = async ({
params,
}) => ({
props: {
...await getI18nProps({
language: params?.language as string,
paths: [
...neededTranslationsFooComponent
],
}),
},
});
// components/FooComponent.jsx
import BarComponent, { neededTranslations as neededTranslationsBarComponent } from './BarComponent';
const neededTranslations = 'components/FooComponent'
export default () => {
const { t } = useTranslations(neededTranslations);
return (
<div>
foo
<BarComponent />
</div>
)
);
export const neededTranslations = [
neededTranslations,
...neededTranslationsBarComponent
]
// components/BarComponent.jsx
export default () => (
<p>
bar
</p>
)
export const neededTranslations = [
'components/BarComponent'
]
An alternative would be: Instead of exporting neededTranslations
seperately, you can just attach it to the component itself e.g.
// components/FooComponent.jsx
const Component = () => (
<p>
foo
</p>
)
Component.neededTranslations = [
'components/FooComponent'
]
export default Component;
Unfortunately Typescript won't allow you to do this, because you can't attach custom properties to the React.FC
type.
I Would love to hear your thoughts. Also, I would love to hear your thoughts about the way I am structuring the namespaces themselves. For those who didn't see the example I mentioned above, I am structuring the namespace tree identically to the way you structure your components and pages.
Here's an example
components/
Foo/
index.jsx
Bar/
index.jsx
Baz/
index.jsx
pages/
index.jsx
ssr.jsx
public/
translations/
components/
Foo/
en.json
ar.json
Bar/
en.json
ar.json
Baz/
en.json
ar.json
pages/
index/
en.json
ar.json
ssr/
en.json
ar.json
I think this way is more natural and more suited to React (and component based frameworks in general).
@omarryhan maybe adding one file (i18n.json) at the component's root instead of having multiple files in a separate folder:
components/
Foo/
index.jsx
style.css
i18n.json
Bar/
index.jsx
style.css
i18n.json
Baz/
index.jsx
I like that better. But where do you keep the translations for pages? Maybe in a special pageTranslations
folder in the root of the project?
maybe something like this will do it, nextjs don't generate a page for i18n.json, and a (my-page/index.tsx is equivalent to my-page.tsx):
pages/
index.tsx
i18n.json
about/
index.tsx
i18n.json
[locale]/
index.tsx
i18n.json
catalog/
index.tsx
i18n.json
About having one file instead of multiple, if you do that, you won't be able to tree shake the translations. So, for every page you load, you'll have to download all the translations in all languages for that particular page.
About the structure itself, I think it will get messy pretty quickly to not have the translations in a dedicated translations folder. Especially if you have more than 2 languages. On the other hand, if you put them in a translations folder, the Next.js router might create a page for it, not sure though.
Another solution is to strictly only translate components, but that's a bit prohibitive imo.
Any progress on this? Would be great to be able to dynamically generate locale routes rather than using a custom build step.
Another alternative to a [lang]
dynamic parent route is generating a custom exportPathMap
like explained in vercel/next.js#5509 (comment), though getStaticPaths
might be cleaner. Either way it'd be great to offload the lang path generation to Next's buildstep with the various export/path generation hooks, rather than using our own custom one.
A new open RFC about i18n in Next.js is open, this time from the Next.js team: vercel/next.js#17078
Version 1.0 of this library will conform 100% to the final implementation of this RFC.
it might make sense to talk to @ijjk about this.
Whether they/he are/is already at the point, that next-translate
could use the new API in a canary
release and if there are any gotchas to be aware of.
๐จ NEWS!
I adapted under 0.19.0-experimental.1
0.19.0-experimental.11
all the Next.js i18n routing. Please, if you can try the experimental version, the PR is still opened #285, if you found some issues please report it on the PR!!
I updated the examples of the repo according to adapt the breaking changes. I tried to add backwards compatibility to almost every change + console.warn
(I still have to write the guide on how to migrate)
I think this is the first step for version 1.0.0. In 0.19.0 we will still have the "build step" but much more adapted to the next.js way and it is only used to encapsulate the namespaces on each page.
It would be great if somebody can try version 0.19.0-experimental.6 to see how everything works in real projects. Now what I need is some feedback from people, if I see that several people can migrate their projects successfully then I will probably do the 0.19 release (That is a big step before 1.0).
I have updated the examples: https://github.com/vinissimus/next-translate/tree/i18n-routing/examples
Thanks a lot!
I added a new example in the repo; without the "build step" and without the appWithI18n
, just raw ๐
https://github.com/vinissimus/next-translate/tree/master/examples/without-build-step
I think it can also be a good alternative, although it requires a little more configuration it's not a big headache. However, as I understood, the Next.js the next step they will do after i18n routing will be to load translations, so maybe for now it's worth keeping the "build step" because it's the simplest way, and when this will be done, we will release the 1.0.0 without the build step.
About this proposal: If someone wants to experiment, I did a first experimental version under 1.0.0-experimental.11 prerelease.1.0.0-experimental.1
This experimental version has:
- Hotreloading/refresh files
- Faster build
- Better development experience
- De-duping logic. Right now there are some alternatives: with builder, without builder + appWithI18n, etc. This is now integrated, to skip compiling namespaces you can pass
loader: false
on thei18n.json
. - Working in the same
pages
directory than Next.js. No need of workarounds. - Easy to test
- Easy to migrate in the future
- No need to indicate to the Next.js config the
locales
anddefaultLocales
onnext.config.js
, this will be injected by the plugin. So you only will have ai18n.js
ori18n.json
root file.
Things are still missing, so several things may still fail, but I prefer you to experiment it, and if you find something, report it here (please don't do PR yet to that branch).
https://github.com/vinissimus/next-translate/blob/master/src/I18nProvider.js#L80
Using router in parent component and inside Provider. I think, it need remove and make require props from parent on the contrary, don't using props
@popuguytheparrot can you explain it a little bit further, maybe with an example. Sorry but I didn't understand It.
Call useRouter in parent(_app) and call again in I18nProvider.
It seems to me that the logic is duplicated
export default function MyApp({ Component, pageProps }) {
const router = useRouter();
return (
<I18nProvider lang={router.locale} namespaces={pageProps._ns}>
<Component {...pageProps} />
</I18nProvider>
</>
);
}
export default function I18nProvider({
lang: lng,
namespaces = {},
children,
logger = missingKeyLogger,
}) {
const { locale } = useRouter() || {}
Ah @popuguytheparrot , you say that because of the example in the repo: https://github.com/vinissimus/next-translate/blob/master/examples/without-build-step/pages/_app.js It's funny, I encourage you to try the example and eliminate the "lang" of the _app.js
, you'll see that it doesn't work. It's like that inside the I18nProvider
the router only works in CSR, and in SSR always gives undefined
, instead, in _app.js
it works both CSR and SSR. I do not understand the reason...
However, in the I18nProvider
is interesting to keep both (from props or router.locale as fallback), because it will not always be the locale of the router, as it is possible for example to make the same page has 2 languages using two I18nProvider. Explained here: https://github.com/vinissimus/next-translate#10-how-to-use-multi-language-in-a-page
The migration guide to 1.0.0 is still available on https://github.com/vinissimus/next-translate/blob/1.0.0-experimental/docs/migration-guide-1.0.0.md (replacing the version to the latest experimental prerelease it should work).
Everything is already quite stabilized, in a short time we will surely go from the experimental version to the canary, since it looks like we will finally evolve around here. And I hope that the final version 1.0.0 will be available in December.
It would help a lot if you could try the latest prerelease and report possible problems; things in the documentation, issues... ๐
Can someone who has windows confirm that it works well in the development environment?
is it possible to type locales with this feature?
https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#template-literal-types
is it possible to type locales with this feature?
https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#template-literal-types
Yes @popuguytheparrot . And feel free to PR. I don't really have much experience with TypeScript and I'm sure you can do much better than I.
I did a short demo video about 1.0:
- Demo video: https://www.youtube.com/watch?v=QnCIjjYLCfc
- Code: https://github.com/vinissimus/next-translate/tree/1.0.0-experimental
I hope you find this useful ๐
(sorry my English)
It doesn't work with nextjs 10 and SSG (static export). Error: i18n support is not compatible with next export
. Don't think it's possible to make use of the built-in i18n feature this way.
@aralroca I've tried running a basic example on windows. It doesn't seem to work in both development and production environments. But a without-loader example is work fine.
Environments
- OS: windows 10 v20H2
- Node: v14.15.0
- Browser: MS Edge latest version
- react: v17.0.1
- next: v1.0.3
- next-translate: v1.0.0-experimental.18
@vimutti77 Thanks to report It! It looks like it is the fault of the absolute import on the default loadLocaleFrom
, which has a mixture of / and \ :
https://github.com/vinissimus/next-translate/runs/1457587510
defaultLoader: (l, n) =>
import(`D:\a\next-translate\next-translate/locales/${l}/${n}`).then(
(m) => m.default
),
Do you know how this import would be for it to be valid?
I suppose that fixing the string would work already. Or another solution will be using an alias in webpack instead. When I have some time I fix it, I already asked if someone could look at it in Windows because I was afraid it would not work after see the failing test.
If you add the loadLocaleFrom
in the configuration, so that it does not use the default, it works?
Thank you very much
@aralroca This is what I got when run yarn build
on basic example of next-translate v1.0.0-experimental.18
.
$ next build
info - Creating an optimized production build
info - Compiled successfully
info - Collecting page data
[=== ] info - Generating static pages (0/24){ lang: '' }
[ ===] info - Generating static pages (0/24){ lang: '' }
{ query: {} }
{ lang: '' }
{ lang: '' }
[ ==] info - Generating static pages (6/24){ query: { slug: '[slug]' } }
{ query: { slug: '[slug]' } }
{ query: { slug: '[slug]' } }
info - Generating static pages (24/24)
info - Finalizing page optimization
Page Size First Load JS
โ โ / 360 B 68.4 kB
โ โ /404 677 B 64.8 kB
โ โ /more-examples 2.2 kB 70.3 kB
โ โ /more-examples/catchall/[...all] 2.05 kB 66.2 kB
โ โ /more-examples/dynamic-namespace 2.54 kB 66.7 kB
โ โ /more-examples/dynamicroute/[slug] 2.08 kB 66.2 kB
+ First Load JS shared by all 64.1 kB
โ chunks/45dce98e7af2a80bb0901cbf74b9d0d8c7943397.85a7f9.js 13.1 kB
โ chunks/framework.a4a060.js 41.8 kB
โ chunks/main.2784a9.js 7.25 kB
โ chunks/pages/_app.78c12f.js 1.28 kB
โ chunks/webpack.e06743.js 751 B
ฮป (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
โ (Static) automatically rendered as static HTML (uses no initial props)
โ (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
(ISR) incremental static regeneration (uses revalidate in getStaticProps)
Closing this. Already fixed on 1.0.0-canary.1. The 1.0 will be released approximately 1-2 weeks. ๐