diff --git a/client/src/features/dashboard/components/DashboardSettingsSheet.spec.ts b/client/src/features/dashboard/components/DashboardSettingsSheet.spec.ts index 38243e69b..b43e311aa 100644 --- a/client/src/features/dashboard/components/DashboardSettingsSheet.spec.ts +++ b/client/src/features/dashboard/components/DashboardSettingsSheet.spec.ts @@ -1,7 +1,11 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { mount } from '@vue/test-utils' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { mount, type VueWrapper } from '@vue/test-utils' import { nextTick, ref, type Ref } from 'vue' -import type { WidgetConfig } from '@bookorbit/types' +import { WIDGET_TYPES, type WidgetConfig } from '@bookorbit/types' + +import { setI18nLocale } from '@/i18n' +import en from '@/locales/en.json' +import pt from '@/locales/pt.json' type UseSmartScopesMock = () => { smartScopes: Ref @@ -11,10 +15,11 @@ type UseSmartScopesMock = () => { type UseDashboardWidgetsMock = () => { widgets: Ref saveWidgets: (widgets: WidgetConfig[]) => Promise - WIDGET_LABELS: Record DEFAULT_WIDGETS: WidgetConfig[] } +const widgetsRef = ref([]) + vi.mock('@/components/ui/sheet', () => { const passthrough = { template: '
' } return { @@ -34,39 +39,127 @@ vi.mock('@/features/smart-scope/composables/useSmartScopes', () => ({ vi.mock('../composables/useDashboardWidgets', () => ({ useDashboardWidgets: vi.fn(() => ({ - widgets: ref([]), + widgets: widgetsRef, saveWidgets: vi.fn<(widgets: WidgetConfig[]) => Promise>(), - WIDGET_LABELS: {}, DEFAULT_WIDGETS: [], })), })) import DashboardSettingsSheet from './DashboardSettingsSheet.vue' +const ALL_WIDGETS: WidgetConfig[] = WIDGET_TYPES.map((type, index) => ({ + id: String(index + 1), + type, + enabled: true, + order: index + 1, +})) + +async function openSheet(): Promise { + const wrapper = mount(DashboardSettingsSheet, { props: { open: false } }) + await wrapper.setProps({ open: true }) + await nextTick() + return wrapper +} + +async function openShelvesTab(wrapper: VueWrapper): Promise { + const shelvesTab = wrapper.findAll('button').find((button) => button.text() === en.dashboard.settings.tabs.shelves) + await shelvesTab?.trigger('click') +} + +function widgetRowLabels(wrapper: VueWrapper): string[] { + return wrapper.findAll('span.flex-1').map((span) => span.text()) +} + +function shelfOptionLabels(wrapper: VueWrapper): string[] { + return wrapper + .find('select') + .findAll('option') + .map((option) => option.text()) +} + +beforeEach(async () => { + vi.clearAllMocks() + localStorage.clear() + widgetsRef.value = [] + await setI18nLocale('en') +}) + +afterEach(async () => { + await setI18nLocale('en') +}) + describe('DashboardSettingsSheet', () => { - beforeEach(() => { - vi.clearAllMocks() - localStorage.clear() + it('includes continue-listening and want-to-read in the shelf selector', async () => { + const wrapper = await openSheet() + await openShelvesTab(wrapper) + + const optionLabels = shelfOptionLabels(wrapper) + + expect(optionLabels).toContain(en.dashboard.settings.shelfNames.continueListening) + expect(optionLabels).toContain(en.dashboard.settings.shelfNames.wantToRead) }) - it('includes continue-listening and want-to-read in the shelf selector', async () => { - const wrapper = mount(DashboardSettingsSheet, { - props: { open: false }, - }) + it('lists every shelf type in the selector using catalog names', async () => { + const wrapper = await openSheet() + await openShelvesTab(wrapper) - await wrapper.setProps({ open: true }) + expect(shelfOptionLabels(wrapper).sort()).toEqual(Object.values(en.dashboard.settings.shelfNames).sort()) + }) + + it('translates the shelf selector when the locale changes', async () => { + const wrapper = await openSheet() + await openShelvesTab(wrapper) + + await setI18nLocale('pt') await nextTick() - await wrapper - .findAll('button') - .find((button) => button.text() === 'Shelves') - ?.trigger('click') - const optionLabels = wrapper - .find('select') - .findAll('option') - .map((option) => option.text()) + expect(shelfOptionLabels(wrapper).sort()).toEqual(Object.values(pt.dashboard.settings.shelfNames).sort()) + }) - expect(optionLabels).toContain('Continue Listening') - expect(optionLabels).toContain('Want to Read') + it('renders every widget name from the catalog rather than a hardcoded map', async () => { + widgetsRef.value = ALL_WIDGETS + const wrapper = await openSheet() + + expect(widgetRowLabels(wrapper)).toEqual([ + en.dashboard.settings.widgetNames.readingStreak, + en.dashboard.settings.widgetNames.currentlyReading, + en.dashboard.settings.widgetNames.readingGoal, + en.dashboard.settings.widgetNames.readingDna, + en.dashboard.settings.widgetNames.monthlyChallenge, + en.dashboard.settings.widgetNames.highlightOfTheDay, + en.dashboard.settings.widgetNames.neglectedGems, + en.dashboard.settings.widgetNames.readingRhythm, + en.dashboard.settings.widgetNames.diversityScore, + en.dashboard.settings.widgetNames.libraryOverview, + en.dashboard.settings.widgetNames.yearProjection, + en.dashboard.settings.widgetNames.longWait, + ]) + }) + + it('translates widget names when the locale changes (issue #796)', async () => { + widgetsRef.value = ALL_WIDGETS + const wrapper = await openSheet() + + await setI18nLocale('pt') + await nextTick() + + const labels = widgetRowLabels(wrapper) + + expect(labels).toEqual([ + pt.dashboard.settings.widgetNames.readingStreak, + pt.dashboard.settings.widgetNames.currentlyReading, + pt.dashboard.settings.widgetNames.readingGoal, + pt.dashboard.settings.widgetNames.readingDna, + pt.dashboard.settings.widgetNames.monthlyChallenge, + pt.dashboard.settings.widgetNames.highlightOfTheDay, + pt.dashboard.settings.widgetNames.neglectedGems, + pt.dashboard.settings.widgetNames.readingRhythm, + pt.dashboard.settings.widgetNames.diversityScore, + pt.dashboard.settings.widgetNames.libraryOverview, + pt.dashboard.settings.widgetNames.yearProjection, + pt.dashboard.settings.widgetNames.longWait, + ]) + expect(labels).not.toContain(en.dashboard.settings.widgetNames.readingStreak) + expect(labels).not.toContain(en.dashboard.settings.widgetNames.currentlyReading) }) }) diff --git a/client/src/features/dashboard/components/DashboardSettingsSheet.vue b/client/src/features/dashboard/components/DashboardSettingsSheet.vue index f35138d58..ecbb59a7d 100644 --- a/client/src/features/dashboard/components/DashboardSettingsSheet.vue +++ b/client/src/features/dashboard/components/DashboardSettingsSheet.vue @@ -7,6 +7,7 @@ import type { ScrollerConfig, ScrollerType, WidgetConfig } from '@bookorbit/type import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet' import { useSmartScopes } from '@/features/smart-scope/composables/useSmartScopes' import { DEFAULT_SCROLLERS, SCROLLER_LABELS, useDashboardConfig } from '../composables/useDashboardConfig' +import { useDashboardLabels } from '../composables/useDashboardLabels' import { useDashboardWidgets } from '../composables/useDashboardWidgets' import { useDraggableList } from '../composables/useDraggableList' @@ -16,8 +17,9 @@ const emit = defineEmits<{ 'update:open': [value: boolean] }>() const { t } = useI18n() const { scrollers, saveScrollers, MAX_SCROLLERS } = useDashboardConfig() -const { widgets, saveWidgets, WIDGET_LABELS, DEFAULT_WIDGETS } = useDashboardWidgets() +const { widgets, saveWidgets, DEFAULT_WIDGETS } = useDashboardWidgets() const { smartScopes, fetchSmartScopes } = useSmartScopes() +const { widgetName, shelfTypeName } = useDashboardLabels() const activeTab = ref<'widgets' | 'shelves'>('widgets') const draft = ref([]) @@ -211,7 +213,7 @@ function resetToDefault() { /> - {{ WIDGET_LABELS[widget.type] }} + {{ widgetName(widget.type) }} @@ -277,8 +279,8 @@ function resetToDefault() { class="h-8 flex-1 appearance-none rounded-md border border-input bg-background px-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-ring" @change="onTypeChange(scroller)" > - diff --git a/client/src/features/dashboard/composables/__tests__/useDashboardLabels.spec.ts b/client/src/features/dashboard/composables/__tests__/useDashboardLabels.spec.ts new file mode 100644 index 000000000..aa01791b7 --- /dev/null +++ b/client/src/features/dashboard/composables/__tests__/useDashboardLabels.spec.ts @@ -0,0 +1,216 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import { SCROLLER_TYPES, WIDGET_TYPES, type ScrollerConfig, type ScrollerType, type WidgetType } from '@bookorbit/types' + +import { i18n, setI18nLocale } from '@/i18n' +import en from '@/locales/en.json' +import pt from '@/locales/pt.json' +import { useDashboardLabels } from '../useDashboardLabels' + +// The composable calls useI18n(), so it has to run inside a component setup. +function mountComposable(): ReturnType { + let result!: ReturnType + mount( + { + setup() { + result = useDashboardLabels() + return () => null + }, + }, + { global: { plugins: [i18n] } }, + ) + return result +} + +type CatalogNames = { widgetNames: Record; shelfNames: Record } + +const CATALOGS: Record<'en' | 'pt', CatalogNames> = { + en: en.dashboard.settings, + pt: pt.dashboard.settings, +} + +// Mirrors the kebab-case type to camel-case message key mapping the composable owns. +// Declared independently so a mistake in either map fails the test rather than cancelling out. +const WIDGET_KEY_BY_TYPE: Record = { + 'reading-streak': 'readingStreak', + 'currently-reading': 'currentlyReading', + 'reading-goal': 'readingGoal', + 'reading-dna': 'readingDna', + 'monthly-challenge': 'monthlyChallenge', + 'highlight-of-the-day': 'highlightOfTheDay', + 'neglected-gems': 'neglectedGems', + 'reading-rhythm': 'readingRhythm', + 'diversity-score': 'diversityScore', + 'library-overview': 'libraryOverview', + 'year-projection': 'yearProjection', + 'long-wait': 'longWait', +} + +const SHELF_KEY_BY_TYPE: Record = { + 'recently-added': 'recentlyAdded', + 'continue-reading': 'continueReading', + 'continue-listening': 'continueListening', + 'want-to-read': 'wantToRead', + 'up-next-in-series': 'upNextInSeries', + random: 'random', + 'smart-scope': 'smartScope', +} + +function smartScopeShelf(overrides: Partial = {}): ScrollerConfig { + return { id: '1', type: 'smart-scope', label: 'Unread Favorites', enabled: true, order: 1, limit: 20, smartScopeId: 42, ...overrides } +} + +beforeEach(async () => { + await setI18nLocale('en') +}) + +afterEach(async () => { + await setI18nLocale('en') +}) + +describe('useDashboardLabels', () => { + describe('widgetName', () => { + it('resolves every widget type through the English catalog', () => { + const { widgetName } = mountComposable() + + expect(WIDGET_TYPES.length).toBe(Object.keys(CATALOGS.en.widgetNames).length) + for (const type of WIDGET_TYPES) { + const key = WIDGET_KEY_BY_TYPE[type] + expect(widgetName(type), `widget name for ${type}`).toBe(CATALOGS.en.widgetNames[key]) + } + }) + + it('never leaks a raw message path for any widget type', () => { + const { widgetName } = mountComposable() + + for (const type of WIDGET_TYPES) { + const name = widgetName(type) + expect(name.length, `widget name for ${type}`).toBeGreaterThan(0) + expect(name, `widget name for ${type}`).not.toContain('dashboard.settings') + } + }) + + it('gives every widget type a distinct name so the customization list is unambiguous', () => { + const { widgetName } = mountComposable() + + const names = WIDGET_TYPES.map((type) => widgetName(type)) + + expect(new Set(names).size).toBe(WIDGET_TYPES.length) + }) + + it('follows the active locale instead of rendering hardcoded English (issue #796)', async () => { + const { widgetName } = mountComposable() + + await setI18nLocale('pt') + + for (const type of WIDGET_TYPES) { + const key = WIDGET_KEY_BY_TYPE[type] + expect(widgetName(type), `widget name for ${type}`).toBe(CATALOGS.pt.widgetNames[key]) + } + expect(widgetName('reading-streak')).not.toBe(CATALOGS.en.widgetNames.readingStreak) + expect(widgetName('currently-reading')).not.toBe(CATALOGS.en.widgetNames.currentlyReading) + }) + + it('re-resolves when the locale changes back', async () => { + const { widgetName } = mountComposable() + + await setI18nLocale('pt') + const translated = widgetName('reading-streak') + await setI18nLocale('en') + + expect(translated).not.toBe(widgetName('reading-streak')) + expect(widgetName('reading-streak')).toBe(CATALOGS.en.widgetNames.readingStreak) + }) + }) + + describe('shelfTypeName', () => { + it('resolves every scroller type through the English catalog', () => { + const { shelfTypeName } = mountComposable() + + expect(SCROLLER_TYPES.length).toBe(Object.keys(CATALOGS.en.shelfNames).length) + for (const type of SCROLLER_TYPES) { + const key = SHELF_KEY_BY_TYPE[type] + expect(shelfTypeName(type), `shelf name for ${type}`).toBe(CATALOGS.en.shelfNames[key]) + } + }) + + it('gives every scroller type a distinct name so the type selector is unambiguous', () => { + const { shelfTypeName } = mountComposable() + + const names = SCROLLER_TYPES.map((type) => shelfTypeName(type)) + + expect(new Set(names).size).toBe(SCROLLER_TYPES.length) + }) + + it('follows the active locale', async () => { + const { shelfTypeName } = mountComposable() + + await setI18nLocale('pt') + + for (const type of SCROLLER_TYPES) { + const key = SHELF_KEY_BY_TYPE[type] + expect(shelfTypeName(type), `shelf name for ${type}`).toBe(CATALOGS.pt.shelfNames[key]) + } + }) + }) + + describe('shelfTitle', () => { + it('ignores the persisted English label and resolves built-in shelves from their type', async () => { + const { shelfTitle, shelfTypeName } = mountComposable() + // Configs saved before localization stored a fixed English label that would otherwise + // survive a language change. + const stored: ScrollerConfig = { id: '1', type: 'continue-reading', label: 'Continue Reading', enabled: true, order: 1, limit: 20 } + + expect(shelfTitle(stored)).toBe(CATALOGS.en.shelfNames.continueReading) + + await setI18nLocale('pt') + + expect(shelfTitle(stored)).toBe(CATALOGS.pt.shelfNames.continueReading) + expect(shelfTitle(stored)).toBe(shelfTypeName('continue-reading')) + }) + + it('resolves every built-in shelf type from its type rather than its label', () => { + const { shelfTitle, shelfTypeName } = mountComposable() + + for (const type of SCROLLER_TYPES) { + if (type === 'smart-scope') continue + const stored: ScrollerConfig = { id: '1', type, label: 'Stale Stored Label', enabled: true, order: 1, limit: 20 } + expect(shelfTitle(stored), `shelf title for ${type}`).toBe(shelfTypeName(type)) + } + }) + + it('keeps the user-authored smart scope name untranslated', async () => { + const { shelfTitle } = mountComposable() + const shelf = smartScopeShelf({ label: 'Unread Favorites' }) + + expect(shelfTitle(shelf)).toBe('Unread Favorites') + + await setI18nLocale('pt') + + expect(shelfTitle(shelf)).toBe('Unread Favorites') + }) + + it('trims surrounding whitespace from a smart scope name', () => { + const { shelfTitle } = mountComposable() + + expect(shelfTitle(smartScopeShelf({ label: ' Unread Favorites ' }))).toBe('Unread Favorites') + }) + + it('falls back to the localized generic name when a smart scope shelf references no scope', async () => { + const { shelfTitle } = mountComposable() + const shelf = smartScopeShelf({ smartScopeId: undefined, label: 'SmartScope' }) + + expect(shelfTitle(shelf)).toBe(CATALOGS.en.shelfNames.smartScope) + + await setI18nLocale('pt') + + expect(shelfTitle(shelf)).toBe(CATALOGS.pt.shelfNames.smartScope) + }) + + it('falls back to the localized generic name when the smart scope name is blank', () => { + const { shelfTitle } = mountComposable() + + expect(shelfTitle(smartScopeShelf({ label: ' ' }))).toBe(CATALOGS.en.shelfNames.smartScope) + }) + }) +}) diff --git a/client/src/features/dashboard/composables/useDashboardConfig.ts b/client/src/features/dashboard/composables/useDashboardConfig.ts index 72d0ef00f..b32124fda 100644 --- a/client/src/features/dashboard/composables/useDashboardConfig.ts +++ b/client/src/features/dashboard/composables/useDashboardConfig.ts @@ -14,6 +14,8 @@ export const DEFAULT_SCROLLERS: ScrollerConfig[] = [ { id: '4', type: 'up-next-in-series', label: 'Up Next in Series', enabled: false, order: 6, limit: 20 }, ] +// Persisted-only. Shelf headings and the type selector resolve their text from the active +// locale via useDashboardLabels(); these values just keep stored configs shaped as before. export const SCROLLER_LABELS: Record = { 'continue-reading': 'Continue Reading', 'continue-listening': 'Continue Listening', diff --git a/client/src/features/dashboard/composables/useDashboardLabels.ts b/client/src/features/dashboard/composables/useDashboardLabels.ts new file mode 100644 index 000000000..d73521cb4 --- /dev/null +++ b/client/src/features/dashboard/composables/useDashboardLabels.ts @@ -0,0 +1,57 @@ +import { useI18n } from 'vue-i18n' + +import type { ScrollerConfig, ScrollerType, WidgetType } from '@bookorbit/types' +import type { MessageSchema } from '@/i18n' + +type WidgetNameKey = keyof MessageSchema['dashboard']['settings']['widgetNames'] +type ShelfNameKey = keyof MessageSchema['dashboard']['settings']['shelfNames'] + +// Typed against the English catalog so a renamed or removed message fails typecheck +// instead of silently rendering a raw key path. +const WIDGET_NAME_KEYS: Record = { + 'reading-streak': 'readingStreak', + 'currently-reading': 'currentlyReading', + 'reading-goal': 'readingGoal', + 'reading-dna': 'readingDna', + 'monthly-challenge': 'monthlyChallenge', + 'highlight-of-the-day': 'highlightOfTheDay', + 'neglected-gems': 'neglectedGems', + 'reading-rhythm': 'readingRhythm', + 'diversity-score': 'diversityScore', + 'library-overview': 'libraryOverview', + 'year-projection': 'yearProjection', + 'long-wait': 'longWait', +} + +const SHELF_NAME_KEYS: Record = { + 'recently-added': 'recentlyAdded', + 'continue-reading': 'continueReading', + 'continue-listening': 'continueListening', + 'want-to-read': 'wantToRead', + 'up-next-in-series': 'upNextInSeries', + random: 'random', + 'smart-scope': 'smartScope', +} + +export function useDashboardLabels() { + const { t } = useI18n() + + function widgetName(type: WidgetType): string { + return t(`dashboard.settings.widgetNames.${WIDGET_NAME_KEYS[type]}`) + } + + function shelfTypeName(type: ScrollerType): string { + return t(`dashboard.settings.shelfNames.${SHELF_NAME_KEYS[type]}`) + } + + // A Smart Scope shelf is titled with the user's own scope name, which is content and stays + // untranslated. Every other shelf resolves from its type: the persisted `label` predates + // localization and holds a fixed English string that would survive a language change. + function shelfTitle(scroller: ScrollerConfig): string { + if (scroller.type !== 'smart-scope') return shelfTypeName(scroller.type) + const smartScopeName = scroller.smartScopeId ? scroller.label.trim() : '' + return smartScopeName || shelfTypeName('smart-scope') + } + + return { widgetName, shelfTypeName, shelfTitle } +} diff --git a/client/src/features/dashboard/composables/useDashboardWidgets.ts b/client/src/features/dashboard/composables/useDashboardWidgets.ts index b0bb84018..0afd7f230 100644 --- a/client/src/features/dashboard/composables/useDashboardWidgets.ts +++ b/client/src/features/dashboard/composables/useDashboardWidgets.ts @@ -5,21 +5,6 @@ import { WIDGET_TYPES } from '@bookorbit/types' import { api } from '@/lib/api' import { useAuth } from '@/features/auth/composables/useAuth' -const WIDGET_LABELS: Record = { - 'reading-goal': 'Reading Goal', - 'currently-reading': 'Currently Reading', - 'reading-streak': 'Reading Streak', - 'library-overview': 'Library Overview', - 'highlight-of-the-day': 'Highlight of the Day', - 'monthly-challenge': 'Monthly Challenge', - 'year-projection': 'Year Projection', - 'neglected-gems': 'Neglected Gems', - 'reading-dna': 'Reading DNA', - 'long-wait': 'The Long Wait', - 'diversity-score': 'Diversity Score', - 'reading-rhythm': 'Reading Rhythm', -} - const DEFAULT_WIDGETS: WidgetConfig[] = [ { id: '1', type: 'reading-streak', enabled: true, order: 1 }, { id: '2', type: 'currently-reading', enabled: true, order: 2 }, @@ -134,7 +119,6 @@ export function useDashboardWidgets() { readingGoal, saveWidgets, saveReadingGoal, - WIDGET_LABELS, DEFAULT_WIDGETS, } } diff --git a/client/src/locales/de.json b/client/src/locales/de.json index 29f368c7c..664976ef4 100644 --- a/client/src/locales/de.json +++ b/client/src/locales/de.json @@ -4584,6 +4584,29 @@ "widgets": "Widgets", "shelves": "Regale" }, + "widgetNames": { + "readingStreak": "Lese-Serie", + "currentlyReading": "Aktuell gelesen", + "readingGoal": "Reading Goal", + "readingDna": "Lese-DNA", + "monthlyChallenge": "Monatliche Herausforderung", + "highlightOfTheDay": "Highlight des Tages", + "neglectedGems": "Vernachlässigte Schätze", + "readingRhythm": "Leserhythmus", + "diversityScore": "Diversitätswert", + "libraryOverview": "Bibliotheksübersicht", + "yearProjection": "Jahresprognose", + "longWait": "Die lange Wartezeit" + }, + "shelfNames": { + "recentlyAdded": "Recently Added", + "continueReading": "Continue Reading", + "continueListening": "Continue Listening", + "wantToRead": "Want to Read", + "upNextInSeries": "Up Next in Series", + "random": "Discover Something New", + "smartScope": "Smart Scope" + }, "reorderHintWidget": "Ziehen zum Neuanordnen. Umschalten, um ein Widget ein- oder auszublenden.", "reorderHintShelf": "Ziehen zum Neuanordnen. Umschalten, um ein Regal ein- oder auszublenden.", "smartScope": "Smart Scope", diff --git a/client/src/locales/en.json b/client/src/locales/en.json index b81157e99..e9a1a21b4 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -4584,6 +4584,29 @@ "widgets": "Widgets", "shelves": "Shelves" }, + "widgetNames": { + "readingStreak": "Reading Streak", + "currentlyReading": "Currently Reading", + "readingGoal": "Reading Goal", + "readingDna": "Reading DNA", + "monthlyChallenge": "Monthly Challenge", + "highlightOfTheDay": "Highlight of the Day", + "neglectedGems": "Neglected Gems", + "readingRhythm": "Reading Rhythm", + "diversityScore": "Diversity Score", + "libraryOverview": "Library Overview", + "yearProjection": "Year Projection", + "longWait": "The Long Wait" + }, + "shelfNames": { + "recentlyAdded": "Recently Added", + "continueReading": "Continue Reading", + "continueListening": "Continue Listening", + "wantToRead": "Want to Read", + "upNextInSeries": "Up Next in Series", + "random": "Discover Something New", + "smartScope": "Smart Scope" + }, "reorderHintWidget": "Drag to reorder. Toggle to show or hide a widget.", "reorderHintShelf": "Drag to reorder. Toggle to show or hide a shelf.", "smartScope": "Smart Scope", diff --git a/client/src/locales/es.json b/client/src/locales/es.json index f155306a3..590ee64f8 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -4584,6 +4584,29 @@ "widgets": "widgets", "shelves": "estantes" }, + "widgetNames": { + "readingStreak": "Racha de lectura", + "currentlyReading": "Actualmente leyendo", + "readingGoal": "Reading Goal", + "readingDna": "Lectura de ADN", + "monthlyChallenge": "Reto Mensual", + "highlightOfTheDay": "Lo más destacado del día", + "neglectedGems": "Gemas olvidadas", + "readingRhythm": "Ritmo de lectura", + "diversityScore": "Puntuación de diversidad", + "libraryOverview": "Descripción general de la biblioteca", + "yearProjection": "Proyección del año", + "longWait": "La larga espera" + }, + "shelfNames": { + "recentlyAdded": "Recently Added", + "continueReading": "Continue Reading", + "continueListening": "Continue Listening", + "wantToRead": "Want to Read", + "upNextInSeries": "Up Next in Series", + "random": "Discover Something New", + "smartScope": "Smart Scope" + }, "reorderHintWidget": "Arrastre para reordenar. Alternar para mostrar u ocultar un widget.", "reorderHintShelf": "Arrastre para reordenar. Alternar para mostrar u ocultar un estante.", "smartScope": "Alcance inteligente", diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json index f567190bd..63bc9f1fb 100644 --- a/client/src/locales/fr.json +++ b/client/src/locales/fr.json @@ -4584,6 +4584,29 @@ "widgets": "Widgets", "shelves": "Étagères" }, + "widgetNames": { + "readingStreak": "Série de lecture", + "currentlyReading": "Lecture en cours", + "readingGoal": "Reading Goal", + "readingDna": "Lire l'ADN", + "monthlyChallenge": "Défi mensuel", + "highlightOfTheDay": "Le moment fort de la journée", + "neglectedGems": "Joyaux négligés", + "readingRhythm": "Rythme de lecture", + "diversityScore": "Score de diversité", + "libraryOverview": "Présentation de la bibliothèque", + "yearProjection": "Année projetée", + "longWait": "La longue attente" + }, + "shelfNames": { + "recentlyAdded": "Recently Added", + "continueReading": "Continue Reading", + "continueListening": "Continue Listening", + "wantToRead": "Want to Read", + "upNextInSeries": "Up Next in Series", + "random": "Discover Something New", + "smartScope": "Smart Scope" + }, "reorderHintWidget": "Faites glisser pour réorganiser. Basculez pour afficher ou masquer un widget.", "reorderHintShelf": "Faites glisser pour réorganiser. Basculez pour afficher ou masquer une étagère.", "smartScope": "Portée intelligente", diff --git a/client/src/locales/it.json b/client/src/locales/it.json index f9eb92100..c62d44ee6 100644 --- a/client/src/locales/it.json +++ b/client/src/locales/it.json @@ -4584,6 +4584,29 @@ "widgets": "Widget", "shelves": "Scaffali" }, + "widgetNames": { + "readingStreak": "Striscia di lettura", + "currentlyReading": "Attualmente sto leggendo", + "readingGoal": "Reading Goal", + "readingDna": "Leggere il DNA", + "monthlyChallenge": "Sfida mensile", + "highlightOfTheDay": "Momento clou della giornata", + "neglectedGems": "Gemme trascurate", + "readingRhythm": "Ritmo di lettura", + "diversityScore": "Punteggio di diversità", + "libraryOverview": "Panoramica della biblioteca", + "yearProjection": "Proiezione dell'anno", + "longWait": "La lunga attesa" + }, + "shelfNames": { + "recentlyAdded": "Recently Added", + "continueReading": "Continue Reading", + "continueListening": "Continue Listening", + "wantToRead": "Want to Read", + "upNextInSeries": "Up Next in Series", + "random": "Discover Something New", + "smartScope": "Smart Scope" + }, "reorderHintWidget": "Trascina per riordinare. Seleziona per mostrare o nascondere un widget.", "reorderHintShelf": "Trascina per riordinare. Attiva/disattiva per mostrare o nascondere uno scaffale.", "smartScope": "Ambito intelligente", diff --git a/client/src/locales/nl.json b/client/src/locales/nl.json index c0e46d47f..ddd4aa2a6 100644 --- a/client/src/locales/nl.json +++ b/client/src/locales/nl.json @@ -4584,6 +4584,29 @@ "widgets": "Widgets", "shelves": "Planken" }, + "widgetNames": { + "readingStreak": "Leesreeks", + "currentlyReading": "Nu aan het lezen", + "readingGoal": "Reading Goal", + "readingDna": "Lees-DNA", + "monthlyChallenge": "Maandelijkse uitdaging", + "highlightOfTheDay": "Markering van de dag", + "neglectedGems": "Vergeten pareltjes", + "readingRhythm": "Leesritme", + "diversityScore": "Diversiteitsscore", + "libraryOverview": "Bibliotheekoverzicht", + "yearProjection": "Jaarprognose", + "longWait": "Het lange wachten" + }, + "shelfNames": { + "recentlyAdded": "Recently Added", + "continueReading": "Continue Reading", + "continueListening": "Continue Listening", + "wantToRead": "Want to Read", + "upNextInSeries": "Up Next in Series", + "random": "Discover Something New", + "smartScope": "Smart Scope" + }, "reorderHintWidget": "Sleep om te herordenen. Schakel in of uit om een widget te tonen of verbergen.", "reorderHintShelf": "Sleep om te herordenen. Schakel in of uit om een plank te tonen of verbergen.", "smartScope": "Smart Scope", diff --git a/client/src/locales/pl.json b/client/src/locales/pl.json index e05d42b9e..2ce886c42 100644 --- a/client/src/locales/pl.json +++ b/client/src/locales/pl.json @@ -4584,6 +4584,29 @@ "widgets": "Widżety", "shelves": "Półki" }, + "widgetNames": { + "readingStreak": "Czytelnicza passa", + "currentlyReading": "Aktualnie czytam", + "readingGoal": "Reading Goal", + "readingDna": "Czytanie DNA", + "monthlyChallenge": "Comiesięczne wyzwanie", + "highlightOfTheDay": "Najważniejsza atrakcja dnia", + "neglectedGems": "Zaniedbane klejnoty", + "readingRhythm": "Rytm czytania", + "diversityScore": "Wynik różnorodności", + "libraryOverview": "Przegląd biblioteki", + "yearProjection": "Projekcja roku", + "longWait": "Długie oczekiwanie" + }, + "shelfNames": { + "recentlyAdded": "Recently Added", + "continueReading": "Continue Reading", + "continueListening": "Continue Listening", + "wantToRead": "Want to Read", + "upNextInSeries": "Up Next in Series", + "random": "Discover Something New", + "smartScope": "Smart Scope" + }, "reorderHintWidget": "Przeciągnij, aby zmienić kolejność. Przełącz, aby pokazać lub ukryć widżet.", "reorderHintShelf": "Przeciągnij, aby zmienić kolejność. Przełącz, aby pokazać lub ukryć półkę.", "smartScope": "Inteligentny zakres", diff --git a/client/src/locales/pt.json b/client/src/locales/pt.json index 89394bba2..408aa22c2 100644 --- a/client/src/locales/pt.json +++ b/client/src/locales/pt.json @@ -4584,6 +4584,29 @@ "widgets": "Widgets", "shelves": "Prateleiras" }, + "widgetNames": { + "readingStreak": "Sequência de Leitura", + "currentlyReading": "Lendo Atualmente", + "readingGoal": "Reading Goal", + "readingDna": "DNA de Leitura", + "monthlyChallenge": "Desafio Mensal", + "highlightOfTheDay": "Destaque do Dia", + "neglectedGems": "Joias Negligenciadas", + "readingRhythm": "Ritmo de Leitura", + "diversityScore": "Score de Diversidade", + "libraryOverview": "Visão Geral da Biblioteca", + "yearProjection": "Projeção Anual", + "longWait": "O Longo Esperar" + }, + "shelfNames": { + "recentlyAdded": "Recently Added", + "continueReading": "Continue Reading", + "continueListening": "Continue Listening", + "wantToRead": "Want to Read", + "upNextInSeries": "Up Next in Series", + "random": "Discover Something New", + "smartScope": "Smart Scope" + }, "reorderHintWidget": "Arraste para reordenar. Alterne para mostrar ou ocultar um widget.", "reorderHintShelf": "Arraste para reordenar. Alterne para mostrar ou ocultar uma prateleira.", "smartScope": "SmartScope", diff --git a/client/src/locales/sl.json b/client/src/locales/sl.json index 80ffb2f02..b595f081f 100644 --- a/client/src/locales/sl.json +++ b/client/src/locales/sl.json @@ -4584,6 +4584,29 @@ "widgets": "Gradniki", "shelves": "Police" }, + "widgetNames": { + "readingStreak": "Bralni niz", + "currentlyReading": "Trenutno bereš", + "readingGoal": "Reading Goal", + "readingDna": "Bralni DNK", + "monthlyChallenge": "Mesečni izziv", + "highlightOfTheDay": "Poudarek dneva", + "neglectedGems": "Zapostavljeni biseri", + "readingRhythm": "Bralni ritem", + "diversityScore": "Ocena raznolikosti", + "libraryOverview": "Pregled knjižnice", + "yearProjection": "Letna napoved", + "longWait": "Dolgo čakanje" + }, + "shelfNames": { + "recentlyAdded": "Recently Added", + "continueReading": "Continue Reading", + "continueListening": "Continue Listening", + "wantToRead": "Want to Read", + "upNextInSeries": "Up Next in Series", + "random": "Discover Something New", + "smartScope": "Smart Scope" + }, "reorderHintWidget": "Povleci za spremembo vrstnega reda. Preklopi za prikaz ali skritje gradnika.", "reorderHintShelf": "Povleci za spremembo vrstnega reda. Preklopi za prikaz ali skritje police.", "smartScope": "Pametna zbirka", diff --git a/client/src/views/DashboardView.vue b/client/src/views/DashboardView.vue index d3ba8961a..ac24915f2 100644 --- a/client/src/views/DashboardView.vue +++ b/client/src/views/DashboardView.vue @@ -13,6 +13,7 @@ import DashboardSettingsSheet from '@/features/dashboard/components/DashboardSet import DashboardWelcome from '@/features/dashboard/components/DashboardWelcome.vue' import DashboardWidgetRow from '@/features/dashboard/components/DashboardWidgetRow.vue' import { useDashboardConfig } from '@/features/dashboard/composables/useDashboardConfig' +import { useDashboardLabels } from '@/features/dashboard/composables/useDashboardLabels' import { useOnboardingTour } from '@/features/onboarding/composables/useOnboardingTour' import { useSmartScopes } from '@/features/smart-scope/composables/useSmartScopes' @@ -21,6 +22,7 @@ const { hasPermission } = usePermissions() const { user } = useAuth() const { libraries, loading: librariesLoading, fetchLibraries } = useLibraries() const { scrollers, pruneDeletedSmartScopeScrollers } = useDashboardConfig() +const { shelfTitle } = useDashboardLabels() const { maybeStartTour } = useOnboardingTour() const { smartScopes, loaded: smartScopesLoaded, fetchSmartScopes } = useSmartScopes() @@ -108,7 +110,7 @@ onUnmounted(() => { v-for="(scroller, index) in enabledScrollers" :key="`${scroller.id}-${scroller.type}-${scroller.smartScopeId ?? 0}`" :type="scroller.type" - :title="scroller.label" + :title="shelfTitle(scroller)" :limit="scroller.limit" :smartScope-id="scroller.smartScopeId" class="animate-fade-up"