import { readFile } from 'fs-extra' import { resolve } from 'pathe' import type { ManifestOptions } from 'vite-plugin-pwa' import { getEnv } from '../../config/env' import { i18n } from '../../config/i18n' import type { LocaleObject } from '#i18n' // We have to extend the ManifestOptions interface from 'vite-plugin-pwa' // as that interface doesn't define the share_target field of Web App Manifests. interface ExtendedManifestOptions extends ManifestOptions { share_target: { action: string method: string enctype: string params: { text: string url: string files: [{ name: string accept: string[] }] } } } export type LocalizedWebManifest = Record<string, Partial<ExtendedManifestOptions>> export const pwaLocales = i18n.locales as LocaleObject[] type WebManifestEntry = Pick<ExtendedManifestOptions, 'name' | 'short_name' | 'description'> type RequiredWebManifestEntry = Required<WebManifestEntry & Pick<ExtendedManifestOptions, 'dir' | 'lang'>> export const createI18n = async (): Promise<LocalizedWebManifest> => { const { env } = await getEnv() const envName = `${env === 'release' ? '' : `(${env})`}` const { pwa } = await readI18nFile('en.json') const defaultManifest: Required<WebManifestEntry> = pwa.webmanifest[env] const locales: RequiredWebManifestEntry[] = await Promise.all( pwaLocales .filter(l => l.code !== 'en-US') .map(async ({ code, dir = 'ltr', file, files }) => { // read locale file or files const { pwa, app_name, app_desc_short } = file ? await readI18nFile(file) : await findBestWebManifestData(files, env) const entry: WebManifestEntry = pwa?.webmanifest?.[env] ?? {} if (!entry.name && app_name) entry.name = dir === 'rtl' ? `${envName} ${app_name}` : `${app_name} ${envName}` if (!entry.short_name && app_name) entry.short_name = dir === 'rtl' ? `${envName} ${app_name}` : `${app_name} ${envName}` if (!entry.description && app_desc_short) entry.description = app_desc_short return <RequiredWebManifestEntry>{ ...defaultManifest, ...entry, lang: code, dir, } }), ) locales.push({ ...defaultManifest, lang: 'en-US', dir: 'ltr', }) return locales.reduce((acc, { lang, dir, name, short_name, description }) => { acc[lang] = { scope: '/', id: '/', start_url: '/', display: 'standalone', lang, name, short_name, description, dir, background_color: '#ffffff', theme_color: '#ffffff', icons: [ { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png', }, { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png', }, { src: 'maskable-icon.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable', }, ], share_target: { action: '/web-share-target', method: 'POST', enctype: 'multipart/form-data', params: { text: 'text', url: 'text', files: [ { name: 'files', accept: ['image/*', 'video/*'], }, ], }, }, } acc[`${lang}-dark`] = { scope: '/', id: '/', start_url: '/', display: 'standalone', lang, name, short_name, description, dir, background_color: '#111111', theme_color: '#111111', icons: [ { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png', }, { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png', }, { src: 'maskable-icon.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable', }, ], share_target: { action: '/web-share-target', method: 'POST', enctype: 'multipart/form-data', params: { text: 'text', url: 'text', files: [ { name: 'files', accept: ['image/*', 'video/*'], }, ], }, }, } return acc }, {} as LocalizedWebManifest) } async function readI18nFile(file: string) { return JSON.parse(Buffer.from( await readFile(resolve(`./locales/${file}`), 'utf-8'), ).toString()) } interface PWAEntry { webmanifest?: Record<string, { name?: string short_name?: string description?: string }> } interface JsonEntry { pwa?: PWAEntry app_name?: string app_desc_short?: string } async function findBestWebManifestData(files: string[], env: string) { const entries: JsonEntry[] = await Promise.all(files.map(async (file) => { const { pwa, app_name, app_desc_short } = await readI18nFile(file) return { pwa, app_name, app_desc_short } })) let pwa: PWAEntry | undefined let app_name: string | undefined let app_desc_short: string | undefined for (const entry of entries) { const webmanifest = entry?.pwa?.webmanifest?.[env] if (webmanifest) { if (pwa) { if (webmanifest.name) pwa.webmanifest![env].name = webmanifest.name if (webmanifest.short_name) pwa.webmanifest![env].short_name = webmanifest.short_name if (webmanifest.description) pwa.webmanifest![env].description = webmanifest.description } else { pwa = entry.pwa } } if (entry.app_name) app_name = entry.app_name if (entry.app_desc_short) app_desc_short = entry.app_desc_short } return { pwa, app_name, app_desc_short } }