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/i18n #179

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions configuration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ declare module 'virtual:nuxt-pwa-configuration' {
export const display: 'fullscreen' | 'standalone' | 'minimal-ui' | 'browser'
export const installPrompt: string | undefined
export const periodicSyncForUpdates: number
export const i18nSplitManifest: boolean
export const i18nSplitServiceWorker: boolean
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"@nuxt/module-builder": "^0.8.3",
"@nuxt/schema": "^3.10.1",
"@nuxt/test-utils": "^3.11.0",
"@nuxtjs/i18n": "^9.1.1",
"@playwright/test": "^1.40.1",
"@types/node": "^18",
"bumpp": "^9.2.0",
Expand Down
1,063 changes: 747 additions & 316 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions src/runtime/components/NuxtPwaAssetsI18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { MetaObject } from '@nuxt/schema'
import { defineComponent, ref } from 'vue'
import { pwaInfo } from 'virtual:pwa-info'
import { pwaAssetsHead } from 'virtual:pwa-assets/head'
import { useHead, useLocalePath } from '#imports'

export default defineComponent({
setup() {
const meta = ref<MetaObject>({ link: [] })
const localePath = useLocalePath()
useHead(meta)
if (pwaAssetsHead.themeColor)
meta.value.meta = [{ name: 'theme-color', content: pwaAssetsHead.themeColor.content }]

if (pwaAssetsHead.links.length)
// @ts-expect-error: links are fine
meta.value.link!.push(...pwaAssetsHead.links)

if (pwaInfo) {
const { webManifest } = pwaInfo
if (webManifest) {
const { href, useCredentials } = webManifest
const prefix = localePath("/").replace("^/$","")
if (useCredentials) {
meta.value.link!.push({
rel: 'manifest',
href: prefix+href,
crossorigin: 'use-credentials',
})
}
else {
meta.value.link!.push({
rel: 'manifest',
href: prefix+href,
})
}
}
}

return () => null
},
})
35 changes: 35 additions & 0 deletions src/runtime/components/VitePwaManifestI18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { MetaObject } from '@nuxt/schema'
import { defineComponent, ref } from 'vue'
import { pwaInfo } from 'virtual:pwa-info'
import { useHead, useLocalePath } from '#imports'

export default defineComponent({
async setup() {
if (pwaInfo) {
const meta = ref<MetaObject>({ link: [] })
const localePath = useLocalePath()
useHead(meta)

const { webManifest } = pwaInfo
if (webManifest) {
const { href, useCredentials } = webManifest
const prefix = localePath("/").replace("^/$","")
if (useCredentials) {
meta.value.link!.push({
rel: 'manifest',
href: prefix+href,
crossorigin: 'use-credentials',
})
}
else {
meta.value.link!.push({
rel: 'manifest',
href: prefix+href,
})
}
}
}

return () => null
},
})
12 changes: 10 additions & 2 deletions src/runtime/plugins/pwa.client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { nextTick, reactive, ref } from 'vue'
import type { UnwrapNestedRefs } from 'vue'
import { useRegisterSW } from 'virtual:pwa-register/vue'
import { display, installPrompt, periodicSyncForUpdates } from 'virtual:nuxt-pwa-configuration'
import { display, installPrompt, periodicSyncForUpdates, i18nSplitServiceWorker } from 'virtual:nuxt-pwa-configuration'
import type { BeforeInstallPromptEvent, PwaInjection, UserChoice } from './types'
import { defineNuxtPlugin } from '#imports'
import type { Plugin } from '#app'

const plugin: Plugin<{
pwa?: UnwrapNestedRefs<PwaInjection>
}> = defineNuxtPlugin(() => {
}> = defineNuxtPlugin(({$router, $config}) => {
const registrationError = ref(false)
const swActivated = ref(false)
const showInstallPrompt = ref(false)
Expand Down Expand Up @@ -50,10 +50,18 @@ const plugin: Plugin<{
}, timeout)
}

let baseUrl = ""
if (i18nSplitServiceWorker) {
const separator = $config.public.i18n.routesNameSeparator
const locale = $router.currentRoute.value.name.split(separator)[1]
baseUrl = $router.resolve({name:`index${separator}${locale}`}).path
}

const {
offlineReady, needRefresh, updateServiceWorker,
} = useRegisterSW({
immediate: true,
baseUrl: baseUrl,
onRegisterError() {
registrationError.value = true
},
Expand Down
17 changes: 16 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ export interface ClientOptions {
installPrompt?: boolean | string
}

export interface I18nOptions {
/**
* Split manifest per locale : defaults to false.
*/
splitManifest?: boolean
/**
* Split service worker per locale : defaults to false.
*/
splitServiceWorker?: boolean
}

export interface PwaModuleOptions extends Partial<VitePWAOptions> {
registerWebManifestInRouteRules?: boolean
/**
Expand All @@ -30,5 +41,9 @@ export interface PwaModuleOptions extends Partial<VitePWAOptions> {
/**
* Options for plugin.
*/
client?: ClientOptions
client?: ClientOptions,
/**
* Options for i18n.
*/
i18n?: I18nOptions,
}
13 changes: 9 additions & 4 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { lstat } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
import { createReadStream, readFileSync } from 'node:fs'
import type { Nuxt } from '@nuxt/schema'
import { resolve } from 'pathe'
import type { NitroConfig } from 'nitropack'
Expand Down Expand Up @@ -61,7 +61,7 @@ export function configurePWAOptions(
config.dontCacheBustURLsMatching = new RegExp(buildAssetsDir)

// handle payload extraction
if (nuxt.options.experimental.payloadExtraction) {
if (nuxt.options.experimental.payloadExtraction && !options.i18n?.splitServiceWorker) {
const enableGlobPatterns = nuxt.options._generate
|| (
!!nitroConfig.prerender?.routes?.length
Expand Down Expand Up @@ -141,13 +141,18 @@ function createManifestTransform(
if (latestEntry)
latestEntry.revision = revision
else
entries.push({ url: latest, revision, size: data.size })
entries.push({ url: base+latest, revision, size: data.size })

const id = JSON.parse(readFileSync(latestJson, "utf8")).id
entries.filter(e => e.url.match(/.*_payload.js(on)?$/)).forEach((e) => {
e.revision = null
e.url += `?${id}`
})
}
else {
entries = entries.filter(e => e.url !== latest)
}
}

return { manifest: entries, warnings: [] }
}
}
79 changes: 79 additions & 0 deletions src/utils/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@

import { useNuxt, loadNuxtModuleInstance } from '@nuxt/kit'
import { resolve, join, basename } from 'pathe'
import type { PwaModuleOptions } from 'vite-plugin-pwa'
import type { NuxtI18nOptions } from '@nuxtjs/i18n'
import type { NuxtModule } from 'nuxt/schema'

export async function webManifests(manifestDir) {
const i18nOptions = await getNuxtModuleOptions('@nuxtjs/i18n') as NuxtI18nOptions

return i18nOptions.locales.map(({code})=>{
const localePath = getLocalePath(i18nOptions, code)
return {
localDir: resolve(manifestDir, localePath),
optionsI18n: { manifest: {
start_url: `/${localePath}`,
scope: `/${localePath}`,
lang: code,
}}
}
})
}

export async function swOptions(options: PwaModuleOptions) {
const i18nOptions = await getNuxtModuleOptions('@nuxtjs/i18n') as NuxtI18nOptions
const ignorePrefix = i18nOptions.locales.map(({code})=>code).join(',')
return i18nOptions.locales.map(({code})=>{
const prefix = getLocalePath(i18nOptions, code)
const swDest = join(options.outDir, prefix, basename(options.swDest) || "sw.js")
const ret:Partial<PwaModuleOptions> = {
outDir: join(options.outDir, prefix),
swDest: swDest,
}
ret.injectManifest = ret.workbox = ({
swDest: swDest,
globPatterns: [join(prefix, '**', '_payload.json')],
globIgnores: prefix ? [] : [join(`{${ignorePrefix}}`, '**', '_payload.json')],
modifyURLPrefix: {"": "/"},
})
return ret
})
}

//from https://github.com/nuxt-modules/sitemap/blob/main/src/util/kit.ts
async function getNuxtModuleOptions(module: string | NuxtModule, nuxt: Nuxt = useNuxt()) {
const moduleMeta = (typeof module === 'string' ? { name: module } : await module.getMeta?.()) || {}
const { nuxtModule } = (await loadNuxtModuleInstance(module, nuxt))

let moduleEntry: [string | NuxtModule, Record<string, any>] | undefined
for (const m of nuxt.options.modules) {
if (Array.isArray(m) && m.length >= 2) {
const _module = m[0]
const _moduleEntryName = typeof _module === 'string'
? _module
: (await (_module as any as NuxtModule).getMeta?.())?.name || ''
if (_moduleEntryName === moduleMeta.name)
moduleEntry = m as [string | NuxtModule, Record<string, any>]
}
}

let inlineOptions = {}
if (moduleEntry)
inlineOptions = moduleEntry[1]
if (nuxtModule.getOptions)
return nuxtModule.getOptions(inlineOptions, nuxt)
return inlineOptions
}

function getLocalePath(options: NuxtI18nOptions, localeCode: string) {
switch (options.strategy) {
case 'prefix_except_default':
case 'prefix_and_default':
return localeCode === options.defaultLocale ? "" : localeCode
case 'prefix':
return localeCode
default:
throw "Strategy not implemented"
}
}
20 changes: 14 additions & 6 deletions src/utils/module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { join } from 'node:path'
import { mkdir } from 'node:fs/promises'
import {
addComponent,
addDevServerHandler,
Expand Down Expand Up @@ -73,10 +72,18 @@ export async function doSetup(options: PwaModuleOptions, nuxt: Nuxt) {
name: 'NuxtPwaManifest',
filePath: resolver.resolve(runtimeDir, 'components/VitePwaManifest'),
}),
addComponent({
name: 'NuxtPwaManifestI18n',
filePath: resolver.resolve(runtimeDir, 'components/VitePwaManifestI18n'),
}),
addComponent({
name: 'NuxtPwaAssets',
filePath: resolver.resolve(runtimeDir, 'components/NuxtPwaAssets'),
}),
addComponent({
name: 'NuxtPwaAssetsI18n',
filePath: resolver.resolve(runtimeDir, 'components/NuxtPwaAssetsI18n'),
}),
addComponent({
name: 'PwaAppleImage',
filePath: resolver.resolve(runtimeDir, 'components/PwaAppleImage.vue'),
Expand Down Expand Up @@ -142,8 +149,7 @@ export async function doSetup(options: PwaModuleOptions, nuxt: Nuxt) {

const api = resolveVitePluginPWAAPI()
if (api) {
await mkdir(manifestDir, { recursive: true })
await writeWebManifest(manifestDir, options.manifestFilename || 'manifest.webmanifest', api, pwaAssets)
await writeWebManifest(manifestDir, options, api, pwaAssets)
}
},
})
Expand Down Expand Up @@ -172,6 +178,8 @@ export async function doSetup(options: PwaModuleOptions, nuxt: Nuxt) {
export const display = '${display}'
export const installPrompt = ${JSON.stringify(installPrompt)}
export const periodicSyncForUpdates = ${typeof client.periodicSyncForUpdates === 'number' ? client.periodicSyncForUpdates : 0}
export const i18nSplitManifest = ${options.i18n?.splitManifest}
export const i18nSplitServiceWorker = ${options.i18n?.splitServiceWorker}
`
}
},
Expand Down Expand Up @@ -309,7 +317,7 @@ export const periodicSyncForUpdates = ${typeof client.periodicSyncForUpdates ===
if (nuxt3_8) {
nuxt.hook('nitro:build:public-assets', async () => {
await regeneratePWA(
options.outDir!,
options,
pwaAssets,
resolveVitePluginPWAAPI(),
)
Expand All @@ -319,7 +327,7 @@ export const periodicSyncForUpdates = ${typeof client.periodicSyncForUpdates ===
nuxt.hook('nitro:init', (nitro) => {
nitro.hooks.hook('rollup:before', async () => {
await regeneratePWA(
options.outDir!,
options,
pwaAssets,
resolveVitePluginPWAAPI(),
)
Expand All @@ -328,7 +336,7 @@ export const periodicSyncForUpdates = ${typeof client.periodicSyncForUpdates ===
if (nuxt.options._generate) {
nuxt.hook('close', async () => {
await regeneratePWA(
options.outDir!,
options,
pwaAssets,
resolveVitePluginPWAAPI(),
)
Expand Down
Loading