Skip to content

Commit

Permalink
feat: layer vue i18n config merging (#2358)
Browse files Browse the repository at this point in the history
* feat: layer vue i18n config merging

* test: update snapshot

* fix: layer vuei18n configurations merge order

* test: disable jit compilation for vuei18n layer test

* docs: describe VueI18n option merging on layers page
  • Loading branch information
BobbieGoede authored Sep 4, 2023
1 parent bdc2991 commit c411518
Show file tree
Hide file tree
Showing 15 changed files with 207 additions and 79 deletions.
3 changes: 2 additions & 1 deletion docs/content/2.guide/15.layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ Mixing locale configuration such as lazy loading objects and strings may not wor


## Pages & Routing

Pages in the `pages` directory from extended layers will automatically be merged and have i18n support as if they were part of your project.

Page routes defined in `i18n.pages` in each layer configuration will be merged as well.
Expand Down Expand Up @@ -114,3 +113,5 @@ This example would result in the project supporting two locales (`en`, `nl`) and
::
::

## VueI18n options
Options defined in VueI18n configuration files within layers are merged and override each other according to their layers priority.
15 changes: 14 additions & 1 deletion playground/layers/i18n-layer/i18n.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ export default defineI18nConfig(() => {
ja: {
layerText: 'これはマージされたロケールキーです'
}
}
},
modifiers: {
// @ts-ignore
pascalCase: (str: string) =>
str
.split(' ')
.map(s => s.slice(0, 1).toUpperCase() + s.slice(1))
.join('')
},
missingWarn: false,
fallbackWarn: false,
warnHtmlMessage: false,
silentFallbackWarn: true,
silentTranslationWarn: true
}
})
14 changes: 13 additions & 1 deletion playground/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,20 @@
"about": "About this site"
}
},
"snakeCaseText": "@.snakeCase:{'pages.title.about'}",
"pascalCaseText": "@.pascalCase:{'pages.title.about'}",
"welcome": "Welcome",
"hello": "Hello {name} !",
"tag": "<p>Tag</p>",
"items": [{ "name": "apple" }, { "name": "banana" }, { "name": "strabelly" }]
"items": [
{
"name": "apple"
},
{
"name": "banana"
},
{
"name": "strawberry"
}
]
}
2 changes: 2 additions & 0 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ definePageMeta({
<div>
<h1>Demo: Nuxt 3</h1>
<h2>{{ $t('hello', { name: 'nuxt3' }) }}</h2>
<p>{{ $t('snakeCaseText') }}</p>
<p>{{ $t('pascalCaseText') }}</p>
<p>{{ $t('bar.buz', { name: 'buz' }) }}</p>
<h2>Pages</h2>
<nav>
Expand Down
7 changes: 6 additions & 1 deletion playground/vue-i18n.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,10 @@ export default defineI18nConfig(() => ({
modifiers: {
// @ts-ignore
snakeCase: (str: string) => str.split(' ').join('-')
}
},
missingWarn: true,
fallbackWarn: true,
warnHtmlMessage: true,
silentFallbackWarn: false,
silentTranslationWarn: false
}))
19 changes: 18 additions & 1 deletion specs/fixtures/layer_consumer/i18n.config.ts
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
export default {}
export default {
messages: {
nl: {
about: 'Over deze site',
snakeCaseText: "@.snakeCase:{'about'}",
pascalCaseText: "@.pascalCase:{'about'}"
},
fr: {
about: 'À propos de ce site',
snakeCaseText: "@.snakeCase:{'about'}",
pascalCaseText: "@.pascalCase:{'about'}"
}
},
modifiers: {
// @ts-ignore
snakeCase: (str: string) => str.split(' ').join('-')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default {
messages: {
en: {
about: 'Should be overridden'
}
},
modifiers: {
// @ts-ignore
pascalCase: (str: string) =>
str
.split(' ')
.map(s => s.slice(0, 1).toUpperCase() + s.slice(1))
.join('')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// https://nuxt.com/docs/guide/directory-structure/nuxt.config
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: ['fr', 'nl', 'en'],
defaultLocale: 'nl',
detectBrowserLanguage: false,
vueI18n: './i18n.config.ts'
}
})
16 changes: 15 additions & 1 deletion specs/fixtures/layer_consumer/layer-simple/i18n.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
export default {
messages: {
fr: {
thanks: 'Merci!'
thanks: 'Merci!',
about: 'Should be overridden'
},
nl: {
thanks: 'Bedankt!'
},
en: {
about: 'About this site',
snakeCaseText: "@.snakeCase:{'about'}",
pascalCaseText: "@.pascalCase:{'about'}"
}
},
modifiers: {
// @ts-ignore
pascalCase: (str: string) =>
str
.split(' ')
.map(s => s.slice(0, 1).toUpperCase() + s.slice(1))
.join('')
}
}
2 changes: 1 addition & 1 deletion specs/fixtures/layer_consumer/layer-simple/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: ['fr', 'nl'],
locales: ['fr', 'nl', 'en'],
defaultLocale: 'nl',
detectBrowserLanguage: false,
vueI18n: './i18n.config.ts'
Expand Down
2 changes: 1 addition & 1 deletion specs/fixtures/layer_consumer/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// https://nuxt.com/docs/guide/directory-structure/nuxt.config
export default defineNuxtConfig({
extends: ['./layer-simple'],
extends: ['./layer-simple', './layer-simple-secondary'],
modules: ['@nuxtjs/i18n']
})
4 changes: 3 additions & 1 deletion specs/fixtures/layer_consumer/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const localePath = useLocalePath()
const i18nHead = useLocaleHead({ addSeoAttributes: { canonicalQueries: ['page'] } })
useHead({
title: t('home'),
title: 'Home',
htmlAttrs: {
lang: i18nHead.value.htmlAttrs!.lang
},
Expand All @@ -20,6 +20,8 @@ useHead({
<template>
<div>
<div id="layer-message">{{ $t('thanks') }}</div>
<div id="snake-case">{{ $t('snakeCaseText') }}</div>
<div id="pascal-case">{{ $t('pascalCaseText') }}</div>
<LangSwitcher />
<section>
<strong><code>useHead</code> with <code>useLocaleHead</code></strong
Expand Down
28 changes: 26 additions & 2 deletions specs/layers/layers_vuei18n_options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,38 @@ import { getText } from '../helper'
describe('nuxt layers vuei18n options', async () => {
await setup({
rootDir: fileURLToPath(new URL(`../fixtures/layer_consumer`, import.meta.url)),
browser: true
browser: true,
nuxtConfig: {
overrides: {
compilation: {
jit: false
}
}
}
})

test('layer vueI18n options provides `nl` message', async () => {
const home = url('/')
const page = await createPage(undefined) // set browser locale
const page = await createPage(undefined)
await page.goto(home)

expect(await getText(page, '#layer-message')).toEqual('Bedankt!')
})

test('layer vueI18n options properties are merge and override by priority', async () => {
const home = url('/')
const page = await createPage(undefined)
await page.goto(home)

expect(await getText(page, '#snake-case')).toEqual('Over-deze-site')
expect(await getText(page, '#pascal-case')).toEqual('OverDezeSite')

await page.click(`#set-locale-link-en`)
expect(await getText(page, '#snake-case')).toEqual('About-this-site')
expect(await getText(page, '#pascal-case')).toEqual('AboutThisSite')

await page.click(`#set-locale-link-fr`)
expect(await getText(page, '#snake-case')).toEqual('À-propos-de-ce-site')
expect(await getText(page, '#pascal-case')).toEqual('ÀProposDeCeSite')
})
})
91 changes: 48 additions & 43 deletions src/gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,60 +155,65 @@ export function generateLoaderOptions(
genCodes += ` const ${rootKey} = Object({})\n`
for (const [key, value] of Object.entries(rootValue)) {
if (key === 'vueI18n') {
genCodes += ` const vueI18nConfigLoader = async (loader) => {
genCodes += ` const vueI18nConfigLoader = async loader => {
const config = await loader().then(r => r.default || r)
return typeof config === 'object'
? config
: typeof config === 'function'
? await config()
: {}
if (typeof config === 'object') return config
if (typeof config === 'function') return await config()
return {}
}
`
const basicVueI18nConfigCode = generateVueI18nConfiguration(vueI18nConfigPathInfo, ({ absolute: absolutePath, relative: relativePath, hash, relativeBase, type }, { dir, base, ext }) => {
const configImportKey = makeImportKey(relativeBase, dir, base)
return `const vueI18n = await vueI18nConfigLoader((${genDynamicImport(genImportSpecifier(configImportKey, ext, absolutePath, type, { hash, resourceType: 'config' }), { comment: `webpackChunkName: "${normalizeWithUnderScore(relativePath)}_${hash}"` })}))\n`
})
if (basicVueI18nConfigCode != null) {
genCodes += ` ${basicVueI18nConfigCode}`
genCodes += ` ${rootKey}.${key} = vueI18n\n`
} else {
genCodes += ` ${rootKey}.${key} = ${toCode({})}\n`
}
genCodes += ` ${rootKey}.${key}.messages ??= {}\n`
if (vueI18nConfigPaths.length > 0) {
genCodes += ` const deepCopy = (src, des, predicate) => {
for (const key in src) {
if (typeof src[key] === 'object') {
if (!typeof des[key] === 'object') des[key] = {}
deepCopy(src[key], des[key], predicate)
} else {
if (predicate) {
if (predicate(src[key], des[key])) {
des[key] = src[key]
}
} else {
genCodes += ` ${rootKey}.${key} = ${toCode({ messages: {} })}\n`
const combinedConfigs = [vueI18nConfigPathInfo, ...vueI18nConfigPaths].reverse()
genCodes += ` const deepCopy = (src, des, predicate) => {
for (const key in src) {
if (typeof src[key] === 'object') {
if (!(typeof des[key] === 'object')) des[key] = {}
deepCopy(src[key], des[key], predicate)
} else {
if (predicate) {
if (predicate(src[key], des[key])) {
des[key] = src[key]
}
} else {
des[key] = src[key]
}
}
}
const mergeMessages = async (messages, loader) => {
const layerConfig = await vueI18nConfigLoader(loader)
const vueI18n = layerConfig || {}
const layerMessages = vueI18n.messages || {}
for (const [locale, message] of Object.entries(layerMessages)) {
messages[locale] ??= {}
deepCopy(message, messages[locale])
}
const mergeVueI18nConfigs = async (configuredMessages, loader) => {
const layerConfig = await vueI18nConfigLoader(loader)
const cfg = layerConfig || {}
cfg.messages ??= {}
const skipped = ['messages']
for (const [k, v] of Object.entries(cfg).filter(([k]) => !skipped.includes(k))) {
if(nuxtI18nOptions.vueI18n?.[k] === undefined) {
nuxtI18nOptions.vueI18n[k] = v
} else {
deepCopy(v, nuxtI18nOptions.vueI18n[k])
}
}
`
for (const [locale, message] of Object.entries(cfg.messages)) {
configuredMessages[locale] ??= {}
deepCopy(message, configuredMessages[locale])
}
for (const configPath of vueI18nConfigPaths) {
const additionalVueI18nConfigCode = generateVueI18nConfiguration(configPath, ({ absolute: absolutePath, relative: relativePath, hash, relativeBase, type }, { dir, base, ext }) => {
const configImportKey = makeImportKey(relativeBase, dir, base)
return `await mergeMessages(${rootKey}.${key}.messages, (${genDynamicImport(genImportSpecifier(configImportKey, ext, absolutePath, type, { hash, resourceType: 'config' }), { comment: `webpackChunkName: "${normalizeWithUnderScore(relativePath)}_${hash}"` })}))\n`
})
}
`
for (const configPath of combinedConfigs) {
const additionalVueI18nConfigCode = generateVueI18nConfiguration(
configPath,
({ absolute: absolutePath, relative: relativePath, hash, relativeBase, type }, { dir, base, ext }) => {
const configImportKey = makeImportKey(relativeBase, dir, base)
return `await mergeVueI18nConfigs(${rootKey}.${key}.messages, (${genDynamicImport(
genImportSpecifier(configImportKey, ext, absolutePath, type, { hash, resourceType: 'config' }),
{ comment: `webpackChunkName: "${normalizeWithUnderScore(relativePath)}_${hash}"` }
)}))\n`
}
)
if (additionalVueI18nConfigCode != null) {
genCodes += ` ${additionalVueI18nConfigCode}`
}
Expand Down
Loading

0 comments on commit c411518

Please sign in to comment.