Open amannn opened 4 months ago
Our use case is usually having the opposite '/[locale]/[market]'. Which works-ish so far.
Routing is usually a pain afterwards, because we do not have the same utility functions as we next-intl
provides, where you can just define the path without having to worry about the dynamic parameters. I've been thinking about this for a while and I think it would be very nice if next-intl
was able to support N-number of dynamic params.
This is of course a bit more complicated as you write π
Which works-ish so far.
Do you have some details about the parts that don't work? Which localePrefix
setting do you use? Since the [locale]
prefix comes first, I'm wondering if the built-in routing capabilities from next-intl
could work for you here.
That being said, do you support an infinite number of markets? Otherwise, with the newly added capability for customization of prefixes, maybe you could define them statically.
E.g.:
import {LocalePrefix} from 'next-intl/routing';
export const locales = ['en-US', 'en-UK', /* ... */] as const;
export const localePrefix = {
mode: 'always',
prefixes: {
'en-US': '/en/us',
'en-UK': '/en/uk'
// ...
}
} satisfies LocalePrefix<typeof locales>;
See also: Can I read the matched prefix in my app?
I really love the multi-tenancy support in mind for this library, but I am wondering if the tenant could change the default locale, and the supported locales also managed by the tenant how it would be configured?
like in middlewere fetch tenant details then pass the options?
also if the messages are customized as well how it would be implemented?
in my app right now I am still on the pages router and now migrating to the app router and I faced too many challenges with the setup I am using next-i18next
in getServerSideProp
first I fetch the tenant based on the request host header then pass the active locales and the default one to the library options idk how it would be possible to handle this use case in next-intl
@yaman3bd Let's continue the conversation from https://github.com/amannn/next-intl/discussions/532#discussioncomment-9677402 here since it's more related to multi-tenancy than switching locales.
What you've shared in the other thread:
App structure:
.
βββ app/
βββ [domain]/
β βββ [locale]/
β βββ layout.tsx
β βββ not-found.tsx
β βββ page.tsx
βββ _components
βββ providers.tsx
βββ robots.ts
βββ sitemap.ts
Notes:
tenant
is fetched based on the host
headertenant.locale
indicates the default localetenant.supported_locales
includes all supported locales of a given tenantMy question would be:
Can you provide some examples how the URLs look like in the browser address bar for the user? Do you wish to implement any rewrites that would hide the [domain]
or [locale]
segment? Can you by chance share a URL to your app so I can check?
the urls:
the tenant can customize their domain to have their own domains smth like:
https://courseintelli.com/
or have a subdomain from our root domain:
it is:
*.msaaq.net
so:
https://aglowingbrain.msaaq.net/
and yes I will implement rewrite to hide the [domain]
, for the [locale]
segment I will implement rewrite only if the user locale was the default one, I do not want to show it in the URL, otherwise I will have a URL prefix
for example, if the tenant default locale is: AR
and the user language is also AR
I will apply a rewrite to hide it from the URL so the URL is:
tenant.msaaq.net
but the local param is: AR
and if the user detected language is: EN
I will implement a redirect this time and keep the locale prefix in the URL
but right now my production app is still on Page Router and I am not implementing any rewrites or redirects for the URLs I just get the host header in getServerSideProps
and fetch everything based on it.
but now I am migrating to App Router and I thought it would be much better to have it as a param
Thanks, that helps! What is your motivation for the [domain]
segment? You could continue to use the host header in Server Components if that has worked well for you so far to read the tenant.
Here's an example that should work if you only have a top-level [locale]
segment:
// middleware.tsx
import createIntlMiddleware from 'next-intl/middleware';
import {NextRequest} from 'next/server';
export default async function middleware(request: NextRequest) {
const tenant = await getTenant(request);
const handleI18nRouting = createIntlMiddleware({
locales: tenant.locales,
localePrefix: 'as-necessary',
defaultLocale: tenant.defaultLocale
});
const response = handleI18nRouting(request);
return response;
}
// ...
app/
[locale]
layout.tsx
page.tsx
That being said, [domain]
could be used to implement static rendering. Might be a bit more tricky, but based on the middleware implementation from above, you could probably modify the response
to adapt the rewrite to route to your [domain]
segment after the locale negotiation has run:
const rewrite = response.headers.get('x-middleware-rewrite');
if (rewrite) {
response.headers.set('x-middleware-rewrite', /* adapted URL with [domain] segment */)
}
Let me know if that helps!
const tenant = await getTenant(request);
this is really cool!, I did not know I could fetch data in the middleware!
but if the tenant fetch returns 404 or 500 can I return notFound or show the error page?
and the tenant messages are also fetched from the CMS should I make the fetch in getRequestConfig
?
import { headers } from "next/headers";
import { notFound } from "next/navigation";
import { getRequestConfig } from "next-intl/server";
export default getRequestConfig(async ({ locale }) => {
const host = headers.get("host");
const tenant = await getTenant(host);
const locales = tenant.supported_locales;
if (!locales.includes(locale as any)) notFound();
const messages = await getMessages(host, locale);
return {
messages: messages
};
});
but I am wondering if the user navigates to another page is getRequestConfig
invoked again?
or does it get called on the very first page reload only?
What is your motivation for the
[domain]
segment?
I think it helps for better isolation for the tenant so the data does not get mixup,
because I had an issue that if 2 tenants or more are in the app at the same time and they both accidentally made a page reload the data of the very first tenant who did the reload gets leaked to another tenant the issue was because I was reading the tenant host from the request headers and then assigning it to same axios instance so I think if I have implemented top-level [domain]
segment and used to read the tenant from I would not have this issue
and when I want to do a client-side fetch or mutation I need to send the tenant domain with the request headers and it helps for better structure since each tenant can customize the theme, page layout, and page blocks.
but if the tenant fetch returns 404 or 500 can I return notFound or show the error page?
A middleware can return a 404 status code, but unfortunately not a rendered 404 page. So if you need that, you'd have to call notFound
in a page/i18n.ts
.
the tenant messages are also fetched from the CMS should I make the fetch in getRequestConfig?
Yep!
I had an issue that if 2 tenants or more are in the app at the same time and they both accidentally made a page reload the data of the very first tenant who did the reload gets leaked to another tenant the issue was because I was reading the tenant host from the request headers and then assigning it to same axios instance
I see! Depending on your setup that could still happen, so the better choice is to not assign anything to global singleton instances. If you use fetch
in pages and i18n.ts
that should be fine.
So if static rendering is not a concern, you might be able to achieve a slighter easier setup by avoiding the [domain]
segment.
Hope this helps!
Our use-case has multiple TLDs (different regions for a storefront) with different kinds of locales. Currently we read the host and based on that determine the tenant/region, but this makes almost all pages dynamic. We could of course create duplicate instances with an ENV for the region, however we already have 8 storefronts and in the future even more.
Having a [domain] part that is rewritten/hidden in this library to also enable static rendering would be really great!
@pepijn-vanvlaanderen Have you by chance tried adapting x-middleware-rewrite
as mentioned above in https://github.com/amannn/next-intl/issues/1107#issuecomment-2152373012? Haven't tried it yet, but I think it could work for this use case.
Which works-ish so far.
Do you have some details about the parts that don't work? Which
localePrefix
setting do you use? Since the[locale]
prefix comes first, I'm wondering if the built-in routing capabilities fromnext-intl
could work for you here.That being said, do you support an infinite number of markets? Otherwise, with the newly added capability for customization of prefixes, maybe you could define them statically.
E.g.:
import {LocalePrefix} from 'next-intl/routing'; export const locales = ['en-US', 'en-UK', /* ... */] as const; export const localePrefix = { mode: 'always', prefixes: { 'en-US': '/en/us', 'en-UK': '/en/uk' // ... } } satisfies LocalePrefix<typeof locales>;
See also: Can I read the matched prefix in my app?
Sorry for the late reply.
Currently using always
. The built-in routing capabilities of next-intl do help, but would love if we could extend them to include more dynamic parameters as well π
E.g useRouter
allows you to send locale
as in the option parameter, but would be cool if we could extend that in user land to include other parameters. Makes it easier to enforce across apps.
This also applies to the middleware.
Now I am making some poor extension in the middleware to automatically redirect to the correct market, as well as the routing is a bit cumbersome as we have to construct the url manually.
Again, everything is possible to solve in user-land, but it will most likely be worse than what this library offers for locales.
Our market parameter contains information on both the country, but also the customer segment (B2B/B2C). Might not be the best approach but that is what we have now.
My approach to multi-tenancy might be a bit different to the use-cases of others as well π
@pepijn-vanvlaanderen Have you by chance tried adapting
x-middleware-rewrite
as mentioned above in #1107 (comment)? Haven't tried it yet, but I think it could work for this use case.
@amannn I was indeed able to make this work, thanks!
Also for anyone trying the same solution, I also disabled alternateLinks
and created the hreflang metas myself in the generateMetadata for every locale per domain.
I see! Depending on your setup that could still happen, so the better choice is to not assign anything to global singleton instances. If you use
fetch
in pages andi18n.ts
that should be fine.
@amannn it worked as a charm, thanks!
I have completed the setup and it is working fine, but I had an issue with createSharedPathnamesNavigation
I have to pass the supported locales but my locales are fetched from CMS how would I pass them to the function?
in i18n.ts
it is working fine
import { notFound } from "next/navigation";
import { getRequestConfig } from "next-intl/server";
import { fetchTenant } from "@/app/fetch/services/tenant-service";
import { fetchTranslations } from "@/app/fetch/services/translations-service";
export default getRequestConfig(async ({ locale }) => {
const tenant = await fetchTenant();
if (!tenant) notFound();
const locales = tenant.supported_locales.map((locale) => locale.code);
const defaultLocale = tenant.locale;
if (!locales.includes(locale)) notFound();
const translations = (await fetchTranslations());
return {
messages: translations[locale] ?? translations[defaultLocale]
};
});
I could not find a way to export the locales from i18n.ts
to createSharedPathnamesNavigation
I would appreciate any help or guidance.
it worked as a charm, thanks!
Awesome! π
I have completed the setup and it is working fine, but I had an issue with
createSharedPathnamesNavigation
I have to pass the supported locales but my locales are fetched from CMS how would I pass them to the function?
For this very use case, you can not pass locales
to createSharedPathnamesNavigation
βthe setting is optional for this function :). I think I need to mention that in the docs β¦
I think I need to mention that in the docs
Thank you for your response! I found out that this is actually mentioned in the docs: locales-unknown. sorry for missing it.
Is your feature request related to a problem? Please describe.
next-intl
currently requires a top-level[locale]
segment. While we support custom prefixes like/uk
β/en-gb
with https://github.com/amannn/next-intl/pull/1086, users have expressed the desire to add additional segments likeapp/[tenant]/[locale]
.Related: https://github.com/amannn/next-intl/issues/1055
(this issue was extracted from https://github.com/amannn/next-intl/issues/653)
Describe the solution you'd like
The solution here really depends on if:
[tenant]
segment comes before the[locale]
segment[tenant]
segment is visible to the user in the public pathnameAd 1) If the
[tenant]
segment comes after the[locale]
segment, this should already work. The only assumption fromnext-intl
is that the[locale]
segment comes right at the beginning.Ad 2) In case you support a finite number of tenants that are known at build time, you could use custom prefixes to define them as necessary (e.g.
'en-US-x-tenant1': '/tenant1/en-US'
). Alternatively, you could also deploy your app separately for each tenant and set abasePath
for each tenant.Ad 3) If the
[tenant]
segment is not visible to the user, you should already be able to implement this via composing the middleware or if you anyway have a requirement for static rendering, you could consider usingunstable_setRequestLocale
to pass alocale
that depends on the tenant.Ad 4) This use case is already supported pretty well via the
domains
config option.Therefore many use cases should already be possible. The one that remains is where the routing looks like
[tenant]/[locale]
, you support a dynamic number of tenants and the tenant is visible to the user in the pathname (e.g./tenant1/en
).Continuing on the work from https://github.com/amannn/next-intl/pull/1086, maybe we could support a configurable pattern that is incorporated by the middleware and the navigation APIs.
Example API:
Since we have two or more dynamic segments following each other,
next-intl
requires knowledge about which segment to retrieve the locale from.It would be great to research how common this pattern is, therefore please leave a thumbs up here, leave a comment with your use case and possibly subscribe to updates if this is relevant for you!
Describe alternatives you've considered
If you're out of other options, you could also consider providing your own implementation for the middleware and navigation APIs (see e.g. https://github.com/amannn/next-intl/issues/609). Note that you can still use component APIs like
useTranslations
in this case.