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

feat: support loading messages from file without lazy-loading #1130

Merged
merged 2 commits into from
Mar 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions docs/content/en/options-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ When using an object form, the properties can be:
- `iso` (required when using SEO features) - The ISO code used for SEO features and for matching browser locales when using [`detectBrowserLanguage`](#detectbrowserlanguage) functionality. Should be in one of those formats:
* ISO 639-1 code (e.g. `'en'`)
* ISO 639-1 and ISO 3166-1 alpha-2 codes, separated by hyphen (e.g. `'en-US'`)
- `file` (requires [`lazy`](#lazy) to be enabled) - the name of the file. Will be resolved relative to `langDir` path when loading locale messages lazily
- `file` - the name of the file. Will be resolved relative to `langDir` path when loading locale messages from file
- `dir` (from `v6.19.0`) The dir property specifies the direction of the elements and content, value could be `'rtl'`, `'ltr'` or `'auto'`.
- `domain` (required when using [`differentDomains`](#differentdomains)) - the domain name you'd like to use for that locale (including the port if used)
- `...` - any custom property set on the object will be exposed at runtime. This can be used, for example, to define the language name for the purpose of using it in a language selector on the page.
Expand Down Expand Up @@ -122,7 +122,7 @@ Routes generation strategy. Can be set to one of the following:

Whether the translations should be lazy-loaded. If this is enabled, you MUST configure `langDir` option, and locales must be an array of objects, each containing a `file` key.

Loading locale messages lazily means that only messages for currently used locale (and potentially of the default locale, if different from current locale) will be loaded on page loading.
Loading locale messages lazily means that only messages for currently used locale (and for the fallback locale, if different from current locale) will be loaded on page loading.

See also [Lazy-load translations](/lazy-load-translations).

Expand All @@ -131,13 +131,7 @@ See also [Lazy-load translations](/lazy-load-translations).
- type: `string` or `null`
- default: `null`

<alert type="warning">

This option only works and is required when `lazy` is enabled.

</alert>

Directory that contains translation files when lazy-loading messages. Use Webpack paths like `~/locales/` (with trailing slash).
Directory that contains translation files to load. Can be used with or without lazy-loading (the `lazy` option). Use Webpack paths like `~/locales/` (with trailing slash).

## `detectBrowserLanguage`

Expand Down
12 changes: 3 additions & 9 deletions docs/content/es/options-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ When using an object form, the properties can be:
- `iso` (required when using SEO features) - The ISO code used for SEO features and for matching browser locales when using [`detectBrowserLanguage`](#detectbrowserlanguage) functionality. Should be in one of those formats:
* ISO 639-1 code (e.g. `'en'`)
* ISO 639-1 and ISO 3166-1 alpha-2 codes, separated by hyphen (e.g. `'en-US'`)
- `file` (requires [`lazy`](#lazy) to be enabled) - the name of the file. Will be resolved relative to `langDir` path when loading locale messages lazily
- `file` - the name of the file. Will be resolved relative to `langDir` path when loading locale messages from file
- `dir` (from `v6.19.0`) The dir property specifies the direction of the elements and content, value could be `'rtl'`, `'ltr'` or `'auto'`.
- `domain` (required when using [`differentDomains`](#differentdomains)) - the domain name you'd like to use for that locale (including the port if used)
- `...` - any custom property set on the object will be exposed at runtime. This can be used, for example, to define the language name for the purpose of using it in a language selector on the page.
Expand Down Expand Up @@ -122,7 +122,7 @@ Routes generation strategy. Can be set to one of the following:

Whether the translations should be lazy-loaded. If this is enabled, you MUST configure `langDir` option, and locales must be an array of objects, each containing a `file` key.

Loading locale messages lazily means that only messages for currently used locale (and potentially of the default locale, if different from current locale) will be loaded on page loading.
Loading locale messages lazily means that only messages for currently used locale (and for the fallback locale, if different from current locale) will be loaded on page loading.

See also [Lazy-load translations](/lazy-load-translations).

Expand All @@ -131,13 +131,7 @@ See also [Lazy-load translations](/lazy-load-translations).
- type: `string` or `null`
- default: `null`

<alert type="warning">

This option only works and is required when `lazy` is enabled.

</alert>

Directory that contains translation files when lazy-loading messages. Use Webpack paths like `~/locales/` (with trailing slash).
Directory that contains translation files to load. Can be used with or without lazy-loading (the `lazy` option). Use Webpack paths like `~/locales/` (with trailing slash).

## `detectBrowserLanguage`

Expand Down
13 changes: 7 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ export default function (moduleOptions) {
return
}

if (options.lazy) {
if (!options.langDir) {
throw new Error(formatMessage('When using the "lazy" option you must also set the "langDir" option.'))
}
if (options.lazy && !options.langDir) {
throw new Error(formatMessage('When using the "lazy" option you must also set the "langDir" option.'))
}

if (options.langDir) {
if (!options.locales.length || typeof options.locales[0] === 'string') {
throw new Error(formatMessage('When using the "langDir" option the "locales" option must be a list of objects.'))
throw new Error(formatMessage('When using the "langDir" option the "locales" must be a list of objects.'))
}
for (const locale of options.locales) {
if (typeof (locale) === 'string' || !locale.file) {
throw new Error(formatMessage(`All locales must be objects and have the "file" property set when using "lazy".\nFound none in:\n${JSON.stringify(locale, null, 2)}.`))
throw new Error(formatMessage(`All locales must be objects and have the "file" property set when using "langDir".\nFound none in:\n${JSON.stringify(locale, null, 2)}.`))
}
}
options.langDir = this.nuxt.resolver.resolveAlias(options.langDir)
Expand Down
36 changes: 23 additions & 13 deletions src/templates/options.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
<%
const { lazy, locales, langDir, vueI18n } = options.options
const { fallbackLocale } = vueI18n || {}
let fallbackLocaleFile = ''
if (lazy && langDir && vueI18n && fallbackLocale && typeof (fallbackLocale) === 'string') {
const l = locales.find(l => l.code === fallbackLocale)
if (l) {
fallbackLocaleFile = l.file
%>import fallbackMessages from '<%= `../${relativeToBuild(langDir, l.file)}` %>'
const syncLocaleFiles = new Set()
const asyncLocaleFiles = new Set()

if (langDir) {
if (fallbackLocale && typeof (fallbackLocale) === 'string') {
const localeObject = locales.find(l => l.code === fallbackLocale)
if (localeObject) {
syncLocaleFiles.add(localeObject.file)
}
}
for (const locale of locales) {
if (!syncLocaleFiles.has(locale.file) && !asyncLocaleFiles.has(locale.file)) {
(lazy ? asyncLocaleFiles : syncLocaleFiles).add(locale.file)
}
}
for (const file of syncLocaleFiles) {
%>import locale<%= hash(file) %> from '<%= `../${relativeToBuild(langDir, file)}` %>'
<%
}
}
%>

<%
function stringifyValue(value) {
if (value === undefined || typeof value === 'function') {
return String(value);
Expand Down Expand Up @@ -41,18 +53,16 @@ for (const [rootKey, rootValue] of Object.entries(options)) {
}
}

if (lazy && langDir) { %>
if (langDir) { %>
export const localeMessages = {
<%
const files = new Set(locales.map(l => l.file))
// The messages for the fallback locale are imported synchronously and available from the main bundle as then
// it doesn't need to be included in every server-side response and can take better advantage of browser caching.
for (const file of files) {
if (file === fallbackLocaleFile) {%>
<%= `'${file}': () => Promise.resolve(fallbackMessages),` %><%
} else {%>
for (const file of syncLocaleFiles) {%>
<%= `'${file}': () => Promise.resolve(locale${hash(file)}),` %><%
}
for (const file of asyncLocaleFiles) {%>
<%= `'${file}': () => import('../${relativeToBuild(langDir, file)}' /* webpackChunkName: "lang-${file}" */),` %><%
}
}
%>
}
Expand Down
38 changes: 21 additions & 17 deletions src/templates/plugin.main.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,29 +89,33 @@ export default async (context) => {
app.i18n.setLocaleCookie(newLocale)
}

if (options.lazy) {
if (options.langDir) {
const i18nFallbackLocale = app.i18n.fallbackLocale

// Load fallback locale(s).
if (i18nFallbackLocale) {
if (options.lazy) {
// Load fallback locale(s).
if (i18nFallbackLocale) {
/** @type {Promise<void>[]} */
let localesToLoadPromises = []
if (Array.isArray(i18nFallbackLocale)) {
localesToLoadPromises = i18nFallbackLocale.map(fbLocale => loadLanguageAsync(context, fbLocale))
} else if (typeof i18nFallbackLocale === 'object') {
if (i18nFallbackLocale[newLocale]) {
localesToLoadPromises = localesToLoadPromises.concat(i18nFallbackLocale[newLocale].map(fbLocale => loadLanguageAsync(context, fbLocale)))
let localesToLoadPromises = []
if (Array.isArray(i18nFallbackLocale)) {
localesToLoadPromises = i18nFallbackLocale.map(fbLocale => loadLanguageAsync(context, fbLocale))
} else if (typeof i18nFallbackLocale === 'object') {
if (i18nFallbackLocale[newLocale]) {
localesToLoadPromises = localesToLoadPromises.concat(i18nFallbackLocale[newLocale].map(fbLocale => loadLanguageAsync(context, fbLocale)))
}
if (i18nFallbackLocale.default) {
localesToLoadPromises = localesToLoadPromises.concat(i18nFallbackLocale.default.map(fbLocale => loadLanguageAsync(context, fbLocale)))
}
} else if (newLocale !== i18nFallbackLocale) {
localesToLoadPromises.push(loadLanguageAsync(context, i18nFallbackLocale))
}
if (i18nFallbackLocale.default) {
localesToLoadPromises = localesToLoadPromises.concat(i18nFallbackLocale.default.map(fbLocale => loadLanguageAsync(context, fbLocale)))
}
} else if (newLocale !== i18nFallbackLocale) {
localesToLoadPromises.push(loadLanguageAsync(context, i18nFallbackLocale))
await Promise.all(localesToLoadPromises)
}
await Promise.all(localesToLoadPromises)
await loadLanguageAsync(context, newLocale)
} else {
// Load all locales.
await Promise.all(options.localeCodes.map(locale => loadLanguageAsync(context, locale)))
}

await loadLanguageAsync(context, newLocale)
}

app.i18n.locale = newLocale
Expand Down
9 changes: 5 additions & 4 deletions src/templates/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { localeMessages } from './options'
import { localeMessages, options } from './options'
import { formatMessage } from './utils-common'

/**
Expand All @@ -17,11 +17,11 @@ export async function loadLanguageAsync (context, locale) {
}

if (!i18n.loadedLanguages.includes(locale)) {
const localeObject = /** @type {import('../../types').LocaleObject[]} */(i18n.locales).find(l => l.code === locale)
const localeObject = options.normalizedLocales.find(l => l.code === locale)
if (localeObject) {
const { file } = localeObject
if (file) {
/* <% if (options.options.lazy && options.options.langDir) { %> */
/* <% if (options.options.langDir) { %> */
/** @type {import('vue-i18n').LocaleMessageObject | undefined} */
let messages
if (process.client) {
Expand Down Expand Up @@ -50,9 +50,10 @@ export async function loadLanguageAsync (context, locale) {
}
/* <% } %> */
} else {
// eslint-disable-next-line no-console
console.warn(formatMessage(`Could not find lang file for locale ${locale}`))
}
} else {
console.warn(formatMessage(`Attempted to load messages for non-existant locale code "${locale}"`))
}
}
}
79 changes: 79 additions & 0 deletions test/browser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,85 @@ describe(`${browserString} (with fallbackLocale, lazy)`, () => {
})
})

describe(`${browserString} (with fallbackLocale, langDir, non-lazy)`, () => {
/** @type {Nuxt} */
let nuxt
/** @type {import('playwright-chromium').ChromiumBrowser} */
let browser
/** @type {import('playwright-chromium').Page} */
let page

beforeAll(async () => {
const overrides = {
i18n: {
defaultLocale: 'pl',
lazy: false,
langDir: 'lang/',
vueI18n: {
fallbackLocale: 'pl'
}
}
}

const localConfig = loadConfig(__dirname, 'basic', overrides, { merge: true })

// Override after merging options to avoid arrays being merged.
localConfig.i18n.locales = [
{ code: 'en', iso: 'en-US', file: 'en-US.js' },
{ code: 'pl', iso: 'pl-PL', file: 'pl-PL.json' },
{ code: 'no', iso: 'no-NO', file: 'no-NO.json' }
]

nuxt = (await setup(localConfig)).nuxt
browser = await createBrowser()
})

afterAll(async () => {
if (browser) {
await browser.close()
}

await nuxt.close()
})

// Browser language is 'en' and so doesn't match supported ones.
// Issue https://github.com/nuxt-community/i18n-module/issues/643
test('updates language after navigating from default to non-default locale', async () => {
page = await browser.newPage()
await page.goto(url('/'))
expect(await (await page.$('#current-locale'))?.textContent()).toBe('locale: pl')
expect(await (await page.$('#current-page'))?.textContent()).toBe('page: Strona glowna')
await navigate(page, '/no')
expect(await (await page.$('#current-locale'))?.textContent()).toBe('locale: no')
expect(await (await page.$('#current-page'))?.textContent()).toBe('page: Hjemmeside')
})

// Issue https://github.com/nuxt-community/i18n-module/issues/843
test('updates language after navigating from non-default to default locale', async () => {
page = await browser.newPage()
await page.goto(url('/no'))
expect(await (await page.$('#current-locale'))?.textContent()).toBe('locale: no')
expect(await (await page.$('#current-page'))?.textContent()).toBe('page: Hjemmeside')
await navigate(page, '/')
expect(await (await page.$('#current-locale'))?.textContent()).toBe('locale: pl')
expect(await (await page.$('#current-page'))?.textContent()).toBe('page: Strona glowna')
})

test('messages have not been passed through Nuxt state', async () => {
page = await browser.newPage()
await page.goto(url('/'))
// @ts-ignore
const i18nState = await page.evaluate(() => window.__NUXT__.__i18n)
expect(i18nState).toBeUndefined()
})

test('can resolve translation for non-current locale', async () => {
page = await browser.newPage()
await page.goto(url('/'))
expect(await (await page.$('#english-translation'))?.textContent()).toBe('Homepage')
})
})

describe(`${browserString} (SPA)`, () => {
/** @type {Nuxt} */
let nuxt
Expand Down
1 change: 1 addition & 0 deletions test/fixture/basic/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<nuxt-link id="link-about" exact :to="aboutPath">{{ aboutTranslation }}</nuxt-link>
<div id="current-locale">locale: {{ $i18n.locale }}</div>
<div id="message-function">{{ $t('fn') }}</div>
<div id="english-translation">{{ $t('home', 'en') }}</div>
</div>
</template>

Expand Down