Hello Payload 👋
- Using Payload cms v3 on Next.js.
- Project based on the Payload website template
- Fields are localized using
localized: true.
Currently only the
defaultLocaleconfig is respected.
Using query parameters like
http://localhost:3000/?locale=frdoesn't work except for API queries of course.
The payload localization config looks like this and is fully functional in the payload admin panel:
localization: {
locales: [
{
label: {
en: 'English',
fr: 'Anglais',
},
code: 'en',
},
{
label: {
en: 'French',
fr: 'Français',
},
code: 'fr',
},
],
defaultLocale: 'en', // THIS SUCCESSFULLY SWITCHES THE FRONT LANGUAGE
fallback: true,
},
i18n: {
supportedLanguages: { en, fr },
},
Some posts have suggested integrating with next-intl.
I have used next-intl on a pure Next.js project but wouldn't know how to integrate it into my payload project.
I was hoping I could build a language switcher component in the front like this:
'use client'
import React from 'react'
import Link from 'next/link'
import { useRouter, usePathname } from 'next/navigation'
export const LanguageSwitcher = () => {
const router = useRouter()
const pathname = usePathname()
return (
<div className="flex gap-2">
<div className="flex items-center gap-3">
<Link href={pathname} locale="en">
EN
</Link>
<Link href={pathname} locale="fr">
FR
</Link>
</div>
</div>
)
}This does not work however.
Thank you for your help 🦾
My issue is resolved thanks to the amazing mflisikowski! ❤️
Hey bro, I read your question for about 5 min, but ... where is the dear "mflisikowski" answer so?
He has shared a repository.
@701070944713703474would you be willing to share your reference implementation here?
Thanks again for the help :>
there is no repository link, could you mention it please
I would be interested as well 🙂
are you guys both using the website template on payloadcms v3?
@841618502690275329
I am
but I can't find any example about how it works
Got it, I'll get back to you later. I'm setting up a template with language selection
I can't wait for it.
if you mention me after doing it, I would be appreciate it.
please wait i switch my repo to public again 🙂
I replied to
@1297618297503748167via private message, which is why you can’t see it 🙂
Thanks 👍
I'll make sure to set up an overhauled website template forking from payloadcms main
@1297618297503748167
Supportive & passionate people! Thank you all.
There might be quite a bit of mess in the code, but I’m trying to work on my website in my free time and I’m using Payload for it.
Could you guys please mention a very short snippet of get payload, find section, I have a deep trouble with passing params to where clause to filter data or Now, to local option.
deny the i18n config, I need some advice for the local option section
Hi! Could you clarify your question a bit? If possible, could you provide a small example or more details about the issue you’re facing? This will help us understand your problem better and offer more precise advice. 😊
@900169367927550032
WIP and a bit hacky, will improve on this.
language support on branch
/feat/website-template-i18n-l10n-next-intlThis solution is based on the current website template.
What you need to do:
- set the enabled languages in:
/src/i18n- make fields localized by adding
localized: trueI've opted to not use message files for this.
I might use them for the language selection label later on.
The language selection should be working. Let me know if there are any issues
I am gonna watch this thread, very similar to my work progress. I have a localized most of my Payload Project (Sitemaps, robots and so on) - the only thing I still need to work on, is a locale switcher, which respects the different localized slugs.
Will definitely upload it to github, as soon as I cleaned up.
I think the only way to do so, is to take the actual pathname, run getDocument with 'all' and filter for the localized slug, which we are requesting. Maybe someone has a better idea?
BTW: Really do like the approach at:
templates/website/src/i18n/routing.tsThanks, bro. Regarding the collections config and querying documents in this example, none of the collection fields have localized: true, and only querying pages checks the locales. Do I understand that correctly
yeah, you need to set localized: true in your fields
just pass lang in rootlayout and page.tsx as a param
the 'hacky' locale switcher i've implemented uses this hook:
the component is located under
providers:
The goal is implementing a clean provider. The WIP implementation is commented out, feel free to fix it
next-intl uses a locale prefix and your localized page will be available under its slug or fall back to the fallbackLanguage which you should have configured.
.../en/dashboard → .../fr/dashboard
now one thing to note is that you can translate the slugs as well:
.../en/home → .../fr/accueil
but I wouldn't recommend using localized slugs right now
Heyy Arrrr & D, thank you for posting your code - I will have a look a bit later during the day.
What leads to this statement? "But I wouldn't recommend using localized slugs right now"
Or is it just because of all the other changes you need to think about (Sitemap, Links in <RichText> and so on?
Thanks
@1297618297503748167, I tried to add localized to slug, but it doesn't work properly, the last saved slug in any language works and the rest doesn't work anymore.
and also I noticed that the language switcher send user to home page instead of changing the language on current page.
and I think for the last polished thing, we should be able to remove the default for example en from route url to having a better clean url.
feel free to improve on it
that sounds like the field is not localized. make sure the locale is displayed as part of the fields name e.g.
Slug — EN(in the admin dashboard). You might want to recreate your db
it has, but I don't why, the last slug languages only stored as doc slug, maybe it need some check on db.
When writing your own components without properly configuring all fields, you might try to access a routes that don't exist under the current language prefix.
The second reason being that my very hacky locale switcher might not properly redirect you to the correct route on language change.
This is why I personally wouldn't recommend it. If you manage to solve these issues, I'd like to know how you did it
It's getting late for me, see you around
Hello guys! This thread discuss my work too. Did you guys managed to translate the slugs too?
For localized routes I suggest doing this (its from [next-intl docs](
https://next-intl.dev/docs/routing))
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'de'],
defaultLocale: 'en',
// Thepathnames
object holds pairs of internal and
// external paths. Based on the locale, the external
// paths are rewritten to the shared, internal ones.
pathnames: {
// If all locales use the same pathname, a single
// external path can be used for all locales
'/': '/',
'/blog': '/blog',
// If locales use different paths, you can
// specify each external path per locale
'/about': {
en: '/about',
de: '/ueber-uns'
},
// Dynamic params are supported via square brackets
'/news/[articleSlug]-[articleId]': {
en: '/news/[articleSlug]-[articleId]',
de: '/neuigkeiten/[articleSlug]-[articleId]'
},
// Static pathnames that overlap with dynamic segments
// will be prioritized over the dynamic segment
'/news/just-in': {
en: '/news/just-in',
de: '/neuigkeiten/aktuell'
},
// Also (optional) catch-all segments are supported
'/categories/[...slug]': {
en: '/categories/[...slug]',
de: '/kategorien/[...slug]'
}
}
});This works great except in 1 particular case.
If I have English as the current language and I´m on the following route the following route:
And then I switch to German, the page will not be found since the dynamic part will stay the same. So when I switch to German the URL will be:
BASE/URL/de/neuigkeiten/
hello-worldinstead of:
BASE/URL/de/neuigkeiten/
hallo-weltSo for that particular case you would have to add some custom logic if you want. I think it´s redundant since the user will probably already be on the correct locale from the start. Otherwise it works great.
Maybe you have solved it already, otherwise try this in your collection:
...slugField().map((field) => ({
...field,
localized: true,
})),Your live preview works as expected?
Hey there, thanks for your feedback on topic,
I didn't go for localized slug, I think it can be universal en for path, I don't think it has a significant effect on SEO, what do you think
I think it's effect a little bit
Your live preview works with the localization?
I can't remember exactly, but yeah it was ok, it just needs to override localized for SEO plugin Fields.
I just added localized to a richText, works as expected in the 'left' side where is the admin panel, but in the right side in the live preview not updating in the live preview 😦
I didn't try rich text for SEO fields as a live preview, just use a localized description field.
I don't using for seo
Just a regular richtext field
O I'm sorry
It's my bad
Now I get what you say
Let me check it, and I will drop the result
If you dashboard is behind some sort of login it wont have an impact on SEO since it will probably not even be indexed. Otherwise, yes it might be bad for SEO to have english slugs for french pages
It´s fairly easy to setup to be honest, you just define your pathnames and next-intl will automatically translate they routes for you. So if a user requests /dashboard in french it will automatically give them /tableau-de-bord (or whatever it is in French)
your live preview works?
Haven´t setup it up yet and tested it properly to be honest but I will check. The normal preview works fine, just pass the locale to your preview function
I'll try it for sure, thanks
Yes, my live preview works. I only have it setup on my posts collection at the moment, but it works for all locales
Do you have a public repo where I can check?
Do it from the start, it will save you a lot of headache later😀 I´ts more painful to add later on (in my experience)
Not public but I can send you code snippets
Do you use the website template?
Does the website template come with built-in localization support, such as URLs like
/en/contactand
/de/kontakt? Or is there an other template necessary?
Yes, just a moment and I will send you the code
You're right man
No, you have to install next-intl. Payload only handles the localization for your docs and the admin, you have to handle the frontend with next-intl (or similar packages)
ok thanks for the info, what about the localization example of payload from github?
https://github.com/payloadcms/payload/tree/main/examples/localizationshould i use the website template for my next project or this example which is i guess done and ready to go
For non technical users who want to write blogs on websites, localized slug is a little mess, does next intl do it automatically for Persian language with correct meaning?
Message will to too long if I send to whole files.
Here is on the /posts/index.ts:
admin: {
group: groupLabels.posts,
defaultColumns: ['title', 'slug', '_status', 'publishedAt'],
livePreview: {
url: ({ data, req }) => {
if (!data?.category || !data.slug) return ''
return generatePostPreviewPath({
slug: data.slug,
category: data.category,
locale: req.locale || 'en',
req,
})
},
},
preview: ({ data, req }: { data: Record<string, any>; req: PayloadRequest }) => {
console.log('Normal Preview called:', { data, req })
if (data?.category && typeof data.category === 'object' && 'slug' in data.category) {
const url = generatePostPreviewPath({
slug: typeof data?.slug === 'string' ? data.slug : '',
category: data.category,
locale: req.locale || 'en',
req,
})
return url
}
return ''
},
useAsTitle: 'title',
},Here is the function to retrieve the URL in the file utilities/generatePreviewPath.ts
export const generatePreviewPath = ({ collection, slug, req }: Props) => {
const path =${collectionPrefixMap[collection]}/${slug}
const params = {
slug,
collection,
path,
}
const encodedParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
encodedParams.append(key, value)
})
const isProduction =
process.env.NODE_ENV === 'production' || Boolean(process.env.VERCEL_PROJECT_PRODUCTION_URL)
const protocol = isProduction ? 'https:' : req.protocol
const url =${protocol}//${req.host}/next/preview?${encodedParams.toString()}
return url
}Here is a shortened version of my
app/i18n/routing.tsimport { defineRouting } from 'next-intl/routing'
import { createNavigation } from 'next-intl/navigation'
export type Locale = 'en' | 'de' | 'es'
export const routing = defineRouting({
// A list of all locales that are supported
locales: ['en', 'de', 'es'] as const,
// Used when no locale matches
defaultLocale: 'en' as const,
pathnames: {
'/posts': {
en: '/posts',
de: '/beitraege',
es: '/publicaciones',
},
'/posts/[categorySlug]': {
en: '/posts/[categorySlug]',
de: '/beitraege/[categorySlug]',
es: '/publicaciones/[categorySlug]',
},
'/posts/[categorySlug]/[slug]': {
en: '/posts/[categorySlug]/[slug]',
de: '/beitraege/[categorySlug]/[slug]',
es: '/publicaciones/[categorySlug]/[slug]',
},
},
} as const)
// Lightweight wrappers around Next.js' navigation APIs
// that will consider the routing configuration
export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing)You have a function somewhere name generatePostPreviewPath
Can you send that too?
I havent tried it, but it looks to be the website template with next-intl added to it. So yes, it should be both localization for payload admin and frontend.
Thanks for helping!!!
Yes, sorry I sent to wrong function I see now😀
thanks fot the info!
I updated my previous response now. I use
/posts/[categorySlug]/[slug], if you are using just
/posts/[slug]then you can probably just use the default
generatePreviewPath-function that comes with the template and just add locale as a paremeter to that function.
Thanks a lot!
As long as you add your predefined routes for persian it should work. Or what do you think is the mess in this case?
No problem, happy to help!
For routes we should add hard coding in routes.ts file as you mentioned here, right?
I mean adding daily content like blogs needs much effort because of localized slug, what is the modern fast approach?
It's a kind of blog title that is already localized, but the title can be long, and we should add slugs manually as well.
You add hardcoded routes for the routes that will probably never change. So in your case you would just need to add:
// For your post archive
'/posts': {
en: '/posts',
fa: '/داشبورد', (farsi, idk if translation is correct but you get my point)
},
// For a single post
'/posts/[postSlug]': {
en: '/posts/[categorySlug]',
fa: '/داشبورد/[postSlug]',
},You dont have to add a harcoded route for every blogpost that you have, just the static path as you can see.
How many static paths will you have? Maybe 5-10 for a medium website?
Contact
About
FAQ
Posts
Dashboard
You just hardcode these into your routing, and then for dynamic slugs next-intl will handle everything if you follow the pattern I sent above
I just love how you try to help your community 🔥
Exactly, and I'm not talking about base path or static path, I mean content (post, blog,...) slug, that should be added to each one manually, and that's the cost of having it.
Thanks, yeah I´ve needed help also so I know how important it is👍
This is a dilemma for every localized website. I am working on a website with at least 5 different languages. So for every post in English I have to make 4 more posts manually at the moment.
You basically end up with 3 choices:
The WPML plugin itself costs something around 99$/year, cant remember. Then for every post you automatically translate you use credits. These credits expire fast as your website grows, and you´ll have to buy more. See here:
https://wpml.org/documentation/automatic-translation/automatic-translation-pricing/. I´ve done my calculations and for me it´s far from worth it, I will be able to translate like 2 posts before I have to buy more credits.
There are other WordPress translation plugins but your problem will be the same and pricing will be similar for all of them. And WordPress can be a pain to use.
If you have 2 languages, English and Farsi this won´t be a super big problem.
Time examples:
1. Writing the post in English (
1 hour)
2. Pasting the English post in to ChatGPT and translating it (
10 minutes) depending on custom blocks etc.)
3. Every other language you have, same as step 2 (
10 minutes)
So every language will add an extra 10 minutes more or less.
This is basically what the all WordPress plugins does with their automatic translations, they just take your content and send it to either of the below listed services and charge different credits depending on which one you use.
Microsoft
DeepL
WPML AI*
So you could implement it yourself by sending the richtext content to ChatGPT
Amazing details

I was thinking about the last part, is it possible in a bug free and open source way to implement data in this tech stack, to what we can do by copy and pasting data into chatgpt but in automatic and consistent prompt probably. It can be a huge deal for multilingual websites.
So for option 2 you will basically have the below equation. Writing this if someone else want to calculate the effort in time to manually translate things.
M is the time to write a post in one language.
For every extra language, you spend an extra fixed time—say 10 minutes—to translate.
Then if you have n additional languages, the total time is:
For example, if it takes 60 minutes to write the post (M = 60) and you have 2 additional languages (n = 2), then:
Yes it´s possible, I might add it later to my website but I don´t have time for it now actually
Star
Discord
online
Get dedicated engineering support directly from the Payload team.