Skip to content

Commit

Permalink
feat: extend lazy loading
Browse files Browse the repository at this point in the history
resolve #1797
  • Loading branch information
kazupon committed Jan 19, 2023
1 parent b72135e commit 23cb159
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 60 deletions.
9 changes: 8 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,12 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"i18n-ally.localesPaths": [
"playground/locales",
"specs/fixtures/basic/locales",
"specs/fixtures/head/locales",
"specs/fixtures/vue_i18n_options_loader/locales",
"specs/fixtures/lazy/lang"
]
}
74 changes: 52 additions & 22 deletions src/gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,34 @@ export function generateLoaderOptions(
}
}

const generatedImports = new Map<string, string>()
const importMapper = new Map<string, string>()
for (const { code, path, file } of syncLocaleFiles) {
importMapper.set(code, genSafeVariableName(`locale_${code}`))
let loadPath = path
if (file && langDir) {
loadPath = resolveLocaleRelativePath(localesRelativeBase, langDir, file)

function generateSyncImports(gen: string, path?: string) {
if (!path) {
return gen
}
let assertFormat = ''
if (file) {
const { ext } = parsePath(file)
assertFormat = ext.slice(1)

const { base, ext } = parsePath(path)
if (!generatedImports.has(base)) {
let loadPath = path
if (langDir) {
loadPath = resolveLocaleRelativePath(localesRelativeBase, langDir, base)
}
const assertFormat = ext.slice(1)
const variableName = genSafeVariableName(`locale_${convertToImportId(base)}`)
gen += `${genImport(loadPath, variableName, assertFormat ? { assert: { type: assertFormat } } : {})}\n`
importMapper.set(base, variableName)
generatedImports.set(base, loadPath)
}
genCode += `${genImport(
loadPath,
genSafeVariableName(`locale_${code}`),
assertFormat ? { assert: { type: assertFormat } } : {}
)}\n`

return gen
}

for (const { path, paths } of syncLocaleFiles) {
;(path ? [path] : paths || []).forEach(p => {
genCode = generateSyncImports(genCode, p)
})
}

// prettier-ignore
Expand Down Expand Up @@ -102,15 +113,20 @@ export function generateLoaderOptions(
} else if (rootKey === 'localeInfo') {
let codes = `export const localeMessages = {\n`
if (langDir) {
for (const { code } of syncLocaleFiles) {
codes += ` ${toCode(code)}: () => Promise.resolve(${importMapper.get(code)}),\n`
for (const { code, path, paths } of syncLocaleFiles) {
const syncPaths = path ? [path] : paths || []
codes += ` ${toCode(code)}: [${syncPaths.map(path => {
const { base } = parsePath(path)
return `{ key: ${toCode(generatedImports.get(base))}, load: () => Promise.resolve(${importMapper.get(base)}) }`
})}],\n`
}
for (const { code, path, file } of asyncLocaleFiles) {
let loadPath = path
if (file && langDir) {
loadPath = resolveLocaleRelativePath(localesRelativeBase, langDir, file)
}
codes += ` ${toCode(code)}: ${genDynamicImport(loadPath, { comment: `webpackChunkName: "lang-${code}"` })},\n`
for (const { code, path, paths } of asyncLocaleFiles) {
const dynamicPaths = path ? [path] : paths || []
codes += ` ${toCode(code)}: [${dynamicPaths.map(path => {
const { base } = parsePath(path)
const loadPath = resolveLocaleRelativePath(localesRelativeBase, langDir, base)
return `{ key: ${toCode(loadPath)}, load: ${genDynamicImport(loadPath, { comment: `webpackChunkName: "lang-${base}"` })} }`
})}],\n`
}
}
codes += `}\n`
Expand All @@ -130,6 +146,20 @@ export function generateLoaderOptions(
return genCode
}

const IMPORT_ID_CACHES = new Map<string, string>()

function convertToImportId(file: string) {
if (IMPORT_ID_CACHES.has(file)) {
return IMPORT_ID_CACHES.get(file)
}

const { name } = parsePath(file)
const id = name.replace(/-/g, '_').replace(/\./g, '_')
IMPORT_ID_CACHES.set(file, id)

return id
}

function resolveLocaleRelativePath(relativeBase: string, langDir: string, file: string) {
return normalize(`${relativeBase}/${langDir}/${file}`)
}
Expand Down
8 changes: 7 additions & 1 deletion src/options.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@ import type { DeepRequired } from 'ts-essentials'
* stub type definition for @nuxtjs/i18n internally
*/

type LocaleLoader = {
key: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
load: () => Promise<any>
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const loadMessages: () => Promise<any> = () => Promise.resolve({})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const localeMessages: Record<string, () => Promise<any>> = {}
export const localeMessages: Record<string, LocaleLoader[]> = {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const additionalMessages: Record<string, Array<() => Promise<any>>> = {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
59 changes: 51 additions & 8 deletions src/runtime/internal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { isArray, isString, isFunction } from '@intlify/shared'
import { isArray, isString, isFunction, isObject, hasOwn } from '@intlify/shared'
import {
findBrowserLocale,
getLocalesRegex,
Expand Down Expand Up @@ -94,6 +94,24 @@ export function parseAcceptLanguage(input: string): string[] {
return input.split(',').map(tag => tag.split(';')[0])
}

const isNotObjectOrIsArray = (val: unknown) => !isObject(val) || isArray(val)

function deepCopy(src: any, des: any) {
if (isNotObjectOrIsArray(src) || isNotObjectOrIsArray(des)) {
return
}

for (const key in src) {
if (hasOwn(src, key)) {
if (isNotObjectOrIsArray(src[key]) || isNotObjectOrIsArray(des[key])) {
des[key] = src[key]
} else {
deepCopy(src[key], des[key])
}
}
}
}

async function loadMessage(context: NuxtApp, loader: () => Promise<any>) {
let message: LocaleMessages<DefineLocaleMessage> | null = null
try {
Expand All @@ -112,23 +130,48 @@ async function loadMessage(context: NuxtApp, loader: () => Promise<any>) {
}

const loadedLocales: Locale[] = []
const loadedMessages = new Map<string, LocaleMessages<DefineLocaleMessage>>()

export async function loadLocale(
context: NuxtApp,
locale: Locale,
setter: (locale: Locale, message: LocaleMessages<DefineLocaleMessage>) => void
) {
if (process.server || process.dev || !loadedLocales.includes(locale)) {
const loader = localeMessages[locale]
if (loader != null) {
const message = await loadMessage(context, loader)
if (message != null) {
setter(locale, message)
const loaders = localeMessages[locale]
if (loaders != null) {
if (loaders.length === 1) {
const { key, load } = loaders[0]
if (!loadedMessages.has(key)) {
const message = await loadMessage(context, load)
if (message != null) {
loadedMessages.set(key, message)
setter(locale, message)
loadedLocales.push(locale)
}
}
} else if (loaders.length > 1) {
const targetMessage: LocaleMessages<DefineLocaleMessage> = {}
for (const { key, load } of loaders) {
let message: LocaleMessages<DefineLocaleMessage> | undefined | null = null
if (loadedMessages.has(key)) {
message = loadedMessages.get(key)
} else {
message = await loadMessage(context, load)
if (message != null) {
loadedMessages.set(key, message)
}
}
if (message != null) {
deepCopy(message, targetMessage)
}
}
setter(locale, targetMessage)
loadedLocales.push(locale)
}
} else {
console.warn(formatMessage('Could not find ' + locale + ' locale in localeMessages'))
}
} else {
console.warn(formatMessage('Could not find ' + locale + ' locale code in localeMessages'))
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export interface DetectBrowserLanguageOptions {
}

export type LocaleInfo = {
path: string
path?: string
paths?: string[]
} & LocaleObject

export interface RootRedirectOptions {
Expand Down
20 changes: 9 additions & 11 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { promises as fs, constants as FS_CONSTANTS } from 'node:fs'
import { resolveFiles } from '@nuxt/kit'
import { parse, parse as parsePath } from 'pathe'
import { parse, parse as parsePath, resolve } from 'pathe'
import { encodePath } from 'ufo'
import { resolveLockfile } from 'pkg-types'
import { isObject, isString } from '@intlify/shared'
Expand Down Expand Up @@ -54,16 +54,14 @@ export function getNormalizedLocales(locales: NuxtI18nOptions['locales']): Local

export async function resolveLocales(path: string, locales: LocaleObject[]): Promise<LocaleInfo[]> {
const files = await resolveFiles(path, '**/*{json,json5,yaml,yml}')
return files.map(file => {
const parsed = parse(file)
const locale = locales.find((locale: string | LocaleObject) => isObject(locale) && locale.file === parsed.base)
return locales == null
? {
path: file,
file: parsed.base,
code: parsed.name
}
: Object.assign({ path: file }, locale)
const find = (f: string) => files.find(file => file === resolve(path, f))
return (locales as LocaleInfo[]).map(locale => {
if (locale.file) {
locale.path = find(locale.file)
} else if (locale.files) {
locale.paths = locale.files.map(file => find(file)).filter(Boolean) as string[]
}
return locale
})
}

Expand Down
8 changes: 8 additions & 0 deletions src/vue-i18n-routing.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare module 'vue-i18n-routing' {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface LocaleObject extends Record<string, any> {
files?: string[]
}
}

export {}
48 changes: 39 additions & 9 deletions test/__snapshots__/gen.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import locale_fr from \\"../locales/fr.json\\" assert { type: \\"json\\" };
export const localeCodes = [\\"en\\",\\"ja\\",\\"fr\\"]
export const localeMessages = {
\\"en\\": () => Promise.resolve(locale_en),
\\"ja\\": () => Promise.resolve(locale_ja),
\\"fr\\": () => Promise.resolve(locale_fr),
\\"en\\": [{ key: \\"../locales/en.json\\", load: () => Promise.resolve(locale_en) }],
\\"ja\\": [{ key: \\"../locales/ja.json\\", load: () => Promise.resolve(locale_ja) }],
\\"fr\\": [{ key: \\"../locales/fr.json\\", load: () => Promise.resolve(locale_fr) }],
}
export const additionalMessages = Object({})
Expand Down Expand Up @@ -37,9 +37,39 @@ exports[`lazy 1`] = `
"export const localeCodes = [\\"en\\",\\"ja\\",\\"fr\\"]
export const localeMessages = {
\\"en\\": () => import(\\"../locales/en.json\\" /* webpackChunkName: \\"lang-en\\" */),
\\"ja\\": () => import(\\"../locales/ja.json\\" /* webpackChunkName: \\"lang-ja\\" */),
\\"fr\\": () => import(\\"../locales/fr.json\\" /* webpackChunkName: \\"lang-fr\\" */),
\\"en\\": [{ key: \\"../locales/en.json\\", load: () => import(\\"../locales/en.json\\" /* webpackChunkName: \\"lang-en.json\\" */) }],
\\"ja\\": [{ key: \\"../locales/ja.json\\", load: () => import(\\"../locales/ja.json\\" /* webpackChunkName: \\"lang-ja.json\\" */) }],
\\"fr\\": [{ key: \\"../locales/fr.json\\", load: () => import(\\"../locales/fr.json\\" /* webpackChunkName: \\"lang-fr.json\\" */) }],
}
export const additionalMessages = Object({})
export const resolveNuxtI18nOptions = async (context) => {
const nuxtI18nOptions = Object({})
nuxtI18nOptions.defaultLocale = \\"en\\"
const vueI18nOptionsLoader = async (context) => Object({\\"locale\\":\\"en\\",\\"fallbackLocale\\":\\"fr\\",\\"messages\\": Object({\\"en\\":{
\\"hello\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"Hello!\\"])};fn.source=\\"Hello!\\";return fn;})()
},}),})
nuxtI18nOptions.vueI18n = await vueI18nOptionsLoader(context)
return nuxtI18nOptions
}
export const nuxtI18nInternalOptions = Object({__normalizedLocales: [Object({\\"code\\":\\"en\\"})]})
export const NUXT_I18N_MODULE_ID = \\"@nuxtjs/i18n\\"
export const isSSG = false
export const isSSR = true
"
`;

exports[`multiple files 1`] = `
"export const localeCodes = [\\"en\\",\\"ja\\",\\"fr\\",\\"es\\",\\"es-AR\\"]
export const localeMessages = {
\\"en\\": [{ key: \\"../locales/en.json\\", load: () => import(\\"../locales/en.json\\" /* webpackChunkName: \\"lang-en.json\\" */) }],
\\"ja\\": [{ key: \\"../locales/ja.json\\", load: () => import(\\"../locales/ja.json\\" /* webpackChunkName: \\"lang-ja.json\\" */) }],
\\"fr\\": [{ key: \\"../locales/fr.json\\", load: () => import(\\"../locales/fr.json\\" /* webpackChunkName: \\"lang-fr.json\\" */) }],
\\"es\\": [{ key: \\"../locales/es.json\\", load: () => import(\\"../locales/es.json\\" /* webpackChunkName: \\"lang-es.json\\" */) }],
\\"es-AR\\": [{ key: \\"../locales/es.json\\", load: () => import(\\"../locales/es.json\\" /* webpackChunkName: \\"lang-es.json\\" */) },{ key: \\"../locales/es-AR.json\\", load: () => import(\\"../locales/es-AR.json\\" /* webpackChunkName: \\"lang-es-AR.json\\" */) }],
}
export const additionalMessages = Object({})
Expand Down Expand Up @@ -130,9 +160,9 @@ import locale_fr from \\"../locales/fr.json\\" assert { type: \\"json\\" };
export const localeCodes = [\\"en\\",\\"ja\\",\\"fr\\"]
export const localeMessages = {
\\"en\\": () => Promise.resolve(locale_en),
\\"ja\\": () => Promise.resolve(locale_ja),
\\"fr\\": () => Promise.resolve(locale_fr),
\\"en\\": [{ key: \\"../locales/en.json\\", load: () => Promise.resolve(locale_en) }],
\\"ja\\": [{ key: \\"../locales/ja.json\\", load: () => Promise.resolve(locale_ja) }],
\\"fr\\": [{ key: \\"../locales/fr.json\\", load: () => Promise.resolve(locale_fr) }],
}
export const additionalMessages = Object({\\"en\\":[() => Promise.resolve({
Expand Down
Loading

0 comments on commit 23cb159

Please sign in to comment.