adonisjs / inertia

Official Inertia.js adapter for AdonisJS
MIT License
65 stars 5 forks source link

Support i18n helpers with Inertia #21

Open axelvaindal opened 2 months ago

axelvaindal commented 2 months ago

Package version

1.0.0-25

Describe the bug

Hello,

I'm using Inertia with AdonisJS 6 and I need to create an internationalized version of the website. Notably, most of the static content must be translated.

With AdonisJS + Edge, it's fairly simple as all the code is processed client side.

import { HttpContext } from '@adonisjs/core/http'

export default class PostsController {
  async store({ i18n, session }: HttpContext) {
    session.flash('success', {
      message: i18n.t('post.created')
    })
  }
}

// Inside edge template
<h1> {{ t('messages.heroTitle') }} </h1>

However though, when I create a wrapper around i18n to be used in my React component, it imports the server side code which is not good.

Is there any way to have a t() helper resolving the translations the same way we can do this in Edge ?

If not, can you pinpoint me in the right direction in creating such a thing ?

Thanks 🙏.

Reproduction repo

No response

miguelmanteigueiro commented 1 month ago

Hi @axelvaindal, have you found a solution? Looking forward!

keil0 commented 1 month ago

@miguelmanteigueiro the best solution I've found is to pass the i18n instance via Inertia's shared props.

# config/inertia.ts

import { defineConfig } from '@adonisjs/inertia'

export default defineConfig({
  /**
   * Path to the Edge view that will be used as the root view for Inertia responses
   */
  rootView: 'inertia_layout',

  /**
   * Data that should be shared with all rendered pages
   */
  sharedData: {
    user: (ctx) => ctx.auth?.user?.serialize({ fields: ['id', 'email'] }),
    errors: (ctx) => ctx.session.flashMessages.get('errors'),
    notification: (ctx) => ctx.session.flashMessages.get('notification'),
    i18n: (ctx) => {
      return {
        ...ctx.i18n,
        locale: ctx.i18n.locale,
      }
    },
  },

  /**
   * Options for the server-side rendering
   */
  ssr: {
    enabled: true,
    entrypoint: 'inertia/app/ssr.tsx',
  },
})

I'll try to optimize that later.

miguelmanteigueiro commented 1 month ago

@miguelmanteigueiro the best solution I've found is to pass the i18n instance via Inertia's shared props.


# config/inertia.ts

import { defineConfig } from '@adonisjs/inertia'

export default defineConfig({

  /**

   * Path to the Edge view that will be used as the root view for Inertia responses

   */

  rootView: 'inertia_layout',

  /**

   * Data that should be shared with all rendered pages

   */

  sharedData: {

    user: (ctx) => ctx.auth?.user?.serialize({ fields: ['id', 'email'] }),

    errors: (ctx) => ctx.session.flashMessages.get('errors'),

    notification: (ctx) => ctx.session.flashMessages.get('notification'),

    i18n: (ctx) => {

      return {

        ...ctx.i18n,

        locale: ctx.i18n.locale,

      }

    },

  },

  /**

   * Options for the server-side rendering

   */

  ssr: {

    enabled: true,

    entrypoint: 'inertia/app/ssr.tsx',

  },

})

I'll try to optimize that later.

Thanks! And how would one change the i18n in a view (e.g., Index.svelte) and change the shared prop? I've tried finding that information in Inertia documentation and Adonis but no cigar.

keil0 commented 1 month ago

@miguelmanteigueiro I use a combination of custom header and cookie in a middleware :

# app/middleware/detect_user_locale_middleware.ts

import { I18n } from '@adonisjs/i18n'
import i18nManager from '@adonisjs/i18n/services/main'
import type { NextFn } from '@adonisjs/core/types/http'
import { type HttpContext, RequestValidator } from '@adonisjs/core/http'

export default class DetectUserLocaleMiddleware {
  static {
    RequestValidator.messagesProvider = (ctx) => {
      return ctx.i18n.createMessagesProvider()
    }
  }

  protected getRequestLocale(ctx: HttpContext) {
    // Retrieve supported languages
    const supportedLocales = i18nManager.supportedLocales()

    // First, try to read the language from a custom header
    const customHeaderLocale = ctx.request.header('X-User-Language')

    // If the custom header exists and is a valid locale, use it
    if (customHeaderLocale && supportedLocales.includes(customHeaderLocale)) {
      return customHeaderLocale
    }

    // Then check the cookie
    const cookieLocale = ctx.request.cookie('user-locale')
    if (cookieLocale && supportedLocales.includes(cookieLocale)) {
      return cookieLocale
    }

    // Fallback to the Accept-Language header if no valid custom header or cookie
    const userLanguages = ctx.request.languages()
    return i18nManager.getSupportedLocaleFor(userLanguages)
  }

  async handle(ctx: HttpContext, next: NextFn) {
    const language = this.getRequestLocale(ctx)

    // Update the cookie if necessary
    if (!ctx.request.cookie('user-locale') || ctx.request.cookie('user-locale') !== language) {
      ctx.response.cookie('user-locale', language, {
        httpOnly: true,
        path: '/',
        maxAge: 60 * 60 * 24 * 30, // Example duration: 30 days
        sameSite: true, // Improve cookie security
      })
    }

    // Update the locale for this request
    ctx.i18n = i18nManager.locale(language || i18nManager.defaultLocale)
    ctx.containerResolver.bindValue(I18n, ctx.i18n)

    return next()
  }
}

/**
 * Notify TypeScript about i18n property
 */
declare module '@adonisjs/core/http' {
  export interface HttpContext {
    i18n: I18n
  }
}