elk/modules/pwa/i18n.ts

330 lines
9.1 KiB
TypeScript

import { Buffer } from 'node:buffer'
import { readFile } from 'node:fs/promises'
import { createResolver } from '@nuxt/kit'
import type { ManifestOptions } from 'vite-plugin-pwa'
import { getEnv } from '../../config/env'
import { currentLocales } from '../../config/i18n'
import { THEME_COLORS } from '../../constants/index'
export type LocalizedWebManifest = Record<string, Partial<ManifestOptions>>
export const pwaLocales = currentLocales
type WebManifestEntry = Pick<ManifestOptions, 'name' | 'short_name' | 'description' | 'screenshots' | 'shortcuts'>
type RequiredWebManifestEntry = Required<WebManifestEntry & Pick<ManifestOptions, 'dir' | 'lang' | 'screenshots' | 'shortcuts'>>
export async function createI18n(): Promise<LocalizedWebManifest> {
const { env } = await getEnv()
const envName = `${env === 'release' ? '' : `(${env})`}`
const { action, nav, pwa } = await readI18nFile('en.json')
const defaultManifest: Required<WebManifestEntry> = pwa.webmanifest[env]
const defaultShortcuts: ManifestOptions['shortcuts'] = [{
name: nav.home,
url: '/home',
icons: [
{ src: 'shortcuts/home-96x96.png', sizes: '96x96', type: 'image/png' },
{ src: 'shortcuts/home.png', sizes: '192x192', type: 'image/png' },
],
}, {
name: nav.local,
url: '/?local-pwa-shortcut=true',
icons: [
{ src: 'shortcuts/local-96x96.png', sizes: '96x96', type: 'image/png' },
{ src: 'shortcuts/local.png', sizes: '192x192', type: 'image/png' },
],
}, {
name: nav.notifications,
url: '/?notifications-pwa-shortcut=true',
icons: [
{ src: 'shortcuts/notifications-96x96.png', sizes: '96x96', type: 'image/png' },
{ src: 'shortcuts/notifications.png', sizes: '192x192', type: 'image/png' },
],
}, {
name: action.compose,
url: '/compose',
icons: [
{ src: 'shortcuts/compose-96x96.png', sizes: '96x96', type: 'image/png' },
{ src: 'shortcuts/compose.png', sizes: '192x192', type: 'image/png' },
],
}, {
name: nav.settings,
url: '/settings',
icons: [
{ src: 'shortcuts/settings-96x96.png', sizes: '96x96', type: 'image/png' },
{ src: 'shortcuts/settings.png', sizes: '192x192', type: 'image/png' },
],
}]
const defaultScreenshots: ManifestOptions['screenshots'] = [{
src: 'screenshots/dark-1.webp',
sizes: '3840x2400',
type: 'image/webp',
label: pwa.screenshots.dark,
}, {
src: 'screenshots/light-1.webp',
sizes: '3840x2400',
type: 'image/webp',
label: pwa.screenshots.light,
}]
const manifestEntries: Partial<ManifestOptions> = {
scope: '/',
id: '/',
start_url: '/',
orientation: 'natural',
display: 'standalone',
display_override: ['window-controls-overlay'],
categories: ['social', 'social networking', 'news'],
icons: [
{
src: 'pwa-64x64.png',
sizes: '64x64',
type: 'image/png',
},
{
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/*'],
},
],
},
},
handle_links: 'preferred',
launch_handler: {
client_mode: ['navigate-existing', 'auto'],
},
edge_side_panel: {
preferred_width: 480,
},
}
if (env === 'release') {
manifestEntries.prefer_related_applications = true
manifestEntries.related_applications = [{
platform: 'windows',
url: 'https://www.microsoft.com/store/apps/9PNZMMXQHQZ5',
id: '53213ElkTeam.Elk_6x2f3wfg7gnst',
}]
}
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 { action, app_desc_short, app_name, nav, pwa } = file
? await readI18nFile(file as string)
: await findBestWebManifestData(files as string[], env)
const entry = 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
// clone default screenshots and shortcuts
const useScreenshots = [...defaultScreenshots.map(screenshot => ({ ...screenshot }))]
const useShortcuts = [...defaultShortcuts.map(shortcut => ({ ...shortcut }))]
const pwaScreenshots = pwa?.screenshots
if (pwaScreenshots) {
useScreenshots.forEach((screenshot, idx) => {
if (idx === 0 && pwaScreenshots?.dark)
screenshot.label = pwaScreenshots.dark
if (idx === 1 && pwaScreenshots?.light)
screenshot.label = pwaScreenshots.light
})
}
useShortcuts.forEach((shortcut, idx) => {
if (idx === 0 && nav?.home)
shortcut.name = nav.home
if (idx === 1 && nav?.local)
shortcut.name = nav.local
if (idx === 2 && nav?.notifications)
shortcut.name = nav.notifications
if (idx === 3 && action?.compose)
shortcut.name = action?.compose
if (idx === 4 && nav?.settings)
shortcut.name = nav.settings
})
return <RequiredWebManifestEntry>{
...defaultManifest,
...entry,
lang: code,
dir,
screenshots: useScreenshots,
shortcuts: useShortcuts,
}
}),
)
locales.push({
...defaultManifest,
lang: 'en-US',
dir: 'ltr',
screenshots: defaultScreenshots,
shortcuts: defaultShortcuts,
})
return locales.reduce((acc, {
lang,
dir,
name,
short_name,
description,
shortcuts,
screenshots,
}) => {
acc[lang] = {
lang,
name,
short_name,
description,
dir,
background_color: THEME_COLORS.backgroundLight,
theme_color: THEME_COLORS.themeLight,
...manifestEntries,
shortcuts,
screenshots,
}
acc[`${lang}-dark`] = {
lang,
name,
short_name,
description,
dir,
background_color: THEME_COLORS.backgroundDark,
theme_color: THEME_COLORS.themeDark,
...manifestEntries,
shortcuts,
screenshots,
}
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
}>
screenshots?: Record<string, string>
shortcuts?: Record<string, string>
}
interface JsonEntry {
pwa?: PWAEntry
app_name?: string
app_desc_short?: string
action?: Record<string, any>
nav?: Record<string, any>
screenshots?: Record<string, string>
}
async function findBestWebManifestData(files: string[], env: string) {
const entries: JsonEntry[] = await Promise.all(files.map(async (file) => {
const { action, app_name, app_desc_short, nav, pwa } = await readI18nFile(file)
return { action, app_name, app_desc_short, nav, pwa }
}))
let pwa: PWAEntry | undefined
let app_name: string | undefined
let app_desc_short: string | undefined
const action: Record<string, any> = {}
const nav: Record<string, any> = {}
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
if (entry.nav) {
['home', 'local', 'notifications', 'settings'].forEach((key) => {
const value = entry.nav![key]
if (value)
nav[key] = value
})
}
if (entry.action?.compose)
action.compose = entry.action.compose
if (entry.pwa?.screenshots) {
if (!pwa)
pwa = {}
pwa.screenshots = pwa.screenshots ?? {}
Object
.entries(entry.pwa.screenshots)
.forEach(([key, value]) => pwa!.screenshots![key] = value)
}
}
return { action, app_desc_short, app_name, nav, pwa }
}