diff --git a/composables/setups.ts b/composables/setups.ts
index 5a9947f2..4e1c09c2 100644
--- a/composables/setups.ts
+++ b/composables/setups.ts
@@ -1,5 +1,3 @@
-import { pwaInfo } from 'virtual:pwa-info'
-import type { Link } from '@unhead/schema'
 import type { Directions } from 'vue-i18n-routing'
 import { buildInfo } from 'virtual:build-info'
 import type { LocaleObject } from '#i18n'
@@ -7,28 +5,6 @@ import type { LocaleObject } from '#i18n'
 export function setupPageHeader() {
   const { locale, locales, t } = useI18n()
 
-  const link: Link[] = []
-
-  if (pwaInfo && pwaInfo.webManifest) {
-    const { webManifest } = pwaInfo
-    if (webManifest) {
-      const { href, useCredentials } = webManifest
-      if (useCredentials) {
-        link.push({
-          rel: 'manifest',
-          href,
-          crossorigin: 'use-credentials',
-        })
-      }
-      else {
-        link.push({
-          rel: 'manifest',
-          href,
-        })
-      }
-    }
-  }
-
   const localeMap = (locales.value as LocaleObject[]).reduce((acc, l) => {
     acc[l.code!] = l.dir ?? 'auto'
     return acc
@@ -46,6 +22,12 @@ export function setupPageHeader() {
         titleTemplate += ` (${buildInfo.env})`
       return titleTemplate
     },
-    link,
+    link: process.client && useRuntimeConfig().public.pwaEnabled
+      ? () => [{
+          key: 'webmanifest',
+          rel: 'manifest',
+          href: `/manifest-${locale.value}.webmanifest`,
+        }]
+      : [],
   })
 }
diff --git a/config/pwa.ts b/config/pwa.ts
index 4711bc40..46c0a066 100644
--- a/config/pwa.ts
+++ b/config/pwa.ts
@@ -1,7 +1,5 @@
 import { isCI, isDevelopment } from 'std-env'
 import type { VitePWANuxtOptions } from '../modules/pwa/types'
-import { APP_NAME } from '../constants'
-import { getEnv } from './env'
 
 export const pwa: VitePWANuxtOptions = {
   mode: isCI ? 'production' : 'development',
@@ -13,40 +11,9 @@ export const pwa: VitePWANuxtOptions = {
   strategies: 'injectManifest',
   injectRegister: false,
   includeManifestIcons: false,
-  manifest: async () => {
-    const { env } = await getEnv()
-    const envName = `${env === 'release' ? '' : ` (${env})`}`
-    return {
-      scope: '/',
-      id: '/',
-      name: `${APP_NAME}${envName}`,
-      short_name: `${APP_NAME}${envName}`,
-      description: `A nimble Mastodon Web Client${envName}`,
-      theme_color: '#ffffff',
-      icons: [
-        {
-          src: 'pwa-192x192.png',
-          sizes: '192x192',
-          type: 'image/png',
-        },
-        {
-          src: 'pwa-512x512.png',
-          sizes: '512x512',
-          type: 'image/png',
-        },
-        /*
-        {
-          src: 'logo.svg',
-          sizes: '250x250',
-          type: 'image/png',
-          purpose: 'any maskable',
-        },
-  */
-      ],
-    }
-  },
+  manifest: false,
   injectManifest: {
-    globPatterns: ['**/*.{js,json,css,html,txt,svg,png,ico,webp,woff,woff2,ttf,eot,otf,wasm}'],
+    globPatterns: ['**/*.{js,json,css,html,txt,svg,png,ico,webp,woff,woff2,ttf,eot,otf,wasm,webmanifest}'],
     globIgnores: ['emojis/**'],
   },
   devOptions: {
diff --git a/locales/en-US.json b/locales/en-US.json
index e1a57bf9..7fcf4839 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -187,7 +187,29 @@
     "dismiss": "Dismiss",
     "title": "New Elk update available!",
     "update": "Update",
-    "update_available_short": "Update Elk"
+    "update_available_short": "Update Elk",
+    "webmanifest": {
+      "dev": {
+        "description": "A nimble Mastodon web client (dev)",
+        "name": "Elk (dev)",
+        "short_name": "Elk (dev)"
+      },
+      "main": {
+        "description": "A nimble Mastodon web client (main)",
+        "name": "Elk (main)",
+        "short_name": "Elk (main)"
+      },
+      "preview": {
+        "description": "A nimble Mastodon web client (preview)",
+        "name": "Elk (preview)",
+        "short_name": "Elk (preview)"
+      },
+      "release": {
+        "description": "A nimble Mastodon web client",
+        "name": "Elk",
+        "short_name": "Elk"
+      }
+    }
   },
   "search": {
     "search_desc": "Search for people & hashtags"
diff --git a/modules/pwa/i18n.ts b/modules/pwa/i18n.ts
new file mode 100644
index 00000000..5774f40f
--- /dev/null
+++ b/modules/pwa/i18n.ts
@@ -0,0 +1,85 @@
+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'
+
+export type LocalizedWebManifest = Record<string, Partial<ManifestOptions>>
+
+export const pwaLocales = i18n.locales as LocaleObject[]
+
+type WebManifestEntry = Pick<ManifestOptions, 'name' | 'short_name' | 'description'>
+type RequiredWebManifestEntry = Required<WebManifestEntry & Pick<ManifestOptions, 'dir' | 'lang'>>
+
+export const createI18n = async (): Promise<LocalizedWebManifest> => {
+  const { env } = await getEnv()
+  const envName = `${env === 'release' ? '' : `(${env})`}`
+  const { pwa } = await readI18nFile('en-US.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 }) => {
+        // read locale file
+        const { pwa, app_name, app_desc_short } = await readI18nFile(file!)
+        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,
+      theme_color: '#ffffff',
+      icons: [
+        {
+          src: 'pwa-192x192.png',
+          sizes: '192x192',
+          type: 'image/png',
+        },
+        {
+          src: 'pwa-512x512.png',
+          sizes: '512x512',
+          type: 'image/png',
+        },
+      ],
+    }
+
+    return acc
+  }, {} as LocalizedWebManifest)
+}
+
+async function readI18nFile(file: string) {
+  return JSON.parse(Buffer.from(
+    await readFile(resolve(`./locales/${file}`), 'utf-8'),
+  ).toString())
+}
diff --git a/modules/pwa/index.ts b/modules/pwa/index.ts
index fbf369fc..1cd02d4a 100644
--- a/modules/pwa/index.ts
+++ b/modules/pwa/index.ts
@@ -1,9 +1,10 @@
 import { defineNuxtModule } from '@nuxt/kit'
-import type { VitePWAOptions, VitePluginPWAAPI } from 'vite-plugin-pwa'
+import type { VitePluginPWAAPI } from 'vite-plugin-pwa'
 import { VitePWA } from 'vite-plugin-pwa'
 import type { Plugin } from 'vite'
 import type { VitePWANuxtOptions } from './types'
 import { configurePWAOptions } from './config'
+import { type LocalizedWebManifest, createI18n, pwaLocales } from './i18n'
 
 export * from './types'
 export default defineNuxtModule<VitePWANuxtOptions>({
@@ -20,6 +21,7 @@ export default defineNuxtModule<VitePWANuxtOptions>({
     const resolveVitePluginPWAAPI = (): VitePluginPWAAPI | undefined => {
       return vitePwaClientPlugin?.api
     }
+    let webmanifests: LocalizedWebManifest | undefined
 
     // TODO: combine with configurePWAOptions?
     nuxt.hook('nitro:init', (nitro) => {
@@ -37,12 +39,55 @@ export default defineNuxtModule<VitePWANuxtOptions>({
       const plugin = viteInlineConfig.plugins.find(p => p && typeof p === 'object' && 'name' in p && p.name === 'vite-plugin-pwa')
       if (plugin)
         throw new Error('Remove vite-plugin-pwa plugin from Vite Plugins entry in Nuxt config file!')
-      const resolvedOptions: Partial<VitePWAOptions> = {
-        ...options,
-        manifest: options.manifest ? await options.manifest() : undefined,
+
+      webmanifests = await createI18n()
+      const generateManifest = (locale: string) => {
+        const manifest = webmanifests![locale]
+        if (!manifest)
+          throw new Error(`No webmanifest found for locale ${locale}`)
+        return JSON.stringify(manifest)
       }
-      configurePWAOptions(resolvedOptions, nuxt)
-      const plugins = VitePWA(resolvedOptions)
+      viteInlineConfig.plugins.push({
+        name: 'elk:pwa:locales:build',
+        apply: 'build',
+        generateBundle(_, bundle) {
+          if (options.disable || !bundle)
+            return
+
+          Object.keys(webmanifests!).map(l => [l, `manifest-${l}.webmanifest`]).forEach(([l, fileName]) => {
+            bundle[fileName] = {
+              isAsset: true,
+              type: 'asset',
+              name: undefined,
+              source: generateManifest(l),
+              fileName,
+            }
+          })
+        },
+      })
+      viteInlineConfig.plugins.push({
+        name: 'elk:pwa:locales:dev',
+        apply: 'serve',
+        configureServer(server) {
+          const localeMatcher = new RegExp(`^${nuxt.options.app.baseURL}manifest-(.*).webmanifest$`)
+          server.middlewares.use((req, res, next) => {
+            const match = req.url?.match(localeMatcher)
+            const entry = match && webmanifests![match[1]]
+            if (entry) {
+              res.statusCode = 200
+              res.setHeader('Content-Type', 'application/manifest+json')
+              res.write(JSON.stringify(entry), 'utf-8')
+              res.end()
+            }
+            else {
+              next()
+            }
+          })
+        },
+      })
+
+      configurePWAOptions(options, nuxt)
+      const plugins = VitePWA(options)
       viteInlineConfig.plugins.push(plugins)
       if (isClient)
         vitePwaClientPlugin = plugins.find(p => p.name === 'vite-plugin-pwa') as Plugin
@@ -61,8 +106,17 @@ export default defineNuxtModule<VitePWANuxtOptions>({
           return
 
         viteServer.middlewares.stack.push({ route: webManifest, handle: emptyHandle })
+        if (webmanifests) {
+          Object.keys(webmanifests).forEach((locale) => {
+            viteServer.middlewares.stack.push({
+              route: `${nuxt.options.app.baseURL}manifest-${locale}.webmanifest`,
+              handle: emptyHandle,
+            })
+          })
+        }
         viteServer.middlewares.stack.push({ route: devSw, handle: emptyHandle })
       })
+
       if (!options.strategies || options.strategies === 'generateSW') {
         nuxt.hook('vite:serverCreated', (viteServer, { isServer }) => {
           if (isServer)
@@ -76,6 +130,16 @@ export default defineNuxtModule<VitePWANuxtOptions>({
       }
     }
     else {
+      nuxt.hook('nitro:config', async (nitroConfig) => {
+        nitroConfig.routeRules = nitroConfig.routeRules || {}
+        for (const locale of pwaLocales) {
+          nitroConfig.routeRules![`/manifest-${locale.code}.webmanifest`] = {
+            headers: {
+              'Content-Type': 'application/manifest+json',
+            },
+          }
+        }
+      })
       nuxt.hook('nitro:init', (nitro) => {
         nitro.hooks.hook('rollup:before', async () => {
           await resolveVitePluginPWAAPI()?.generateSW()
diff --git a/modules/pwa/types.ts b/modules/pwa/types.ts
index 7d3af0d5..90446c52 100644
--- a/modules/pwa/types.ts
+++ b/modules/pwa/types.ts
@@ -1,9 +1,6 @@
-import type { ManifestOptions, VitePWAOptions } from 'vite-plugin-pwa'
-import type { Overwrite } from '../../types/utils'
+import type { VitePWAOptions } from 'vite-plugin-pwa'
 
-export type VitePWANuxtOptions = Overwrite<Partial<VitePWAOptions>, {
-  manifest?: () => Promise<Partial<ManifestOptions>>
-}>
+export interface VitePWANuxtOptions extends Partial<VitePWAOptions> {}
 
 declare module '@nuxt/schema' {
   interface NuxtConfig {
diff --git a/nuxt.config.ts b/nuxt.config.ts
index c46c7bca..4b423a6e 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -113,7 +113,7 @@ export default defineNuxtConfig({
     ],
     prerender: {
       crawlLinks: false,
-      routes: ['/', '/200.html'],
+      routes: ['/'],
     },
   },
   app: {