Skip to content

Commit 30f9f88

Browse files
committed
Merge branch 'main' into i18n/rtl-fixes
2 parents 50ff36e + 6acba09 commit 30f9f88

27 files changed

Lines changed: 513 additions & 151 deletions

CONTRIBUTING.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,12 +340,12 @@ To add a new locale:
340340
5. If the language is `right-to-left`, add `dir: 'rtl'` (see `ar-EG` in config for example)
341341
6. If the language requires special pluralization rules, add a `pluralRule` callback (see `ar-EG` or `ru-RU` in config for examples)
342342

343-
Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization) for more info.
343+
Check [Pluralization rule callback](https://vue-i18n.intlify.dev/guide/essentials/pluralization#custom-pluralization) and [Plural Rules](https://cldr.unicode.org/index/cldr-spec/plural-rules#TOC-Determining-Plural-Categories) for more info.
344344

345345
### Update translation
346346

347347
We track the current progress of translations with [Lunaria](https://lunaria.dev/) on this site: https://i18n.npmx.dev/
348-
If you see any outdated translations in your language, feel free to update the keys to match then English version.
348+
If you see any outdated translations in your language, feel free to update the keys to match the English version.
349349

350350
In order to make sure you have everything up-to-date, you can run:
351351

@@ -408,13 +408,51 @@ See how `es`, `es-ES`, and `es-419` are configured in [config/i18n.ts](./config/
408408
<p>{{ $t('greeting', { name: userName }) }}</p>
409409
```
410410

411+
4. Don't concatenate string messages in the Vue templates, some languages can have different word order. Use placeholders instead.
412+
413+
**Bad:**
414+
415+
```vue
416+
<p>{{ $t('hello') }} {{ userName }}</p>
417+
```
418+
419+
**Good:**
420+
421+
```vue
422+
<p>{{ $t('greeting', { name: userName }) }}</p>
423+
```
424+
425+
**Complex content:**
426+
427+
If you need to include HTML or components inside the translation, use [`i18n-t`](https://vue-i18n.intlify.dev/guide/advanced/component.html) component. This is especially useful when the order of elements might change between languages.
428+
429+
```json
430+
{
431+
"agreement": "I accept the {terms} and {privacy}.",
432+
"terms_link": "Terms of Service",
433+
"privacy_policy": "Privacy Policy"
434+
}
435+
```
436+
437+
```vue
438+
<i18n-t keypath="agreement" tag="p">
439+
<template #terms>
440+
<NuxtLink to="/terms">{{ $t('terms_link') }}</NuxtLink>
441+
</template>
442+
<template #privacy>
443+
<strong>{{ $t('privacy_policy') }}</strong>
444+
</template>
445+
</i18n-t>
446+
```
447+
411448
### Translation key conventions
412449

413450
- Use dot notation for hierarchy: `section.subsection.key`
414451
- Keep keys descriptive but concise
415452
- Group related keys together
416453
- Use `common.*` for shared strings (loading, retry, close, etc.)
417454
- Use component-specific prefixes: `package.card.*`, `settings.*`, `nav.*`
455+
- Do not use dashes (`-`) in translation keys; always use underscore (`_`): e.g., `privacy_policy` instead of `privacy-policy`
418456

419457
### Using i18n-ally (recommended)
420458

app/app.vue

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import type { Directions } from '@nuxtjs/i18n'
3-
import { useEventListener } from '@vueuse/core'
3+
import { useEventListener, onKeyDown, onKeyUp } from '@vueuse/core'
44
import { isEditableElement } from '~/utils/input'
55
66
const route = useRoute()
@@ -47,16 +47,12 @@ if (import.meta.server) {
4747
setJsonLd(createWebSiteSchema())
4848
}
4949
50-
// Global keyboard shortcut:
51-
// "/" focuses search or navigates to search page
52-
// "?" highlights all keyboard shortcut elements
53-
function handleGlobalKeydown(e: KeyboardEvent) {
54-
if (isEditableElement(e.target)) return
55-
56-
if (isKeyWithoutModifiers(e, '/')) {
50+
onKeyDown(
51+
'/',
52+
e => {
53+
if (isEditableElement(e.target)) return
5754
e.preventDefault()
5855
59-
// Try to find and focus search input on current page
6056
const searchInput = document.querySelector<HTMLInputElement>(
6157
'input[type="search"], input[name="q"]',
6258
)
@@ -67,18 +63,29 @@ function handleGlobalKeydown(e: KeyboardEvent) {
6763
}
6864
6965
router.push('/search')
70-
}
66+
},
67+
{ dedupe: true },
68+
)
7169
72-
// For "?" we check the key property directly since it's usually combined with shift
73-
if (e.key === '?') {
70+
onKeyDown(
71+
'?',
72+
e => {
73+
if (isEditableElement(e.target)) return
7474
e.preventDefault()
7575
showKbdHints.value = true
76-
}
77-
}
76+
},
77+
{ dedupe: true },
78+
)
7879
79-
function handleGlobalKeyup() {
80-
showKbdHints.value = false
81-
}
80+
onKeyUp(
81+
'?',
82+
e => {
83+
if (isEditableElement(e.target)) return
84+
e.preventDefault()
85+
showKbdHints.value = false
86+
},
87+
{ dedupe: true },
88+
)
8289
8390
// Light dismiss fallback for browsers that don't support closedby="any" (Safari + old Chrome/Firefox)
8491
// https://codepen.io/paramagicdev/pen/gbYompq
@@ -99,9 +106,6 @@ function handleModalLightDismiss(e: MouseEvent) {
99106
}
100107
101108
if (import.meta.client) {
102-
useEventListener(document, 'keydown', handleGlobalKeydown)
103-
useEventListener(document, 'keyup', handleGlobalKeyup)
104-
105109
// Feature check for native light dismiss support via closedby="any"
106110
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby
107111
const supportsClosedBy =

app/assets/main.css

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ html {
155155
-moz-osx-font-smoothing: grayscale;
156156
text-rendering: optimizeLegibility;
157157
scroll-padding-top: 5rem; /* Offset for fixed header - otherwise anchor headers are cutted */
158+
scrollbar-gutter: stable;
158159
}
159160

160161
/*
@@ -229,6 +230,10 @@ select:focus-visible {
229230
}
230231

231232
/* Scrollbar styling */
233+
* {
234+
scrollbar-color: var(--border) var(--bg);
235+
}
236+
232237
::-webkit-scrollbar {
233238
width: 8px;
234239
height: 8px;
@@ -244,14 +249,13 @@ select:focus-visible {
244249
}
245250

246251
::-webkit-scrollbar-thumb:hover {
247-
background: #404040;
252+
background: var(--border-hover);
248253
}
249254

250255
/* Scrollbar styling for Firefox */
251256
@supports not selector(::-webkit-scrollbar) {
252257
* {
253258
scrollbar-width: thin;
254-
scrollbar-color: var(--border) var(--bg);
255259
}
256260
}
257261

app/components/ColumnPicker.vue

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { ColumnConfig, ColumnId } from '#shared/types/preferences'
3+
import { onKeyDown } from '@vueuse/core'
34
45
const props = defineProps<{
56
columns: ColumnConfig[]
@@ -26,13 +27,16 @@ onClickOutside(
2627
},
2728
)
2829
29-
// Close on Escape key
30-
useEventListener('keydown', event => {
31-
if (event.key === 'Escape' && isOpen.value) {
30+
onKeyDown(
31+
'Escape',
32+
e => {
33+
if (!isOpen.value) return
3234
isOpen.value = false
3335
buttonRef.value?.focus()
34-
}
35-
})
36+
},
37+
{ dedupe: true },
38+
)
39+
3640
// Columns that can be toggled (name is always visible)
3741
const toggleableColumns = computed(() => props.columns.filter(col => col.id !== 'name'))
3842

app/components/DependencyPathPopup.vue

Lines changed: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
<script setup lang="ts">
2+
import { onKeyDown } from '@vueuse/core'
3+
import { UseFocusTrap } from '@vueuse/integrations/useFocusTrap/component'
4+
25
defineProps<{
36
/** Dependency path from root to vulnerable package (readonly from VulnerabilityTreeResult) */
47
path: readonly string[]
@@ -17,13 +20,16 @@ onClickOutside(popupEl, () => {
1720
if (isOpen.value) closePopup()
1821
})
1922
20-
// Close popup on ESC or scroll
21-
function handleKeydown(e: KeyboardEvent) {
22-
if (e.key === 'Escape') closePopup()
23-
}
23+
onKeyDown(
24+
'Escape',
25+
e => {
26+
e.preventDefault()
27+
closePopup()
28+
},
29+
{ dedupe: true, target: popupEl },
30+
)
2431
25-
useEventListener(document, 'keydown', handleKeydown)
26-
useEventListener('scroll', closePopup, true)
32+
useEventListener('scroll', closePopup, { passive: true })
2733
2834
function togglePopup(event: MouseEvent) {
2935
if (isOpen.value) {
@@ -77,34 +83,36 @@ function parsePackageString(pkg: string): { name: string; version: string } {
7783
class="fixed z-[100] bg-bg-elevated border border-border rounded-lg shadow-xl p-3 min-w-64 max-w-sm"
7884
:style="getPopupStyle()"
7985
>
80-
<ul class="list-none m-0 p-0 space-y-0.5">
81-
<li
82-
v-for="(pathItem, idx) in path"
83-
:key="idx"
84-
class="font-mono text-xs"
85-
:style="{ paddingLeft: `${idx * 12}px` }"
86-
>
87-
<span v-if="idx > 0" class="text-fg-subtle me-1">└─</span>
88-
<NuxtLink
89-
:to="{
90-
name: 'package',
91-
params: {
92-
package: [
93-
...parsePackageString(pathItem).name.split('/'),
94-
'v',
95-
parsePackageString(pathItem).version,
96-
],
97-
},
98-
}"
99-
class="hover:underline"
100-
:class="idx === path.length - 1 ? 'text-fg font-medium' : 'text-fg-muted'"
101-
@click="closePopup"
86+
<UseFocusTrap :options="{ immediate: true }">
87+
<ul class="list-none m-0 p-0 space-y-0.5">
88+
<li
89+
v-for="(pathItem, idx) in path"
90+
:key="idx"
91+
class="font-mono text-xs"
92+
:style="{ paddingLeft: `${idx * 12}px` }"
10293
>
103-
{{ pathItem }}
104-
</NuxtLink>
105-
<span v-if="idx === path.length - 1" class="ms-1 text-amber-500">⚠</span>
106-
</li>
107-
</ul>
94+
<span v-if="idx > 0" class="text-fg-subtle me-1">└─</span>
95+
<NuxtLink
96+
:to="{
97+
name: 'package',
98+
params: {
99+
package: [
100+
...parsePackageString(pathItem).name.split('/'),
101+
'v',
102+
parsePackageString(pathItem).version,
103+
],
104+
},
105+
}"
106+
class="hover:underline"
107+
:class="idx === path.length - 1 ? 'text-fg font-medium' : 'text-fg-muted'"
108+
@click="closePopup"
109+
>
110+
{{ pathItem }}
111+
</NuxtLink>
112+
<span v-if="idx === path.length - 1" class="ms-1 text-amber-500">⚠</span>
113+
</li>
114+
</ul>
115+
</UseFocusTrap>
108116
</div>
109117
</div>
110118
</template>

app/components/Readme.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ function handleClick(event: MouseEvent) {
6161
<template>
6262
<article
6363
class="readme prose prose-invert max-w-[70ch] lg:max-w-none px-1"
64+
dir="auto"
6465
v-html="html"
6566
dir="auto"
6667
:style="{
@@ -86,6 +87,8 @@ function handleClick(event: MouseEvent) {
8687
/* Contain all children */
8788
overflow: hidden;
8889
min-width: 0;
90+
/* Contain all children z-index values inside this container */
91+
isolation: isolate;
8992
}
9093
9194
/* README headings - styled by visual level (data-level), not semantic level */
@@ -403,6 +406,8 @@ function handleClick(event: MouseEvent) {
403406
display: revert-layer;
404407
border-radius: 8px;
405408
margin: 1rem 0;
409+
position: relative;
410+
z-index: 1;
406411
}
407412
408413
.readme :deep(video) {

app/components/ReadmeTocDropdown.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function handleScroll(event: Event) {
6969
}
7070
close()
7171
}
72-
useEventListener('scroll', handleScroll, true)
72+
useEventListener('scroll', handleScroll, { passive: true })
7373
7474
// Generate unique ID for accessibility
7575
const inputId = useId()

app/components/Settings/AccentColorPicker.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ onPrehydrate(el => {
1616
</script>
1717

1818
<template>
19-
<fieldset class="flex items-center gap-4">
19+
<fieldset
20+
class="flex items-center gap-4 has-[input:focus-visible]:(outline-solid outline-accent/70 outline-offset-4) rounded-xl w-fit"
21+
>
2022
<legend class="sr-only">{{ $t('settings.accent_colors') }}</legend>
2123
<label
2224
v-for="color in accentColors"
@@ -35,7 +37,7 @@ onPrehydrate(el => {
3537
/>
3638
</label>
3739
<label
38-
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 cursor-pointer has-[:checked]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle) has-[:focus-visible]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle) flex items-center justify-center bg-accent-fallback"
40+
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 cursor-pointer has-[:checked]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle) has-[:focus-visible]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle) flex items-center justify-center bg-fg"
3941
>
4042
<input
4143
type="radio"

app/components/Settings/BgThemePicker.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ onPrehydrate(el => {
1414
</script>
1515

1616
<template>
17-
<fieldset class="flex items-center gap-4">
17+
<fieldset
18+
class="flex items-center gap-4 has-[input:focus-visible]:(outline-solid outline-accent/70 outline-offset-4) rounded-xl w-fit"
19+
>
1820
<legend class="sr-only">{{ $t('settings.background_themes') }}</legend>
1921
<label
2022
v-for="theme in backgroundThemes"

0 commit comments

Comments
 (0)