Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Frontend localization / language switcher

default discord avatar
arrranddlast year
62

Hello Payload 👋



- Using Payload cms v3 on Next.js.


- Project based on the Payload website template


- Fields are localized using

localized: true

.



How could I get my front to display in a selected language?

Currently only the

defaultLocale

config is respected.


Using query parameters like

http://localhost:3000/?locale=fr

doesn'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 },
  },


What do I need to do in order to build a functional language selector?


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! ❤️

  • default discord avatar
    nimazabihilast year

    Hey bro, I read your question for about 5 min, but ... where is the dear "mflisikowski" answer so?

  • default discord avatar
    arrranddlast year

    He has shared a repository.

    @701070944713703474

    would you be willing to share your reference implementation here?


    Thanks again for the help :>

  • default discord avatar
    nimazabihilast year

    there is no repository link, could you mention it please

  • default discord avatar
    wuthirschlast year
    @1297618297503748167

    I would be interested as well 🙂

  • default discord avatar
    arrranddlast year

    are you guys both using the website template on payloadcms v3?



    @900169367927550032

    @841618502690275329
  • default discord avatar
    nimazabihilast year

    I am



    but I can't find any example about how it works

  • default discord avatar
    arrranddlast year

    Got it, I'll get back to you later. I'm setting up a template with language selection

  • default discord avatar
    nimazabihilast year

    I can't wait for it.



    if you mention me after doing it, I would be appreciate it.

  • default discord avatar
    mflisikowskilast year

    please wait i switch my repo to public again 🙂



    https://github.com/mflisikowski/mflisikowski.dev/blob/main/src/middleware.ts


    https://github.com/mflisikowski/mflisikowski.dev/tree/main/src/i18n

    I replied to

    @1297618297503748167

    via private message, which is why you can’t see it 🙂

  • default discord avatar
    arrranddlast year

    Thanks 👍


    I'll make sure to set up an overhauled website template forking from payloadcms main

  • default discord avatar
    nimazabihilast year
    @701070944713703474

    @1297618297503748167

    Supportive & passionate people! Thank you all.

  • default discord avatar
    mflisikowskilast year
    @841618502690275329

    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.

  • default discord avatar
    nimazabihilast year

    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

  • default discord avatar
    mflisikowskilast year
    @841618502690275329

    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. 😊

  • default discord avatar
    arrranddlast year
    @841618502690275329

    @900169367927550032


    WIP and a bit hacky, will improve on this.


    language support on branch

    /feat/website-template-i18n-l10n-next-intl

    https://github.com/arrr-and-d/payload/tree/feat/website-template-i18n-l10n-next-intl/templates/website

    This 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: true

    I'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

  • default discord avatar
    wuthirschlast year

    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.ts
  • default discord avatar
    nimazabihilast year

    Thanks, 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

  • default discord avatar
    arrranddlast year

    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:


    https://github.com/arrr-and-d/payload/blob/feat/website-template-i18n-l10n-next-intl/templates/website/src/hooks/useLanguageSwitch.ts

    the component is located under

    providers

    :


    https://github.com/arrr-and-d/payload/blob/feat/website-template-i18n-l10n-next-intl/templates/website/src/providers/Language/LanguageSelector/index.tsx

    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

  • default discord avatar
    wuthirschlast year

    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?

  • default discord avatar
    nimazabihilast year

    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.

  • default discord avatar
    arrranddlast year

    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

  • default discord avatar
    nimazabihilast year

    it has, but I don't why, the last slug languages only stored as doc slug, maybe it need some check on db.

  • default discord avatar
    arrranddlast year

    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

  • default discord avatar
    adek1449last year

    Hello guys! This thread discuss my work too. Did you guys managed to translate the slugs too?

  • default discord avatar
    themachine0488last year

    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', // The

    pathnames

    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:


    BASE/URL/en/news/hello-world

    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-world

    instead of:


    BASE/URL/de/neuigkeiten/

    hallo-welt

    So 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, })),
  • default discord avatar
    adek1449last year

    Your live preview works as expected?

  • default discord avatar
    nimazabihilast year

    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

  • default discord avatar
    adek1449last year

    I think it's effect a little bit



    Your live preview works with the localization?

  • default discord avatar
    nimazabihilast year

    I can't remember exactly, but yeah it was ok, it just needs to override localized for SEO plugin Fields.

  • default discord avatar
    adek1449last year

    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 😦

  • default discord avatar
    nimazabihilast year

    I didn't try rich text for SEO fields as a live preview, just use a localized description field.

  • default discord avatar
    adek1449last year

    I don't using for seo



    Just a regular richtext field

  • default discord avatar
    nimazabihilast year

    O I'm sorry



    It's my bad



    Now I get what you say



    Let me check it, and I will drop the result

  • default discord avatar
    themachine0488last year

    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)

  • default discord avatar
    adek1449last year
    @1286711107020390481

    your live preview works?

  • default discord avatar
    themachine0488last year

    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

  • default discord avatar
    nimazabihilast year

    I'll try it for sure, thanks

  • default discord avatar
    themachine0488last year

    Yes, my live preview works. I only have it setup on my posts collection at the moment, but it works for all locales

  • default discord avatar
    adek1449last year

    Do you have a public repo where I can check?

  • default discord avatar
    themachine0488last year

    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

  • default discord avatar
    adek1449last year

    Do you use the website template?

  • default discord avatar
    chrissi_stonelast year

    Does the website template come with built-in localization support, such as URLs like

    /en/contact

    and

    /de/kontakt

    ? Or is there an other template necessary?

  • default discord avatar
    themachine0488last year

    Yes, just a moment and I will send you the code

  • default discord avatar
    nimazabihilast year

    You're right man

  • default discord avatar
    themachine0488last year

    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)

  • default discord avatar
    chrissi_stonelast year

    ok thanks for the info, what about the localization example of payload from github?

    https://github.com/payloadcms/payload/tree/main/examples/localization

    should i use the website template for my next project or this example which is i guess done and ready to go

  • default discord avatar
    nimazabihilast year

    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?

  • default discord avatar
    themachine0488last year
    @205786043121664000

    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 }

    @205786043121664000

    Here is a shortened version of my

    app/i18n/routing.ts

    import { 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)
  • default discord avatar
    adek1449last year

    You have a function somewhere name generatePostPreviewPath



    Can you send that too?

  • default discord avatar
    themachine0488last year

    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.

  • default discord avatar
    adek1449last year

    Thanks for helping!!!

  • default discord avatar
    themachine0488last year

    Yes, sorry I sent to wrong function I see now😀

  • default discord avatar
    chrissi_stonelast year

    thanks fot the info!

  • default discord avatar
    themachine0488last year
    @205786043121664000

    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.

  • default discord avatar
    adek1449last year

    Thanks a lot!

  • default discord avatar
    themachine0488last year

    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!

  • default discord avatar
    nimazabihilast year

    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.

  • default discord avatar
    themachine0488last year

    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

  • default discord avatar
    nimazabihilast year

    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.

  • default discord avatar
    themachine0488last year

    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:



    1. Use WordPress and buy a translation plugin and let it automatically translate the content for you.

    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.



    2. Manually translate all your posts

    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.



    3. Write custom logic to automatically translate your content

    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


    Google


    DeepL


    WPML AI*



    So you could implement it yourself by sending the richtext content to ChatGPT

  • default discord avatar
    nimazabihilast year

    Amazing details

    :chefskiss:


    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.

  • default discord avatar
    themachine0488last year

    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:


    Total Time=M+n×10 minutes

    For example, if it takes 60 minutes to write the post (M = 60) and you have 2 additional languages (n = 2), then:


    Total Time=60+2×10=80 minutes.

    Yes it´s possible, I might add it later to my website but I don´t have time for it now actually

Star on GitHub

Star

Chat on Discord

Discord

online

Can't find what you're looking for?

Get dedicated engineering support directly from the Payload team.