i18next icon indicating copy to clipboard operation
i18next copied to clipboard

createInstance mixing up instances

Open theahmadzai opened this issue 5 months ago • 3 comments

🐛 Bug Report

We're calling createInstance each time we're sending an email for different store and load that store's translation into the instance then use the text, but we have got the reports that translations are mixing up some stores that are missing translations instead of falling back to the default JSON object it sends translation from another random store which we suppose might be the last store that sent email.

To Reproduce

import * as Sentry from '@sentry/node'
import i18next, { InitOptions, i18n } from 'i18next'
import Backend from 'i18next-fs-backend'
import enUS from '../lang/en-US.json'
import esES from '../lang/es-ES.json'
import frCA from '../lang/fr-CA.json'
import { StoreType } from '../models'
import { Localization } from '../models/localization'

const DEFAULT_LOCALE = 'en-US'

async function fetchStoreTranslations(
  storeId: string,
  language: string = DEFAULT_LOCALE,
): Promise<Record<string, unknown> | null> {
  if (!storeId) {
    return null
  }

  try {
    const localization = await Localization.get({
      store_id: storeId,
      locale: language,
    })

    return localization?.texts || null
  } catch (err) {
    Sentry.captureException(err)

    return null
  }
}

async function createInstance(): Promise<i18n> {
  const instance = i18next.createInstance()

  await instance.use(Backend).init({
    debug: process.env.NODE_ENV === 'development',
    lng: DEFAULT_LOCALE,
    fallbackLng: DEFAULT_LOCALE,
    resources: {
      'en-US': { translation: enUS },
      'fr-CA': { translation: frCA },
      'es-ES': { translation: esES },
    },
    returnNull: false,
    interpolation: {
      escapeValue: false,
    },
    saveMissing: true,
    missingKeyHandler: (lngs: readonly string[], ns: string, key: string) => {
      const message = `Missing translation key: ${key} for language: ${lngs.join(',')}`

      Sentry.captureMessage(message)

      if (process.env.NODE_ENV === 'development') {
        console.warn(message)
      }
    },
  } as InitOptions)

  return instance
}

export async function getTranslator(store: StoreType): Promise<i18n> {
  if (!store || !('store_id' in store) || !('language' in store)) {
    Sentry.captureMessage('Store object must have store_id and language properties, using defaults')
  }

  const instance = await createInstance()

  const storeTranslations = await fetchStoreTranslations(
    store.store_id,
    store.locales?.defaultLocale || store.language,
  )

  if (storeTranslations) {
    instance.addResourceBundle(store.language, 'translation', storeTranslations, true, true)
  }

  return instance
}

and used like this

const { t } = await getTranslator(store)

await firebase.notifyStoreCustomer(store, customer, {
  type: NOTIFICATION_TYPES.ORDERS,
  title: t('notifications.orders.completed.delivery.title'),
  body: t('notifications.orders.completed.delivery.title'),
})

Expected behavior

It should have sent the notification/email with the right text, previously we were using cloneInstance that might had issue but then we changed it to createInstance but we got the issues again now we're using namespaces we're not expecting issues but we don't know what was wrong in this impelmentation that I report.

Your Environment

  • runtime version: 20.19.4
  • i18next version: 25.3.2
  • i18next-fs-backend: 2.6.0
  • os: Linux
  • It's a webhook services and has a lot of load

theahmadzai avatar Sep 24 '25 14:09 theahmadzai

This seems to be a very strange setup... You are using i18next-fs-backend but also using addResourceBundle.... etc....

Can you please isolate only i18next related code? And please provide a complete but minimal reproducible example repository or codesandbox... not just code snippets... We need to be able to reproduce this locally...

After that we can see where the issue could be and try to address this... but we need a locally reproducible example first. Possible solutions could then be:

  • Remove Backend Plugin
  • Initialize with Complete Resources
  • Use Namespaces (as you mentioned trying)

adrai avatar Sep 24 '25 14:09 adrai

I have removed the backend plugin as we don't need it but I did not get the initialize with Complete Resources.

For reproducability I'm not able to reproduce it but we have been reported the scenarios, my assumption is as there is too much load on webhook service could it be causing some issues, still don't know but as I said we're not expecting issues with name spaces now but if any we'll report. Also since we're creating an instance for each store do you suggest to create a global instance and just put the each store's translation into it's own namespace? our only concern is the memory will grow too much over time, but currently our all texts combined might be under 10kb

Thanks

theahmadzai avatar Sep 24 '25 15:09 theahmadzai

not easy to say without being able to track the real root cause.... but given your current load (10kb total) and the concurrency issues you're experiencing, I'd actually try the global instance approach.

adrai avatar Sep 24 '25 17:09 adrai