Nuxt 3 i18n on server&client
Tags: nuxt, vue, typescript, webdev
Published: 2023-07-09 14:58:24

There's a lot of tutorials on how to setup multi-lingual support on client-side. But I didn't find any tutorial for doing it on nuxt server-side so I did it manually. So there it is!

## Client-side setup

First of all let's look at how I did it on client-side

1. Installing i18n module dependency

   I used edge version

   Manually adding to `package.json`:

   ```json
   {
     "devDependencies": {
       "@nuxtjs/i18n": "npm:@nuxtjs/i18n-edge"
     }
   }
   ```

   then `npm i`

2. Configuring in `nuxt.config.ts`

   ```typescript
   import locales from './locales'

   export default defineNuxtConfig({
     ...
     modules: ['@nuxtjs/i18n'],
     i18n: {
       lazy: true,
       langDir: 'locales',
       locales,
       defaultLocale: 'en',
       detectBrowserLanguage: false,
       strategy: 'no_prefix',
     },
     ...
   })
   ```

   I have chosen `locales` folder for my .json translations and also have dedicated `locales.ts` file to list them:

   ```typescript
   export default [
     {
       code: 'en',
       name: 'English',
     },
     {
       code: 'ru',
       name: 'Русский',
     },
   ].map(lang => ({ file: lang.code + '.json', ...lang }))
   ```

   Example json translation files:

   `locales/en.json`

   ```json
   {
     "pageTitle": "Example page",
     "unknownError": "Unknown error happend!"
   }
   ```

   `locales/ru.json`

   ```json
   {
     "pageTitle": "Страница для примера",
     "unknownError": "Произошла неизвестная ошибка!"
   }
   ```

   I extracted list of locales as `locales.ts` to dedicated file because we will also use them in server-side

   As you can see I have disabled `detectBrowserLanguage` and `strategy` to disable auto language detect and prefixes that allows to, for example, redirect from /en to / with the language set as English

   The reason why is because right now it's buggy in nuxt and can't even detect my browser language so I implemented it on my own what you will see later

3. In `app.vue` at the very end of script setup:

   ```typescript
   const { t } = useI18n()
   await useConfigureI18n()

   useHead({
     title: computed(() => t('pageTitle')),
   })
   ```

   I called my custom `useConfigureI18n` composable to detect browser's language and load/save it from/to cookie

   Then I already used `t` to set page title

   Btw I strongly recommend using [I18n Ally extension](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally) that can show translations in your code, automatically translate them for you using google translation for free and also find and extract raw strings into translations

4. `composables/useConfigureI18n.ts`:

   ```typescript
   export default async () => {
     const { getBrowserLocale, setLocale, locale } = useI18n()

     const savedLang = useCookie('lang')

     watch(locale, newLocale => {
       if (savedLang.value != newLocale) savedLang.value = newLocale
     })

     await setLocale(savedLang.value || getBrowserLocale() || 'en')
   }
   ```

   Here I automatically detect browser language and syncronize it with cookie `lang`

5. Language selector

   We already can use `t` to use translated messages but we also need to let users to select their language

   ```javascript
   <template>
       <select v-model="locale">
       <option
           v-for="locale in locales"
           :key="locale.code"
           :label="locale.name"
           :value="locale.code"
       />
       </select>
   </template>

   <script setup lang="ts">
   import locales from '@/locales'
   const { locale } = useI18n()
   </script>
   ```

Everything is ready on the client!

## Server-side setup

For using I18n I decided to create my own middleware that will add `$t` in event context for event handlers in our endpoints

`server/middleware/i18n.ts`:

```typescript
import { createI18n } from 'vue-i18n'
import locales from '../../locales'
import fs from 'fs-extra'

const i18n = createI18n({
  fallbackLocale: 'en',
}).global

for (const locale of locales) {
  i18n.setLocaleMessage(locale.code, fs.readJSONSync('locales/' + locale.file))
}

export default defineEventHandler(e => {
  e.context.$t = (key: string) =>
    i18n.t(key, getCookie(e, 'lang') || i18n.fallbackLocale.toString())
})

declare module 'h3' {
  interface H3EventContext {
    $t: typeof i18n.t
  }
}
```

We already have user's language in cookie so we can use it to detect user's locale. So I created my wrapper of default `t` that already passes user's locale

I used `fs-extra` module. You can install it or use default `fs` and then `JSON.parse(fs.readFileSync('locales/' + locale.file))`

And also we extending default `H3EventContext` interface so TypeScript will know that we have `$t`

Then we can use it in our endpoints like so:

```typescript
export default defineEventHandler(async e => {
  return e.context.$t('unknownError')
})
```

You can check code in my [cool pet project](https://github.com/AriesAlex/ariex-custom-tabpage) to compare

That's all! We finally have i18n in our app. Happy coding and using machine translation to make users tear their hair out!