diff --git a/client/src/composables/__tests__/useModifierKey.spec.ts b/client/src/composables/__tests__/useModifierKey.spec.ts new file mode 100644 index 000000000..5d98510c7 --- /dev/null +++ b/client/src/composables/__tests__/useModifierKey.spec.ts @@ -0,0 +1,28 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { useModifierKey } from '../useModifierKey' + +function withUserAgent(userAgent: string) { + vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(userAgent) + return useModifierKey() +} + +describe('useModifierKey', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('uses the command glyph on Apple platforms', () => { + const { isMac, modifierKey } = withUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36') + expect(isMac.value).toBe(true) + expect(modifierKey.value).toBe('⌘') + }) + + it.each([ + ['Windows', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'], + ['Linux', 'Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0'], + ])('uses Ctrl on %s, which has no command key', (_platform, userAgent) => { + const { isMac, modifierKey } = withUserAgent(userAgent) + expect(isMac.value).toBe(false) + expect(modifierKey.value).toBe('Ctrl') + }) +}) diff --git a/client/src/composables/useModifierKey.ts b/client/src/composables/useModifierKey.ts new file mode 100644 index 000000000..06901a012 --- /dev/null +++ b/client/src/composables/useModifierKey.ts @@ -0,0 +1,11 @@ +import { computed, type ComputedRef } from 'vue' + +/** + * Apple keyboards label the shortcut modifier differently from every other platform, so a hint + * that hardcodes one of them is wrong for half the users. Keep the detection here so the label + * cannot drift between the places that display it. + */ +export function useModifierKey(): { isMac: ComputedRef; modifierKey: ComputedRef } { + const isMac = computed(() => typeof navigator !== 'undefined' && /Mac/i.test(navigator.userAgent)) + return { isMac, modifierKey: computed(() => (isMac.value ? '⌘' : 'Ctrl')) } +} diff --git a/client/src/features/book/components/KeyboardShortcutOverlay.vue b/client/src/features/book/components/KeyboardShortcutOverlay.vue index b9679a268..8ef9e1de1 100644 --- a/client/src/features/book/components/KeyboardShortcutOverlay.vue +++ b/client/src/features/book/components/KeyboardShortcutOverlay.vue @@ -2,14 +2,14 @@ import { computed } from 'vue' import { X } from '@lucide/vue' import { useI18n } from 'vue-i18n' +import { useModifierKey } from '@/composables/useModifierKey' defineProps<{ open: boolean }>() const emit = defineEmits<{ 'update:open': [value: boolean] }>() const { t } = useI18n() -const isMac = computed(() => typeof navigator !== 'undefined' && /Mac/i.test(navigator.userAgent)) -const modKey = computed(() => (isMac.value ? '⌘' : 'Ctrl')) +const { modifierKey: modKey } = useModifierKey() function handleClose() { emit('update:open', false) diff --git a/client/src/features/settings/__tests__/SettingsNav.spec.ts b/client/src/features/settings/__tests__/SettingsNav.spec.ts index f408ac70e..d046290eb 100644 --- a/client/src/features/settings/__tests__/SettingsNav.spec.ts +++ b/client/src/features/settings/__tests__/SettingsNav.spec.ts @@ -66,10 +66,22 @@ async function search(wrapper: Wrapper, term: string): Promise { describe('SettingsNav', () => { beforeEach(() => { - vi.clearAllMocks() + vi.restoreAllMocks() push.mockClear() }) + describe('shortcut hint', () => { + it('names the command key on Apple platforms', () => { + vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36') + expect(mountNav().text()).toContain('Press \u2318K to jump to any setting') + }) + + it('names Ctrl everywhere else, since those keyboards have no command key', () => { + vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0') + expect(mountNav().text()).toContain('Press Ctrl+K to jump to any setting') + }) + }) + it('places an icon-only back action beside search', () => { const wrapper = mountNav() const back = wrapper.get('[data-testid="settings-nav-back"]') diff --git a/client/src/features/settings/components/SettingsNav.vue b/client/src/features/settings/components/SettingsNav.vue index 6054af81c..c1b02316a 100644 --- a/client/src/features/settings/components/SettingsNav.vue +++ b/client/src/features/settings/components/SettingsNav.vue @@ -4,6 +4,7 @@ import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue' import { useI18n } from 'vue-i18n' import { useRoute, useRouter } from 'vue-router' import { ArrowLeft, ChevronRight, Search, X } from '@lucide/vue' +import { useModifierKey } from '@/composables/useModifierKey' import { usePermissions } from '@/features/auth/composables/usePermissions' import { useSettingsNavStatus } from '../composables/useSettingsNavStatus' import { visibleSettingsNav, type SettingsNavGroup, type SettingsNavItem } from '../lib/settings-nav' @@ -17,6 +18,10 @@ const { t } = useI18n() const route = useRoute() const router = useRouter() const { isSuperuser, userPermissions, isDemoRestrictedAccount } = usePermissions() +const { isMac, modifierKey } = useModifierKey() + +/** Apple writes the combo as one glyph pair, everyone else separates the two keys. */ +const shortcutLabel = computed(() => (isMac.value ? `${modifierKey.value}K` : `${modifierKey.value}+K`)) const query = ref('') const searchInput = ref(null) @@ -296,7 +301,7 @@ defineExpose({ focusSearch }) diff --git a/client/src/features/tools/bulk-rename/views/BulkRenameView.vue b/client/src/features/tools/bulk-rename/views/BulkRenameView.vue index d7876d3ea..f94a91772 100644 --- a/client/src/features/tools/bulk-rename/views/BulkRenameView.vue +++ b/client/src/features/tools/bulk-rename/views/BulkRenameView.vue @@ -246,7 +246,7 @@ function handleRetryPreview(): void { {{ t('tools.bulkRename.executionFailed') }} - {{ executionErrorMessage }} -
+
diff --git a/client/src/locales/de.json b/client/src/locales/de.json index e1dd5b771..1e1ffb39a 100644 --- a/client/src/locales/de.json +++ b/client/src/locales/de.json @@ -3089,7 +3089,6 @@ "searchPlaceholder": "Sucheinstellungen", "clearSearch": "Suche zurücksetzen", "noResults": "Keine Einstellungen entsprechen {query}.", - "shortcutHint": "Drücke Cmd K um zu einer beliebigen Einstellung zu springen", "groups": { "you": "Du", "library": "Bibliothek", diff --git a/client/src/locales/el.json b/client/src/locales/el.json index fe65cc8d1..5d6b782bf 100644 --- a/client/src/locales/el.json +++ b/client/src/locales/el.json @@ -2291,7 +2291,6 @@ "searchPlaceholder": "Ρυθμίσεις αναζήτησης", "clearSearch": "Εκκαθάριση αναζήτησης", "noResults": "Καμία ρύθμιση δεν ταιριάζει με το {query}.", - "shortcutHint": "Πατήστε Cmd K για να μεταβείτε σε οποιαδήποτε ρύθμιση", "groups": { "you": "Εσείς", "library": "Βιβλιοθήκη", diff --git a/client/src/locales/en.json b/client/src/locales/en.json index cbad2a938..f7ccab723 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -3368,7 +3368,7 @@ "searchPlaceholder": "Search settings", "clearSearch": "Clear search", "noResults": "No settings match {query}.", - "shortcutHint": "Press Cmd K to jump to any setting", + "shortcutHint": "Press {shortcut} to jump to any setting", "status": { "scanning": "Scanning" }, diff --git a/client/src/locales/es.json b/client/src/locales/es.json index 797a62de3..dc5c381e5 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -3365,7 +3365,6 @@ "searchPlaceholder": "Ajustes de búsqueda", "clearSearch": "Borrar búsqueda", "noResults": "No hay ajustes que coincidan con {query}.", - "shortcutHint": "Presiona Cmd K para saltar a cualquier ajuste", "status": { "scanning": "Escaneando" }, diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json index a8a603855..7924f9245 100644 --- a/client/src/locales/fr.json +++ b/client/src/locales/fr.json @@ -3306,7 +3306,6 @@ "searchPlaceholder": "Rechercher des paramètres", "clearSearch": "Effacer la recherche", "noResults": "Aucun paramètre ne correspond à « {query} ».", - "shortcutHint": "Appuyez sur Cmd K pour accéder directement à un paramètre", "status": { "scanning": "Analyse en cours" }, diff --git a/client/src/locales/hu.json b/client/src/locales/hu.json index d03a58406..ba20c0179 100644 --- a/client/src/locales/hu.json +++ b/client/src/locales/hu.json @@ -3238,7 +3238,6 @@ "searchPlaceholder": "Keresési beállítások", "clearSearch": "Keresés törlése", "noResults": "Nincsenek a következőnek megfelelő beállítások: {query}.", - "shortcutHint": "Nyomd meg a Cmd K billentyűkombinációt bármelyik beállításhoz ugráshoz", "status": { "scanning": "Beolvasás" }, diff --git a/client/src/locales/ja.json b/client/src/locales/ja.json index b773b3585..0b319a359 100644 --- a/client/src/locales/ja.json +++ b/client/src/locales/ja.json @@ -2262,7 +2262,6 @@ "searchPlaceholder": "検索設定", "clearSearch": "検索をクリア", "noResults": "{query} に一致する設定がありません。", - "shortcutHint": "Cmd K を押して任意の設定にジャンプします", "groups": { "you": "あなたです", "library": "ライブラリ", diff --git a/client/src/locales/ko.json b/client/src/locales/ko.json index c35179a3c..3008bc004 100644 --- a/client/src/locales/ko.json +++ b/client/src/locales/ko.json @@ -2946,7 +2946,6 @@ "searchPlaceholder": "설정 검색", "clearSearch": "검색 초기화", "noResults": "{query}와 일치하는 설정 항목 없음", - "shortcutHint": "설정으로 이동하려면 Cmd K 단축키를 눌러주세요", "groups": { "you": "개인", "library": "라이브러리", diff --git a/client/src/locales/pt.json b/client/src/locales/pt.json index a75d52798..7390f3f20 100644 --- a/client/src/locales/pt.json +++ b/client/src/locales/pt.json @@ -3241,7 +3241,6 @@ "searchPlaceholder": "Procurar nas configurações", "clearSearch": "Limpar busca", "noResults": "Nenhuma configuração corresponde a {query}.", - "shortcutHint": "Pressione Cmd K para pular para qualquer configuração", "status": { "scanning": "Analisando" }, diff --git a/client/src/locales/ro.json b/client/src/locales/ro.json index f2c1d7eef..6991f7ddd 100644 --- a/client/src/locales/ro.json +++ b/client/src/locales/ro.json @@ -2359,7 +2359,6 @@ "searchPlaceholder": "Setări căutare", "clearSearch": "Ștergeți căutarea", "noResults": "Nicio setare nu corespunde cu {query}.", - "shortcutHint": "Apăsați Cmd K pentru a sări la orice setare", "groups": { "you": "Tu", "library": "Bibliotecă", diff --git a/client/src/locales/sl.json b/client/src/locales/sl.json index 671b70429..94ddded27 100644 --- a/client/src/locales/sl.json +++ b/client/src/locales/sl.json @@ -2509,7 +2509,6 @@ "searchPlaceholder": "Nastavitve iskanja", "clearSearch": "Počisti iskanje", "noResults": "Nobena nastavitev ne ustreza {query}.", - "shortcutHint": "Pritisnite Cmd K za skok na katero koli nastavitev", "groups": { "you": "Vi", "library": "Knjižnica", diff --git a/client/src/locales/zh.json b/client/src/locales/zh.json index d80cfddf8..70c4d9223 100644 --- a/client/src/locales/zh.json +++ b/client/src/locales/zh.json @@ -3015,7 +3015,6 @@ "searchPlaceholder": "搜索设置", "clearSearch": "清空搜索内容", "noResults": "没有设置匹配{query}", - "shortcutHint": "按 Cmd K 跳转到任何设置", "status": { "scanning": "扫描中" },