QuiiBz / next-international

Type-safe internationalization (i18n) for Next.js
https://next-international.vercel.app
MIT License
1.31k stars 61 forks source link

On demand / dynamic fetching and reloading of translations #72

Open olafurns7 opened 1 year ago

olafurns7 commented 1 year ago

Why

Large commercial websites that serve visitors of multiple nationalities / locales can have a lot of translation strings.

In our case when these strings are created, they are done so in one locale in our CMS, then they are sent to a third party translation provider that in some cases takes time to return the localised versions of the strings.

I haven't tried how next-international handles what I'm suggesting below

Concept

Enable the functionality of dynamically updating the local key:value files, and reloading the translation provider so it has everything available.

The functionality I'm looking for is basically somehow being able to force a reload of assets. I don't think logic that does the fetching and writing of the updated files is something that belongs within this library.

What we have done before, with other i18n libraries is to iterate through the set locales, fetch data from a CMS, and then map the values out and write to a .json. Then trigger a reload of the 18n library so it repopulates the strings.

omarkhatibco commented 1 year ago

I tried to do the same concept and I thought it might work.

so I downloaded my translations from phrase app and cached them using next cache

so far everything was ok, until I got an error which seems the createI18nServer is expecting imports and does not accept an object

so far my implementation was like this

const locales = await prepareLocales()

export const { getI18n, getScopedI18n, getStaticParams } = createI18nServer(locales)

where locales variable would be

{
en: {
  key:'translation'
  },
de: {
  key:'translation'
  },
}
QuiiBz commented 1 year ago

createI18nServer / createI18nClient accepts an object where the values must be a function returning a promise of a locale object, because it's lazy-loaded on-demand. If you already have the values, you should easily be able to use the correct signature.

radum commented 1 year ago

@QuiiBz Am I right in saying the idea behind the lib is to use static translation files right?

Using an API approach to load the translation data is not in scope.

createGetI18n for example when it does return createT({ localeContent: flattenLocale((await locales[locale]()).default), ...

Is expecting the (await locales[]()).default to be a file that is why it has .default in there right?

If we are to switch the locale function from import to a fetch for example that returns a Promise that resolves to a JSON object will not work.

I guess we can hack around it and provide a default namespace key for the entire thing and that will solve the issue.

But I wonder apart from TS safety is there any other reason why you would want local translations as a default?

In my use case the translations come from an endpoint generated by a CMS where you would want them to be able to be changed on the fly without the need of a build step.

QuiiBz commented 1 year ago

Is expecting the (await locales[]()).default to be a file that is why it has .default in there right?

The file is a JavaScript module that has a default export (specifically the export default, https://next-international.vercel.app/docs/writing-locales), so to get it after the import we have to use the default field. That will work the same if you have JSON files.

radum commented 1 year ago

Thank you. Yeah I got that, perhaps my question was to complicated.

The point I was making is that the createT function handles the way the data is loaded. And that forces you to use a JS module. That logic can be moved into user space and if we need to load a JS module we do the same, if we need to use fetch('htttp://some-translation-endpoint') we could do the same.

So this:

export const { getI18n, getScopedI18n, getStaticParams } = createI18nServer({
  en: () => import('./en'),
  fr: () => import('./fr')
})

could become this:

export const { getI18n, getScopedI18n, getStaticParams } = createI18nServer({
  en: (await import(`./en`)).default,
  fr: (await import(`./fr`)).default
})

Doing so enables us to use fetch() as an alternative to get translations from other sources behind a http call. Because the import is not handled in the core lib.

Hope it make more sense now and I'm guessing that was a conscious choice to only support JS modules for import.

QuiiBz commented 1 year ago

You could totally do a fetch instead of an import - it just needs to return an object with a default key. The issue with your 2nd code example is that we no longer have callbacks, which means all the locales aren't lazy-loaded when needed. This is particularly crucial for client-side rendering.

marsidorowicz commented 8 months ago

i have other question about dynamic translations like

const translatedCountryList = countryList.map((country) => {
    return {
        value: country?.value,
        text: t(`countries.${country.text}`),
    }
})

where it works but gives error of no matching overload or Expected 2 arguments, but got 1 in this case. Is there a way to fix that?

marsidorowicz commented 8 months ago

this seems to fix it, not sure if safe but works and no error:

    const translatedCountryList = countryList.map((country) => {
        const text: any = `countries.${country.text}`
        return {
            value: country?.value,
            text: t(text, {}),
        }
    })
aditya0516 commented 4 months ago

@QuiiBz can you provide an example of doing a fetch instead of an import ?