Guide

Storybook

Render components that use $t in Storybook for Nuxt by serving pre-merged translation payloads as static files.

Storybook for Nuxt (@nuxtjs/storybook) runs your components with the Nuxt app, but without the Nitro server that normally serves translation payloads. To make $t work in stories, write the payloads Storybook would fetch into a static folder before it starts. A complete project is in s00d/nuxtjs-storybook-i18n-micro.

Nuxt config

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-i18n-micro', '@nuxtjs/storybook'],
  i18n: {
    locales: [
      { code: 'en', iso: 'en-US', displayName: 'English' },
      { code: 'de', iso: 'de-DE', displayName: 'Deutsch' },
    ],
    defaultLocale: 'en',
  },
})

Write the payloads

The browser loads /_locales/<page>/<locale>/data.json. Stories have no page route, so they load the index payload. Build one per locale that holds every page's keys plus the root keys:

// .storybook/main.ts
import fs from 'node:fs'
import path from 'node:path'
import type { StorybookConfig } from '@storybook-vue/nuxt'
import { deepMergeTranslationsRecursive } from '@i18n-micro/utils/deep-merge'

type Tree = Record<string, unknown>

const localesDir = path.join(__dirname, '../locales')
const outDir = path.join(__dirname, '../storybook_locales/_locales/index')

const readJson = (file: string): Tree => JSON.parse(fs.readFileSync(file, 'utf-8'))

/** Every `<locale>.json` below `dir`, deep-merged. */
function collect(dir: string, locale: string): Tree {
  if (!fs.existsSync(dir)) return {}
  return fs.readdirSync(dir, { withFileTypes: true }).reduce<Tree>((acc, entry) => {
    const full = path.join(dir, entry.name)
    if (entry.isDirectory()) return deepMergeTranslationsRecursive(acc, collect(full, locale))
    return entry.name === `${locale}.json` ? deepMergeTranslationsRecursive(acc, readJson(full)) : acc
  }, {})
}

for (const file of fs.readdirSync(localesDir).filter((f) => f.endsWith('.json'))) {
  const locale = path.basename(file, '.json')
  // root keys first, page keys win (as in the module's build)
  const merged = deepMergeTranslationsRecursive(readJson(path.join(localesDir, file)), collect(path.join(localesDir, 'pages'), locale))
  fs.mkdirSync(path.join(outDir, locale), { recursive: true })
  fs.writeFileSync(path.join(outDir, locale, 'data.json'), JSON.stringify(merged))
}

const config: StorybookConfig = {
  stories: ['../components/**/*.stories.@(ts|tsx|mdx)'],
  staticDirs: ['../storybook_locales'], // serves /_locales/index/<locale>/data.json
  framework: { name: '@storybook-vue/nuxt', options: {} },
}

export default config

deepMergeTranslationsRecursive comes from @i18n-micro/utils (add it to your dev dependencies); it merges nested keys at any depth, like the module's own build. Add storybook_locales/ to .gitignore.

Testing your app

Translation files

@nuxtjs/storybook