Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nuxt Layers - Absolute "langDir" path #1890

Closed
3 of 4 tasks
bgrand-ch opened this issue Feb 25, 2023 · 13 comments
Closed
3 of 4 tasks

Nuxt Layers - Absolute "langDir" path #1890

bgrand-ch opened this issue Feb 25, 2023 · 13 comments

Comments

@bgrand-ch
Copy link

bgrand-ch commented Feb 25, 2023

Describe the feature

Hello,

Thanks for @nuxtjs/i18n!

I have a monorepo with multiple Nuxt projects. I use a "shared" Nuxt project to extend two other Nuxt projects.

The Nuxt layer tip for paths:

When using relative paths in nuxt.config file of a layer, (with exception of nested extends) they are resolved relative to user's project instead of the layer. As a workaround, use full resolved paths in nuxt.config.
https://nuxt.com/docs/guide/going-further/layers#relative-paths-and-aliases

I would like to use an absolute path with langDir in "shared" nuxt.config, but I have the following error message:

[vite] Internal server error: Failed to resolve import "../Volumes/MacExt/bgd/Documents/test/shared/i18n/en.json" from ".nuxt/i18n.options.mjs". Does the file exist?
  Plugin: vite:import-analysis
  File: /Volumes/MacExt/bgd/Documents/test/app/.nuxt/i18n.options.mjs:4:104
  2  |
  3  |  export const localeMessages = {
  4  |    "en": [{ key: "../Volumes/MacExt/bgd/Documents/test/shared/i18n/en.json", load: () => import("../Volumes/MacExt/bgd/Documents/test/shared/i18n/en.json" /* webpackChunkName: "lang_en_json_en_json" */) }],
     |                                                                                                         ^
  5  |    "fr": [{ key: "../Volumes/MacExt/bgd/Documents/test/shared/i18n/fr.json", load: () => import("../Volumes/MacExt/bgd/Documents/test/shared/i18n/fr.json" /* webpackChunkName: "lang_fr_json_fr_json" */) }],
  6  |  }
      at formatError (file:///Volumes/MacExt/bgd/Documents/test/node_modules/.pnpm/vite@4.1.4_sass@1.32.12/node_modules/vite/dist/node/chunks/dep-ca21228b.js:41418:46)
      at TransformContext.error (file:///Volumes/MacExt/bgd/Documents/test/node_modules/.pnpm/vite@4.1.4_sass@1.32.12/node_modules/vite/dist/node/chunks/dep-ca21228b.js:41414:19)
      at normalizeUrl (file:///Volumes/MacExt/bgd/Documents/test/node_modules/.pnpm/vite@4.1.4_sass@1.32.12/node_modules/vite/dist/node/chunks/dep-ca21228b.js:39706:33)
      at async TransformContext.transform (file:///Volumes/MacExt/bgd/Documents/test/node_modules/.pnpm/vite@4.1.4_sass@1.32.12/node_modules/vite/dist/node/chunks/dep-ca21228b.js:39840:47)
      at async Object.transform (file:///Volumes/MacExt/bgd/Documents/test/node_modules/.pnpm/vite@4.1.4_sass@1.32.12/node_modules/vite/dist/node/chunks/dep-ca21228b.js:41685:30)
      at async loadAndTransform (file:///Volumes/MacExt/bgd/Documents/test/node_modules/.pnpm/vite@4.1.4_sass@1.32.12/node_modules/vite/dist/node/chunks/dep-ca21228b.js:39479:29)

The langDir dynamically adds ../ before path, presumably to get out of the .nuxt folder.

@kazupon Could you implement a different behavior when the path is absolute please? 🙏

"shared" nuxt.config content:

import dotenv from 'dotenv'
import { fileURLToPath, URL } from 'url'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { QuasarResolver } from 'unplugin-vue-components/resolvers'
import { quasar } from '@quasar/vite-plugin'
import { createResolver } from '@nuxt/kit'

const env = dotenv.config({
  path: `../.env.${process.env.STAGE}`
})

// https://nuxt.com/docs/guide/going-further/layers#relative-paths-and-aliases
const { resolve } = createResolver(import.meta.url)

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  runtimeConfig: {
    public: !env.error ? env.parsed : {}
  },
  modules: [
    [
      '@pinia/nuxt',
      {
        autoImports: [
          'defineStore',
          'storeToRefs'
        ]
      }
    ],
    [
      '@nuxtjs/i18n',
      {
        lazy: true,
        langDir: resolve('./i18n'), // FIX: https://github.com/nuxt-modules/i18n/issues/1890
        strategy: 'prefix',
        defaultLocale: 'en',
        locales: [
          {
            code: 'en',
            file: 'en.json'
          },
          {
            code: 'fr',
            file: 'fr.json'
          }
        ]
      }
    ]
  ],
  vite: {
    plugins: [
      AutoImport({
        resolvers: [QuasarResolver()]
      }),
      Components({
        resolvers: [QuasarResolver()]
      }),
      quasar({
        sassVariables: resolve('./assets/styles/quasar-variables.scss')
      })
    ]
  },
  css: [
    '@quasar/extras/roboto-font/roboto-font.css',
    '@quasar/extras/material-icons/material-icons.css',
    'quasar/src/css/index.sass'
  ],
  imports: {
    dirs: [
      'models',
      'services',
      'stores'
    ]
  },
  alias: {
    'assets': fileURLToPath(new URL('./assets', import.meta.url)),
    'i18n': fileURLToPath(new URL('./i18n', import.meta.url)),
    'types': fileURLToPath(new URL('./types', import.meta.url))
  },
  ssr: false
})

Additional information

  • Would you be willing to help implement this feature? (If the path of the concerned file is provided)
  • Could this feature be implemented as a module?

Final checks

@bgrand-ch bgrand-ch changed the title Nuxt Layers - Absolute "langDir" Nuxt Layers - Absolute "langDir" path Feb 25, 2023
@lukaszflorczak
Copy link

lukaszflorczak commented Feb 25, 2023

Hey @bgrand-ch

I also have i18n in the shared layer. So maybe my solution will be helpful for you. I created an init module in my layer where I initialize i18n settings (and some other stuff). In my case, I added a path by resolver with config for the vueI18n param. But probably you can use a similar solution to add your langDir path:

import { createResolver, defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
  async setup(options, nuxt) {
    const resolver = createResolver(import.meta.url)
    nuxt.options.i18n.vueI18n = resolver.resolve('../locale/config/vue-i18n.ts') // It's the  path in my shared layer
  },
})

@bgrand-ch
Copy link
Author

Hello @lukaszflorczak, thanks a lot for your quick response! I will test your solution soon 🤞

@lukaszflorczak
Copy link

It's based on that: https://nuxt.com/docs/guide/going-further/layers#relative-paths-and-aliases

@BobbieGoede
Copy link
Collaborator

It looks like langDir is resolved relative to srcDir, not sure how that works when defined in a layer.

For reference:

const langPath = isString(options.langDir) ? resolve(nuxt.options.srcDir, options.langDir) : null

@Techbinator
Copy link

Techbinator commented Mar 2, 2023

I found an workaround ... it has a small disadvantage(but at least in my case until this issue is fixed is ok) that if you are using layers and try to add translations files in the Lang folder in the "non shared" package that contains this config it will not work(basically all translation files would need to be added in the shared package or this needs to be duplicated on all packages in case you want translation folder/files per package

import { createResolver } from '@nuxt/kit';

// https://nuxt.com/docs/guide/going-further/layers#relative-paths-and-aliases
const { resolve } = createResolver(import.meta.url);
export default defineNuxtConfig({
  modules: [
        '@nuxtjs/i18n',
        {
          strategy: 'prefix',
          defaultLocale: 'fr',
          locale: 'fr',
          legacy: true,
          locales: [{ code: 'fr' }, { code: 'en' }],
          vueI18n: {
            fallbackLocale: 'fr',
            legacy: true,
            messages: {
              en: require(resolve('./lang/en-US.json')),
              fr: require(resolve('./lang/fr-FR.json')),
            },
          },
        },
      ]
  ]
})

The files are fetched and also the url prefix works as intended

@BobbieGoede
Copy link
Collaborator

Using the following config as your i18n layer should work, though I don't think it looks like a clean solution. It changes the langDir to be relative to the project that uses the layer. The first layer is the project layer as described here.

import { createResolver } from '@nuxt/kit'

const { resolve } = createResolver(import.meta.url)

export default defineNuxtConfig({
  modules: [
    (_, nuxt) => {
      const projectLayer = nuxt.options._layers[0]
      // @ts-ignore
      const relativeToProject = '.' + resolve(nuxt.options.i18n.langDir).replace(projectLayer.config.rootDir, '')

      // @ts-ignore
      nuxt.options.i18n.langDir = relativeToProject
    },
    '@nuxtjs/i18n',
  ],
  i18n: {
    strategy: 'prefix',
    defaultLocale: 'en',
    lazy: true,
    langDir: './lang',
    locales: [{ code: 'en', file: 'en.json' }],
  },
})

@Techbinator
Copy link

Techbinator commented Mar 3, 2023

Using the following config as your i18n layer should work, though I don't think it looks like a clean solution. It changes the langDir to be relative to the project that uses the layer. The first layer is the project layer as described here.

import { createResolver } from '@nuxt/kit'

const { resolve } = createResolver(import.meta.url)

export default defineNuxtConfig({
  modules: [
    (_, nuxt) => {
      const projectLayer = nuxt.options._layers[0]
      // @ts-ignore
      const relativeToProject = '.' + resolve(nuxt.options.i18n.langDir).replace(projectLayer.config.rootDir, '')

      // @ts-ignore
      nuxt.options.i18n.langDir = relativeToProject
    },
    '@nuxtjs/i18n',
  ],
  i18n: {
    strategy: 'prefix',
    defaultLocale: 'en',
    lazy: true,
    langDir: './lang',
    locales: [{ code: 'en', file: 'en.json' }],
  },
})

i tried your solution and it seems that the translations are only fetched from the "main" package.

For example i have
shared that is then imported as dependency in package-1, 'package-2' etc.
Then package-1, 'package-2' are dependencies of main.

main <- package1  <- shared
     <- package2  <- shared
         ....

It seems that with your approach even if i add the config in the shared the only lang dir json files that are taken in consideration are the ones in the main file. if i add keys in the lang files in the shared or package-1, 'package-2' etc all of them are ignored.

@BobbieGoede
Copy link
Collaborator

@Techbinator I think I misunderstood, I thought your use case was a shared layer containing all i18n configuration and translations. It sounds like your goal is to combine/merge multiple langDir directories is that correct?

Just to get a clear idea of the use case, does the following look like what you want to achieve?

[main]/langDir
├── en.json
└── fr.json

[shared]/langDir
├── en.json
├── nl.json
└── fr.json

intended result
├── en.json (merged)
├── nl.json (shared)
└── fr.json (merged)
// [main]/langDir/en.json
{
  "title": "foo"
}

// [shared]/langDir/en.json
{
  "description": "bar"
}

// intended result en.json
{
  "title": "foo",
  "description": "bar"
}

I'm not sure if merging/combining files is currently supported/possible. If not, I think there could be a possible workaround by loading the files and using the i18n:extend-messages hook in a module in each layer.

I can imagine lazy loading wouldn't apply to such a workaround though, for that to work it may be required to merge each file and write the resulting file.

@Techbinator
Copy link

@BobbieGoede that is more or less what i would like to do.
Thank you very much for the hint, i will give it a try, it seems like a good starting point.

@kazupon kazupon added the layers label Mar 3, 2023
@BobbieGoede
Copy link
Collaborator

BobbieGoede commented Mar 4, 2023

@Techbinator maybe this workaround works better for your use case.

import type { LocaleObject } from 'vue-i18n-routing'
import pathe from 'pathe'

export default defineNuxtConfig({
  modules: [
    (_, nuxt) => {
      const projectLayer = nuxt.options._layers[0]
      const projectI18n = projectLayer.config.i18n

      if (projectI18n == null) throw new Error('Project layer `i18n` configuration is required')
      if (projectI18n.langDir == null) throw new Error('Project layer `i18n.langDir` is required')

      const getLocaleFiles = (locale: LocaleObject): string[] => {
        if (locale.file != null) return [locale.file]
        if (locale.files != null) return locale.files
        return []
      }

      type FileRelativeOptions = {
        projectLangDir: string
        layerLangDir: string
        layerRootDir: string
      }
      const filesToRelative = (files: string[], opts: FileRelativeOptions) =>
        files.map((file) =>
          pathe.relative(opts.projectLangDir, pathe.resolve(opts.layerRootDir, opts.layerLangDir, file))
        )

      const mergedLocales: LocaleObject[] = []
      for (const layer of nuxt.options._layers) {
        if (layer.config.i18n?.locales == null) continue
        if (layer.config.i18n?.langDir == null) continue

        for (const locale of layer.config.i18n.locales) {
          if (typeof locale === 'string') continue

          const { file, files, ...entry } = locale
          const localeEntry = mergedLocales.find((x) => x.code === locale.code)

          const fileEntries = getLocaleFiles(locale)
          const relativeFiles = filesToRelative(fileEntries, {
            projectLangDir: projectI18n.langDir,
            layerLangDir: layer.config.i18n.langDir,
            layerRootDir: layer.config.rootDir,
          })

          if (localeEntry == null) {
            mergedLocales.push({ ...entry, files: relativeFiles })
          } else {
            localeEntry.files = [...relativeFiles, ...localeEntry.files]
          }
        }
      }

      // @ts-ignore
      nuxt.options.i18n.locales = mergedLocales
    },
    '@nuxtjs/i18n',
  ],
  i18n: {
    strategy: 'prefix',
    defaultLocale: 'en',
    lazy: true,
    langDir: 'lang',
    locales: [{ code: 'en', file: 'en.json' }],
  },
})

This approach tries to merge the locale entries of all layers, by taking the file or files value of each locale and making it relative to the langDir of the project layer and setting the relative paths to files of the locale object.

What it does:

// project layer i18n
{
  langDir: 'lang',
  locales: [{ code: 'fr', file: 'fr.json'}],
}

// shared layer i18n
{
  langDir: 'lang', // layers/shared-i18n/lang
  locales: [{ code: 'fr', file: 'fr.json'}],
}

// transformed result by module
{
  langDir: 'lang',
  locales: [{
    code: 'fr', 
    files: [
      '../layers/shared-i18n/lang/fr.json',
      'fr.json',
    ], 
  }],
}

Be aware that the workaround doesn't take into account that layers may already have arrays of files, but that can easily be supported. I edited the workaround to support both arrays and strings, and fixed an issue how paths were resolved.

@bgrand-ch
Copy link
Author

Thanks @Techbinator and huge thanks @BobbieGoede

@bgrand-ch
Copy link
Author

This feature need to be integrated natively to Nuxt i18n, it's awesome! 🎉

@BobbieGoede
Copy link
Collaborator

@bgrand-ch it's implemented in the module now that #1925 has merged!

@kazupon kazupon closed this as completed Mar 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants