diff --git a/.storybook/handlers.ts b/.storybook/handlers.ts
index fa5544eced..c8e485285a 100644
--- a/.storybook/handlers.ts
+++ b/.storybook/handlers.ts
@@ -91,3 +91,68 @@ export const pdsUsersHandler = http.get('/api/atproto/pds-users', () => {
},
])
})
+
+export const i18nStatusHandler = http.get('/lunaria/status.json', () => {
+ return HttpResponse.json({
+ generatedAt: '2026-01-22T10:07:07.000Z',
+ sourceLocale: {
+ lang: 'en',
+ label: 'English',
+ totalKeys: 500,
+ },
+ locales: [
+ {
+ lang: 'en-GB',
+ label: 'English (UK)',
+ dir: 'ltr',
+ totalKeys: 500,
+ completedKeys: 423,
+ percentComplete: 84,
+ missingKeys: [
+ 'settings.background_themes.label',
+ 'settings.enable_graph_pulse_loop',
+ 'settings.enable_graph_pulse_loop_description',
+ 'settings.data_source.algolia_description',
+ 'settings.data_source.npm_description',
+ 'i18n.contribute_hint',
+ 'i18n.copy_keys',
+ ],
+ githubEditUrl: 'https://github.com/npmx-dev/npmx.dev/edit/main/i18n/locales/en-GB.json',
+ githubHistoryUrl:
+ 'https://github.com/npmx-dev/npmx.dev/commits/main/i18n/locales/en-GB.json',
+ },
+ {
+ lang: 'fr-FR',
+ label: 'Français',
+ dir: 'ltr',
+ totalKeys: 500,
+ completedKeys: 423,
+ percentComplete: 84,
+ missingKeys: [
+ 'settings.background_themes.label',
+ 'settings.enable_graph_pulse_loop',
+ 'settings.enable_graph_pulse_loop_description',
+ 'settings.data_source.algolia_description',
+ 'settings.data_source.npm_description',
+ 'i18n.contribute_hint',
+ 'i18n.copy_keys',
+ ],
+ githubEditUrl: 'https://github.com/npmx-dev/npmx.dev/edit/main/i18n/locales/fr-FR.json',
+ githubHistoryUrl:
+ 'https://github.com/npmx-dev/npmx.dev/commits/main/i18n/locales/fr-FR.json',
+ },
+ {
+ lang: 'de-DE',
+ label: 'Deutsch',
+ dir: 'ltr',
+ totalKeys: 500,
+ completedKeys: 500,
+ percentComplete: 100,
+ missingKeys: [],
+ githubEditUrl: 'https://github.com/npmx-dev/npmx.dev/edit/main/i18n/locales/de-DE.json',
+ githubHistoryUrl:
+ 'https://github.com/npmx-dev/npmx.dev/commits/main/i18n/locales/de-DE.json',
+ },
+ ],
+ })
+})
diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
index f644bb7435..c626f23ef8 100644
--- a/.storybook/preview-head.html
+++ b/.storybook/preview-head.html
@@ -61,3 +61,19 @@
background-color: var(--bg, oklch(0.171 0 0)) !important;
}
+
diff --git a/.storybook/preview.ts b/.storybook/preview.ts
index 30d8f6cfcb..1c810cf948 100644
--- a/.storybook/preview.ts
+++ b/.storybook/preview.ts
@@ -10,16 +10,6 @@ import npmxDark from './theme'
initialize()
-// related: https://github.com/npmx-dev/npmx.dev/blob/1431d24be555bca5e1ae6264434d49ca15173c43/test/nuxt/setup.ts#L12-L26
-// Stub Nuxt specific globals
-// @ts-expect-error - dynamic global name
-globalThis['__NUXT_COLOR_MODE__'] ??= {
- preference: 'system',
- value: 'dark',
- getColorScheme: fn(() => 'dark'),
- addColorScheme: fn(),
- removeColorScheme: fn(),
-}
// @ts-expect-error - dynamic global name
globalThis.defineOgImageComponent = fn()
diff --git a/app/pages/settings.stories.ts b/app/pages/settings.stories.ts
new file mode 100644
index 0000000000..1deaecbc19
--- /dev/null
+++ b/app/pages/settings.stories.ts
@@ -0,0 +1,55 @@
+import Settings from './settings.vue'
+import type { Meta, StoryObj } from '@storybook-vue/nuxt'
+import { userEvent, expect } from 'storybook/test'
+import { pageDecorator } from '../../.storybook/decorators'
+import { i18nStatusHandler } from '../../.storybook/handlers'
+
+const meta = {
+ component: Settings,
+ globals: {
+ locale: 'en-US',
+ },
+ beforeEach: () => localStorage.removeItem('npmx-settings'),
+ parameters: {
+ layout: 'fullscreen',
+ msw: {
+ handlers: [i18nStatusHandler],
+ },
+ },
+ decorators: [pageDecorator],
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+/** English locale (default). The Language section shows a GitHub link to help translate the site. */
+export const Default: Story = {}
+
+export const NpmRegistryDataSource: Story = {
+ play: async ({ canvas, step }) => {
+ await step('Select npm registry as the data source', async () => {
+ const select = await canvas.findByRole('combobox', { name: /data source/i })
+ await userEvent.selectOptions(select, 'npm')
+ await expect(select).toHaveValue('npm')
+ })
+ },
+}
+
+/** Non-English locale with incomplete translations. The Language section shows `SettingsTranslationHelper` with a progress bar and list of missing translation keys. `/lunaria/status.json` is intercepted by MSW to provide mock translation status data. */
+export const NonEnglishTranslationHelper: Story = {
+ globals: {
+ locale: 'fr-FR',
+ },
+}
+
+/** Non-English locale without translations API response. The Language section shows a GitHub link to help translate the site. */
+export const WithoutTranslationHelper: Story = {
+ globals: {
+ locale: 'fr-FR',
+ },
+ parameters: {
+ msw: {
+ handlers: [],
+ },
+ },
+}
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 3e51ad515f..943eb902dd 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -15,7 +15,8 @@ export default defineNuxtConfig({
'@vite-pwa/nuxt',
'@vueuse/nuxt',
'@nuxtjs/i18n',
- ...(isStorybook ? [] : ['@nuxt/fonts', '@nuxtjs/color-mode']),
+ '@nuxtjs/color-mode',
+ ...(isStorybook ? [] : ['@nuxt/fonts']),
],
$test: {