nuxt-modules / i18n

I18n module for Nuxt
https://i18n.nuxtjs.org
MIT License
1.75k stars 483 forks source link

Allow disabling page per locale with `useSetI18nParams` #2782

Open luca-smartpricing opened 9 months ago

luca-smartpricing commented 9 months ago

Environment

Reproduction

https://stackblitz.com/edit/github-c7te5q-s3x21p?file=app.vue

Describe the bug

Also if i've set

setI18nParams({
    it: { blog: 'blog-1-it' },
    de: { blog: 'blog-1-de' },
    en: { blog: undefined },
})

the og:locale:alternate for the en lang it was addes

<meta id="i18n-og-alt-en" property="og:locale:alternate" content="en">

Additional context

No response

Logs

No response

leopoldkristjansson commented 9 months ago

I just started a discussion about this, here: https://github.com/nuxt-modules/i18n/discussions/2781 since I was not sure if I was missing something from the docs or not.

In short, I think the best way to deal with missing translations of dynamic content would be to pass in a falsy value to indicate that this resource is actually not available in that particular language.

{
    'en-us': { slug: 'foobar-en' },
    'fr-fr': { slug: 'foobar-fr' },
    'sv-se': undefined,
 }

This should remove the <link rel="alternate"... for that language from the head. But what should the lang-switcher show?

luca-smartpricing commented 9 months ago

I agree with you, but currently if you set 'sv-se': undefined it doesn't work and automatically i18n create an alternate like /sv-se/current-path. The lang switcher should behave exactly like when you set:

setI18nParams({
    it: { blog: 'blog-1-it' },
    de: { blog: 'blog-1-de' },
    en: { blog: undefined },
})

It should show all languages set in nuxt.config but without href

<a data-v-86f55f02="">English</a>
<a href="/de/blog-1-de">Deutsch</a>
leopoldkristjansson commented 9 months ago

How about allowing something like this?

setI18nParams({
    it: { blog: 'blog-1-it' },
    de: { blog: 'blog-1-de' },
    en: { blog: undefined, alternativeUrl: '/whatever-i-want' },
})

This would produce no rel alternative for en in this case and still produce a link that makes sense for the lang-switcher.

luca-smartpricing commented 9 months ago

I don't think that's good practice. If the translation doesn't exist, it doesn't exist. You can make the lang switcher display as something like this:

Screenshot 2024-02-13 alle 15 21 51

Also because if you receive data from a CMS you don't know which language has not been translated. Maybe content creators don't translate one article into English and the next one they don't translate into German. If you really think that the alternativeUrl field is necessary, you should insert it either in all languages or as an additional parameter in addition to the list of languages. But I think this goes beyond the original problem

leopoldkristjansson commented 9 months ago

I don't think that's good practice. If the translation doesn't exist, it doesn't exist. You can make the lang switcher display as something like this:

I get that and I realize it's not a perfect solution, but I am mostly concerned about making the user aware of all the languages that are available for the website in general. Removing languages from the menu feels broken to me in a different way. The implementation in you screenshot is one way to go about this.

Anyway, the main issue here is that undefined should work as expected when the translation does not exist. The rest can be dealt with as people see fit.

AndersCV commented 7 months ago

I'm currently having this issue as well.

I have many different locales for our project and not all content is available across all locales. useI18nSetParamters() is incorrectly setting alternative links to non-existent pages in my <head> element which causes crawlers to index a bunch of pages that results in 404s which is not very beneficial.

This currently also causes a 404 and an error for the user if they try to switch to a language that is not available on the current page, since we have no way of defnining an alternative URL.

aaronLejeune commented 4 months ago

Hey all,

Any updates on this topic or workarounds we can use in the meantime? Having the same issue here as well

leopoldkristjansson commented 4 months ago

Hey all,

Any updates on this topic or workarounds we can use in the meantime? Having the same issue here as well

In the past I have done hack-y workarounds for this, where I set the missing translations to a fixed constant string in all cases. By doing this you can find the elements and do the post-production you need.

setI18nParams({
    it: { blog: 'blog-1-it' },
    de: { blog: 'blog-1-de' },
    en: { blog: 'NOT_TRANSLATED' },
})
aaronLejeune commented 3 months ago

@leopoldkristjansson , are you doing this in middleware? Would love to see how you did that. Could you provide a small example if you got some time? Thanks in advance!

when I try to log nuxtI18n to the console, its always undefined while I can see the value of nuxtI18N in to.meta...

export default defineNuxtRouteMiddleware((to, from) => {
  console.log("loggin META", to.meta.nuxtI18n) //is undefined
  console.log("loggin META", to.meta.layout) //is logging layout
  console.log("loggin META", to.meta) //is logging nuxtI18n and layout just fine
})
"loggin META" undefined
"loggin META" "default"
"loggin META" { layout: "default", nuxtI18n: { ... } }
AndersCV commented 1 month ago

@aaronLejeune What I did as a workaround to this issue:

As suggested above i set a hardcoded string like this

setI18nParams({
    // slug name and slug value hardcoded
    en: { funnel: 'NOT_TRANSLATED' },
})

then I created a global middleware like this

export default defineNuxtRouteMiddleware((to) => {
  const localePath = useLocalePath()
  if (to.params.funnel === 'NOT_TRANSLATED') {
    // redirect to frontpage
    return localePath('/')
  }
})

Now you will still have rel="alternate" links in your <head> containing the /NOT_TRANSLATED slug so what I did here was add additional rule to my robots.txt

User-agent: *
Disallow: /NOT_TRANSLATED/**

This is the best I could come up with atleast and it doesn't seem like this issue is gonna be solved by the module itself anytime soon.

AndersCV commented 1 month ago

@BobbieGoede I still think this issue deserves an important label. If you plan on using this module for any kind of app with many different languages and dynamic content served from a CMS you will most likely run into this issue.

We have upwards of 50.000 dynamic pages across 15 different languages and it's impossible to maintain them all across all langauges. As a result our language selector leads to 404 pages and incorrect alternate links in our head tags taking up our crawling budget.

BobbieGoede commented 1 month ago

@AndersCV I understand that this issue is important to you, I have no information as to how many projects (with this module) are using dynamic content from a CMS, and how many of those serve partially localized content.

This issue is specific to your use case, it does not violate documented behavior nor does it significantly impact build/runtime performance, so the label is appropriate. Besides that, this issue may seem simple on the surface but it relies on behavior inherent to vue-router's route resolution.

This project is maintained by only a few people in their spare time, it will likely come down to the same people to pick up an issue when they think it necessary or feasible. We welcome contributions and we would be happy to review any PR that provides a proper solution for this issue.

mickaelchanrion commented 3 weeks ago

I'm facing the same issue as you guys. The major problem I see here is that the head tells the browser alternate links exist and they lead to 404 as @AndersCV mentioned.

What I ended up doing is a bit hacky but it seems to work.

  1. For my case, I believe it's ok if the lang switcher redirects to the homepage if the page doesn't exist in the desired locale and since I use a catch-all for all my pages, I can just return an empty slug. In terms of UX, I think the user should be able to change the locale from anywhere, at all times: worst case scenario is just he restarts the navigation from the beginning
    setI18nParams({
    en: { slug: '' }
    })
  2. Then, for each missing locales for that page, I override the generated links and meta:
    
    const missingLocales: string[]

useHead({ link: missingLocales.map((code) => ({ id: i18n-alt-${code} })), meta: missingLocales.map((code) => ({ id: i18n-og-alt-${code} })), })


What's left are empty links and metas:
```html
<link id="i18n-alt-en">
<meta id="i18n-og-alt-en">
Tofandel commented 1 week ago

I have the same problem and I do agree that if we disable a locale for the user the lang switcher should still show an option that goes somewhere (but maybe some people will still want to disable the option), but the alternate tag should not be set as it's absolutely invalid to say that the homepage is an alternate translation of your content and could be penalized

I think then a good api would just be

setI18nParams({
  en: '/en/missing-locale', // This indicates that the locale is missing and the meta tag should not exist, but in the lang switcher redirect to that page
});
setI18nParams({
  en: false, // This indicates that the locale is missing and the meta tag should not exist, and the locale be temporarily removed from the locales array in vue-i18n
});

A better user experience in the switcher would be to indicate that it will not be the translation of the current page but a different page in the chosen locale, maybe with an icon or a title attribute, but that will not be up to nuxt-i18n, with the first option we can likely add a property unavailable: true to locales.en so that devland can distinguish easily

Alternatively we go with the second option only but with unavailable instead of removing the locale and the user adds an alternate link in the language switcher themselves, but it's then a bit less flexible, because it's the language switcher that would decide where to redirect

I'm willing to work on an implementation when I get some time, maybe by the end of next week (if not then in 3 weeks)