aralroca / next-translate

Next.js plugin + i18n API for Next.js 🌍 - Load page translations and use them in an easy way!
MIT License
2.64k stars 207 forks source link

In i18n.js, an array of locales which point to defaultLocale as a fallback #1105

Open rolandjlevy opened 1 year ago

rolandjlevy commented 1 year ago

What version of this package are you using? 2.0.5

What problem do you want to solve?

I have the following folders in my locales folder. My default locale is 'en-gb' and I am just testing translation with one other language, 'ro'.

en-gb hu id ie pl ro th

I have about 20 different the namespace files inside the en-gb folder but there are no namespace files in the other folders for the remaining locales at the moment, whilst in development.

I want all the other locales, apart from 'ro' to point to 'en-gb' as the fallback whenever the useTranslate hook is called.

I tried limiting the locales and domains array to just 'en-gb' and 'ro', but when the other domains load all the pages error out with a 404. Only the Romanian and UK domains are working properly: dev.cromwell.co.uk and dev.cromwell.ro

What do you think is the correct solution to this problem?

Could we have a new setting to the i18n.js config which is an array of locales which point to the default locale as the fallback? Let's call it localesToFallback, So all the namespace files inside the defaultLocale folder, 'en-gb', will be used as the fallback for the locales listed in that array. Something like this:

module.exports = {
    locales = ['en-gb', 'ro', 'hu', 'id', 'ie', 'pl', 'th'],
    defaultLocale: 'en-gb',
    localesToFallback: ['hu', 'id', 'ie', 'pl', 'th'],
    pages: {
        '*': ['common'],
        '/basket': ['basket'],
        '/login': ['login'],
    }
}

Are you willing to submit a pull request to implement this change? Yes, I can try

aralroca commented 1 year ago

Probably you can do it using the loadLocaleFrom config function, that is just to get more flexibility in order to load the translations.

rolandjlevy commented 1 year ago

Thanks so much for getting back to me 😊

If I add the following to the config, will that mean that any locales folders which are empty will fallback to the files inside locales/en-gb?

{
  "loadLocaleFrom": (lang, ns) =>
    // You can use a dynamic import, fetch, whatever. You should
    // return a Promise with the JSON file.
    import(`./locales/en-gb/${ns}.json`).then((m) => m.default),
}
aralroca commented 1 year ago

No, you should implement fallbacks, example:

{
  "loadLocaleFrom": async (lang, ns) => {
     const translationsEnGb =  await import(`./locales/en-gb/${ns}.json`).then((m) => m.default)

     if (lang === 'en-gb') return translationsEnGb;

     const translations = await import(`./locales/${lang}/${ns}.json`).then((m) => m.default)

     return { ...translationsEnGb, ...translations }
  }
}
rolandjlevy commented 1 year ago

Hi @aralroca

Thanks for taking the time to provide me that example code.

I implemented it as you suggested and then got this error: "Critical dependency: the request of a dependency is an expression" so I followed the advice from this issue: https://github.com/aralroca/next-translate/issues/851#issuecomment-1173611946 but then got this error: "Module not found: Can't resolve 'next-translate/lib/cjs/plugin/utils.js'

I am using Next v12.2.5 and below is my config.

Any advice would be much appreciated - thank you

module.exports = {
  locales: [ 'en-gb',  'ro',  'en-ie',  'ie', 'en-in', 'en-za', 'th', 'id', 'ms-my', 'cs-cz', 'hu', 'pl', 'zh-cn'],
  defaultLocale: 'en-gb',
  extensionsRgx: /\.(tsx|ts|js|mjs|jsx)$/,
  keySeparator: '.',
  loadLocaleFrom: async (lang, ns) => {
    const translationsEnGb = await import(`./locales/en-gb/${ns}.json`).then((m) => m.default);
    if (lang === 'en-gb') return translationsEnGb;
    const translations = await import(`./locales/${lang}/${ns}.json`).then((m) => m.default);
    const importedTranslations = { ...translationsEnGb, ...translations };
    return importedTranslations;
  },
  pages: {
    '*': ['common', 'form-error-text'],
    '/basket': ['basket'],
    '/checkout/checkout-login': ['login'],
    '/create-account': ['create-account', 'reusable-components'],
    '/favourites': ['favourites'],
    '/favourites/[tab]': ['favourites'],
    '/forgotten-password': ['forgotten-password'],
  },
};
aralroca commented 1 year ago

@rolandjlevy Instead of 2.0.5 try the latest version of next-translate & next-translate-plugin

rolandjlevy commented 1 year ago

Hi @aralroca

I tried the latest version of next-translate & next-translate-plugin but still get the same error: "Critical dependency: the request of a dependency is an expression'

Instead of using a fallback, do you think using default namespace would solve my problem? This is what I am trying to achieve:

I make an API call in _app,js which checks whether or not translation is active in my db table. If it is not active then I want to remain on the same domain but use the default namespace files, in this case 'en-gb'. Below is the code in _app.js:

Is this the right approach or should I be using default namespace?

useEffect(() => {
    (async () => {
      try {
        // handleMakeRequest just uses axios for making API calls
        const response = await handleMakeRequest({
          method: 'get',
          url: '/domain-locales',
        });
        const translationActive = response?.data.find((item) =>
          currentDomain.includes(item.domain)
        );
        if (!translationActive) {
          const defaultLanguage = 'en-gb';
          await setLanguage(defaultLanguage );
        }
      } catch (err) {
        console.log(err);
      }
    })();
  }, []);

This is the config for next.config.js

{
  "locales": ['en-gb',  'ro',  'en-ie',  'ie', 'en-in', 'en-za', 'th', 'id', 'ms-my', 'cs-cz', 'hu', 'pl', 'zh-cn'],
  defaultLocale: 'en-gb',
  localeDetection: false,
  domains: [
    {
      "domain": "cromwell.co.uk",
      "defaultLocale": "en-gb"
    },
    {
      "domain": "ted.co.uk",
      "defaultLocale": "en-ie"
    },
    {
      "domain": "ted.ie",
      "defaultLocale": "ie"
    },
    {
      "domain": "cromwell.co.in",
      "defaultLocale": "en-in"
    },
    {
      "domain": "cromwell.co.za",
      "defaultLocale": "en-za"
    },
    {
      "domain": "cromwell.co.th",
      "defaultLocale": "th"
    },
    {
      "domain": "cromwell.co.id",
      "defaultLocale": "id"
    },
    {
      "domain": "cromwell.ro",
      "defaultLocale": "ro",
      "http": true
    },
    {
      "domain": "cromwell.com.my",
      "defaultLocale": "ms-my"
    },
    {
      "domain": "cromwell.cz",
      "defaultLocale": "cs-cz"
    },
    {
      "domain": "cromwell.hu",
      "defaultLocale": "hu"
    },
    {
      "domain": "cromwell.pl",
      "defaultLocale": "pl"
    },
    {
      "domain": "cromwell.co.cn",
      "defaultLocale": "zh-cn"
    }
  ]
}
aralroca commented 1 year ago

For me make sense to change the URL to the correct language. However maybe you can consider instead of doing it in a useEffect do these redirects in a middleware, it depends if you prefer not to track these pages in google and do the redirect correctly to the default language.

rolandjlevy commented 1 year ago

Hi @aralroca

Thanks again for your feedback. I will take a look at the middleware approach soon.

But first, is there a bug with the setLanguage function?

Here it says: "You could also use setLanguage to change the language while keeping the same page."

If I could use setLanguage and keep on the same page it would really solve the problem. But it doesn't stay on the page. Here are two examples:

On the Romanian domain https://www.dev.cromwell.ro setLanguage is not called in useEffect because translation is active in the db.

For the Hungarian domain setLanguage is called in useEffect because translation is NOT active in the db. So setLanguage is called with 'en-gb', like this: await setLanguage(defaultLanguage);

But when you go to the Hungarian domain https://www.dev.cromwell.hu it redirects to the UK domain https://www.dev.cromwell.co.uk so I am not able to change the language while keeping the same page.

Can you explain why setLanguage is doing this?

aralroca commented 1 year ago

setLanguage is not doing any extra magic, is using the router.push from next/router:

https://github.com/aralroca/next-translate/blob/de5ad45d005488b36f21fed291e18437af468972/src/setLanguage.tsx#L3-L15

Probably is changing to the domain that you have defined inside domains, but this is part of the i18n Next.js routing part. It's out of the scope of this library.

setLanguage is fine, but it make sense to change the language after user interaction with the page (in client-side), but if you want to do it before (in server-side), probably you should use middleware.

rolandjlevy commented 1 year ago

Hi @aralroca

I tried using middleware following various examples, including your one mentioned here but the rewrite in the middleware doesn't seem to work.

I changed the version of next-translate and next-translate-plugin to 2.3.0 as suggested in this comment

I have middleware.js in the root directory and I am using next v12.2.5

Here is the contents of my middleware.js

import { NextResponse } from 'next/server';
import i18n from './i18n';

export function middleware(request) {
  const locale = request.nextUrl.locale || i18n.defaultLocale;
  request.nextUrl.searchParams.set('lang', locale);
  request.nextUrl.href = request.nextUrl.href.replace(`/${locale}`, '');
  return NextResponse.rewrite(request.nextUrl);
}

Is there anything else I need to do to get the rewrite working?

Many thanks Roland

LarsEjaas commented 1 year ago

loadLocaleFrom

Definitely not an expert here @rolandjlevy, but I think you are close with the middleware. Have you tried simply making a redirect in the last line instead? So:

return NextResponse.redirect(request.nextUrl)
rolandjlevy commented 1 year ago

Thanks @LarsEjaas but that didn't work for me.

However, I managed to find a fallback solution which works for me, here: https://github.com/aralroca/next-translate/issues/1029#issuecomment-1684827974