s00d / nuxt-i18n-micro

Nuxt I18n Micro is a fast, simple, and lightweight internationalization (i18n) module for Nuxt
https://s00d.github.io/nuxt-i18n-micro/
MIT License
87 stars 11 forks source link

No access of i18n instance in nuxt plugins #63

Closed fabkho closed 18 hours ago

fabkho commented 3 days ago

Hi hi, i am currently in the process of migrating a huge application to your very promising package and encountered a major issue.

Describe the bug The provided API ($t, $getLocale, $localePath etc.) cannot be used within Nuxt plugins, resulting in a runtime error.

To Reproduce Try to use any nuxt-i18n-micro composable within the plugin:

export default defineNuxtPlugin((nuxtApp) => {
  const { $getLocale } = nuxtApp.$i18n
  const locale = $getLocale() // error here
})

Error in console:

[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function. This is probably not a Nuxt bug. Find out more at https://nuxt.com/docs/guide/concepts/auto-imports#vue-and-nuxt-composables

Expected behavior The nuxt-i18n-micro composables should be accessible within Nuxt plugins, as plugins are a valid context for accessing Nuxt instance functionality. This would allow proper initialization and setup of i18n functionality at the plugin level.

Environment Information

s00d commented 3 days ago

Hello. You are not using i18n correctly; it should be done like this:

export default defineNuxtPlugin((_nuxtApp) => {
  const { $t, $getLocale } = useI18n() // or const { $t, $getLocale } = useNuxtApp()
  const translatedMessage = $t('test_key')
  const locale = $getLocale() // error here

  console.log(translatedMessage, locale)
})

Also, while testing, I found a bug; please use version v1.32.3.

fabkho commented 3 days ago

Hello, thanks for the fast reply. This does not make a difference in my use case. I think i was not specific enough.

The issue occurs when passing the Nuxt context from a plugin to utility functions. Here's the complete setup to demonstrate:

// plugins/initAuth.ts
export default defineNuxtPlugin((_nuxtApp) => {
  const nuxtApp = useNuxtApp()
  // This works fine - we're in plugin context
  console.log(nuxtApp.$t('some.key'))

  try {
    await authApi.someRequest()
  } catch (e) {
    handleError(nuxtApp, e)
  }
})

// utils/error-handler.ts
export function handleError(
  nuxtApp: NuxtApp,
  e: AxiosError | Error | null,
) {
// This fails - even though we're passing the same nuxtApp context
  showError({
    title: nuxtApp.$t('common.errors.unknown.title'), // Throws composable error
    message: nuxtApp.$t('common.errors.unknown.message')
  })
}

I get the error:

[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function.

This is just one example - the same issue occurs in any utility function, service, or helper that receives the Nuxt context as a parameter from a plugin. This worked perfectly fine with @nuxtjs/i18n package.

Is there a proper way to access i18n functionality in utility functions and services that receive the Nuxt context from plugins?

s00d commented 2 days ago

Now I understand the problem; just don’t use the function call like this:

const val = nuxtApp.$t('common.errors.unknown.title')
showError({
    title: val
})

Instead, you need to use runWithContext, for example:

await nuxtApp.runWithContext(() => $t('test_key'))

I’ll fix this issue later.

s00d commented 2 days ago

I slightly changed the logic in v1.32.4. Now this issue should occur less frequently, but it still can't be completely eliminated. More details are described here: https://nuxt.com/docs/api/composables/use-nuxt-app#runwithcontext.

fabkho commented 2 days ago

Very nice! I got through the auth process of my app 🤝 I refactored most usages of $t in my helpers to just return the translation key and then i call $t in the actual component. Also one important detail for the docs may be that calling $t after an async request can break.

Here's the problematic scenario:

// plugins/initAuth.ts
export default defineNuxtPlugin((_nuxtApp) => {
  const nuxtApp = useNuxtApp()

  try {
    await authService.init(nuxtApp)
  } catch (e) {
    handleError(nuxtApp, e)
  }
})

// services/authService.ts
export async function init(nuxtApp: NuxtApp) {
  try {
    await apiClient.get('/auth/init')
  } catch (e) {
    // This breaks because $t is called after async operation
    showError({
      title: nuxtApp.$t('common.errors.unknown.title'),
      message: nuxtApp.$t('common.errors.unknown.message')
    })
  }
}

BUT if i do the async request inside the plugin itself i still can use the $t after the async call, so this would work:

export default defineNuxtPlugin((_nuxtApp) => {
  const nuxtApp = useNuxtApp()

  try {
    const response = await apiClient.get('/auth/init')
    // This works fine because the async call is directly in the plugin
    showError({
      title: nuxtApp.$t('common.errors.unknown.title'),
      message: nuxtApp.$t('common.errors.unknown.message')
    })
  } catch (e) {
    // Also works here
    showError({
      title: nuxtApp.$t('common.errors.unknown.title'),
      message: nuxtApp.$t('common.errors.unknown.message')
    })
  }
})

The solution for services is to return translation keys instead:

showError({
  title: 'common.errors.unknown.title',
  message: 'common.errors.unknown.message',
  i18n: true
})
fabkho commented 2 days ago

Man, the whole composable situation in the nuxt ecosytem is such a mess 😶‍🌫️

s00d commented 2 days ago

Also one important detail for the docs may be that calling $t after an async request can break.

added

Man, the whole composable situation in the nuxt ecosytem is such a mess 😶‍🌫️

Yes, it's a relevant issue, but over time you get used to it and stop encountering it.