elk/modules/pwa/i18n.ts

232 lines
5.9 KiB
TypeScript

import { Buffer } from 'node:buffer'
import { readFile } from 'fs-extra'
import { createResolver } from '@nuxt/kit'
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: {
title: string
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 async function createI18n(): 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',
purpose: 'any',
},
{
src: 'maskable-icon.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
share_target: {
action: '/web-share-target',
method: 'POST',
enctype: 'multipart/form-data',
params: {
title: 'title',
text: 'text',
url: 'url',
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',
purpose: 'any',
},
{
src: 'maskable-icon.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
share_target: {
action: '/web-share-target',
method: 'POST',
enctype: 'multipart/form-data',
params: {
title: 'title',
text: 'text',
url: 'url',
files: [
{
name: 'files',
accept: ['image/*', 'video/*'],
},
],
},
},
}
return acc
}, {} as LocalizedWebManifest)
}
async function readI18nFile(file: string) {
const { resolve } = createResolver(import.meta.url)
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 }
}