Skip to content

Commit e99d77d

Browse files
authored
Merge branch 'main' into fix/issue-1323
2 parents 1a6465d + d2b21b1 commit e99d77d

34 files changed

+992
-274
lines changed

app/components/AppFooter.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ const closeModal = () => modalRef.value?.close?.()
3535
<LinkBase :to="{ name: 'accessibility' }">
3636
{{ $t('a11y.footer_title') }}
3737
</LinkBase>
38+
<LinkBase :to="{ name: 'translation-status' }">
39+
{{ $t('translation_status.title') }}
40+
</LinkBase>
3841
<button
3942
type="button"
4043
class="cursor-pointer group inline-flex gap-x-1 items-center justify-center underline-offset-[0.2rem] underline decoration-1 decoration-fg/30 font-mono text-fg hover:(decoration-accent text-accent) focus-visible:(decoration-accent text-accent) transition-colors duration-200"

app/components/AppHeader.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ const mobileLinks = computed<NavigationConfigWithGroups>(() => [
8484
external: false,
8585
iconClass: 'i-custom:a11y',
8686
},
87+
{
88+
name: 'Translation Status',
89+
label: $t('translation_status.title'),
90+
to: { name: 'translation-status' },
91+
type: 'link',
92+
external: false,
93+
iconClass: 'i-lucide:languages',
94+
},
8795
],
8896
},
8997
{

app/components/BlueskyComment.vue

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,27 @@ function getHostname(uri: string): string {
3838
return uri
3939
}
4040
}
41+
42+
let segmenter: Intl.Segmenter | null = null
43+
function firstChar(str: string): string {
44+
segmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' })
45+
return Array.from(segmenter.segment(str))[0]?.segment ?? ''
46+
}
4147
</script>
4248

4349
<template>
44-
<div :class="depth === 0 ? 'flex gap-3' : 'flex gap-3 mt-3'">
45-
<!-- Avatar -->
50+
<!--
51+
Depth 0: classic avatar-column layout (all screens)
52+
Depth 1+: Medium-style inline avatar on mobile, avatar-column on desktop
53+
-->
54+
<div :class="depth === 0 ? 'flex gap-3' : 'sm:flex sm:gap-3'">
55+
<!-- Column avatar: always shown at depth 0, desktop-only at depth 1+ -->
4656
<a
4757
:href="`https://bsky.app/profile/${comment.author.handle}`"
4858
target="_blank"
4959
rel="noopener noreferrer"
5060
class="shrink-0"
61+
:class="depth > 0 ? 'hidden sm:block' : ''"
5162
>
5263
<img
5364
v-if="comment.author.avatar"
@@ -65,22 +76,45 @@ function getHostname(uri: string): string {
6576
depth === 0 ? 'w-10 h-10' : 'w-8 h-8 text-sm',
6677
]"
6778
>
68-
{{ (comment.author.displayName || comment.author.handle).charAt(0).toUpperCase() }}
79+
{{ firstChar(comment.author.displayName || comment.author.handle).toUpperCase() }}
6980
</div>
7081
</a>
7182

7283
<div class="flex-1 min-w-0">
7384
<!-- Author info + timestamp -->
74-
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-0">
85+
<div class="flex flex-wrap items-center gap-x-2 gap-y-0">
86+
<!-- Inline avatar: mobile-only for nested comments -->
87+
<a
88+
v-if="depth > 0"
89+
:href="`https://bsky.app/profile/${comment.author.handle}`"
90+
target="_blank"
91+
rel="noopener noreferrer"
92+
class="shrink-0 sm:hidden"
93+
>
94+
<img
95+
v-if="comment.author.avatar"
96+
:src="comment.author.avatar"
97+
:alt="comment.author.displayName || comment.author.handle"
98+
class="w-6 h-6 rounded-full"
99+
width="24"
100+
height="24"
101+
loading="lazy"
102+
/>
103+
<div
104+
v-else
105+
class="w-6 h-6 rounded-full bg-border flex items-center justify-center text-fg-muted text-xs"
106+
>
107+
{{ firstChar(comment.author.displayName || comment.author.handle).toUpperCase() }}
108+
</div>
109+
</a>
75110
<a
76111
:href="`https://bsky.app/profile/${comment.author.handle}`"
77112
target="_blank"
78113
rel="noopener noreferrer"
79-
class="font-medium text-fg hover:underline"
114+
:class="['font-medium text-fg hover:underline', depth > 0 ? 'text-sm' : '']"
80115
>
81116
{{ comment.author.displayName || comment.author.handle }}
82117
</a>
83-
<span class="text-fg-subtle text-sm">@{{ comment.author.handle }}</span>
84118
<span class="text-fg-subtle text-sm">·</span>
85119
<a
86120
:href="getCommentUrl(props.comment)"
@@ -93,7 +127,7 @@ function getHostname(uri: string): string {
93127
</div>
94128

95129
<!-- Comment text with rich segments -->
96-
<p class="text-fg-muted whitespace-pre-wrap">
130+
<p class="text-fg-muted whitespace-pre-wrap mt-2 mb-3">
97131
<template v-for="(segment, i) in processedSegments" :key="i">
98132
<a
99133
v-if="segment.url"
@@ -162,7 +196,7 @@ function getHostname(uri: string): string {
162196
<!-- Like/repost counts -->
163197
<div
164198
v-if="comment.likeCount > 0 || comment.repostCount > 0"
165-
class="mt-2 flex gap-4 text-sm text-fg-subtle"
199+
class="mt-1 flex gap-4 text-sm text-fg-subtle"
166200
>
167201
<span v-if="comment.likeCount > 0">
168202
{{ $t('blog.atproto.like_count', { count: comment.likeCount }, comment.likeCount) }}
@@ -174,7 +208,10 @@ function getHostname(uri: string): string {
174208

175209
<!-- Nested replies -->
176210
<template v-if="comment.replies.length > 0">
177-
<div v-if="depth < MaxDepth" class="mt-2 ps-2 border-is-2 border-border flex flex-col">
211+
<div
212+
v-if="depth < MaxDepth"
213+
class="mt-3 ps-3 border-is-2 border-border flex flex-col gap-3"
214+
>
178215
<BlueskyComment
179216
v-for="reply in comment.replies"
180217
:key="reply.uri"

app/components/ProgressBar.vue

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<script setup lang="ts">
2+
type CompletionColorScheme = {
3+
low: number
4+
medium: number
5+
high: number
6+
full?: boolean
7+
}
8+
9+
const props = withDefaults(
10+
defineProps<{
11+
val: number
12+
label: string
13+
scheme?: CompletionColorScheme
14+
}>(),
15+
{
16+
scheme: () => ({
17+
low: 50,
18+
medium: 75,
19+
high: 90,
20+
full: true,
21+
}),
22+
},
23+
)
24+
25+
const completionClass = computed<string>(() => {
26+
if (props.scheme.full && props.val === 100) {
27+
return 'full'
28+
} else if (props.val > props.scheme.high) {
29+
return 'high'
30+
} else if (props.val > props.scheme.medium) {
31+
return 'medium'
32+
} else if (props.val > props.scheme.low) {
33+
return 'low'
34+
}
35+
36+
return ''
37+
})
38+
</script>
39+
40+
<template>
41+
<progress
42+
class="flex-1 h-3 rounded-full overflow-hidden"
43+
max="100"
44+
:value="val"
45+
:class="completionClass"
46+
:aria-label="label"
47+
></progress>
48+
</template>
49+
50+
<style scoped>
51+
/* Reset & Base */
52+
progress {
53+
-webkit-appearance: none;
54+
appearance: none;
55+
border: none;
56+
@apply bg-bg-muted; /* Background for container */
57+
}
58+
59+
/* Webkit Container */
60+
progress::-webkit-progress-bar {
61+
@apply bg-bg-muted;
62+
}
63+
64+
/* Value Bar */
65+
/* Default <= 50 */
66+
progress::-webkit-progress-value {
67+
@apply bg-red-800 dark:bg-red-900;
68+
}
69+
progress::-moz-progress-bar {
70+
@apply bg-red-800 dark:bg-red-900;
71+
}
72+
73+
/* Low > scheme.low (default: 50) */
74+
progress.low::-webkit-progress-value {
75+
@apply bg-red-500 dark:bg-red-700;
76+
}
77+
progress.low::-moz-progress-bar {
78+
@apply bg-red-500 dark:bg-red-700;
79+
}
80+
81+
/* Medium scheme.medium (default: 75) */
82+
progress.medium::-webkit-progress-value {
83+
@apply bg-orange-500;
84+
}
85+
progress.medium::-moz-progress-bar {
86+
@apply bg-orange-500;
87+
}
88+
89+
/* Good > scheme.high (default: 90) */
90+
progress.high::-webkit-progress-value {
91+
@apply bg-green-500 dark:bg-green-700;
92+
}
93+
progress.high::-moz-progress-bar {
94+
@apply bg-green-500 dark:bg-green-700;
95+
}
96+
97+
/* Completed = 100 */
98+
progress.full::-webkit-progress-value {
99+
@apply bg-green-700 dark:bg-green-500;
100+
}
101+
progress.full::-moz-progress-bar {
102+
@apply bg-green-700 dark:bg-green-500;
103+
}
104+
105+
details[dir='rtl']:not([open]) .icon-rtl {
106+
transform: scale(-1, 1);
107+
}
108+
</style>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<script setup lang="ts">
2+
/** This component is not used at the moment, but we keep it not to lose code
3+
* produced to output report for translations per file. As we might need if
4+
* we split single translation files into multiple as it grows significantly
5+
*/
6+
const { locale } = useI18n()
7+
const { fetchStatus, localesMap } = useI18nStatus()
8+
9+
const localeEntries = computed(() => {
10+
const l = localesMap.value?.values()
11+
if (!l) return []
12+
return [...mapFiles(l)]
13+
})
14+
15+
function* mapFiles(
16+
map: MapIterator<I18nLocaleStatus>,
17+
): Generator<FileEntryStatus, undefined, void> {
18+
for (const entry of map) {
19+
yield {
20+
...entry,
21+
lang: entry.lang,
22+
done: entry.completedKeys,
23+
missing: entry.missingKeys.length,
24+
file: entry.githubEditUrl.split('/').pop() ?? entry.lang,
25+
}
26+
}
27+
}
28+
</script>
29+
30+
<template>
31+
<section class="prose prose-invert max-w-none space-y-8 pt-8">
32+
<h2 id="by-file" tabindex="-1" class="text-xs text-fg-muted uppercase tracking-wider mb-4">
33+
{{ $t('translation_status.by_file') }}
34+
</h2>
35+
<table class="w-full text-start border-collapse">
36+
<thead class="border-b border-border text-start">
37+
<tr>
38+
<th scope="col" class="py-2 px-2 font-medium text-fg-subtle text-sm">
39+
{{ $t('translation_status.table.file') }}
40+
</th>
41+
<th scope="col" class="py-2 px-2 font-medium text-fg-subtle text-sm">
42+
{{ $t('translation_status.table.status') }}
43+
</th>
44+
</tr>
45+
</thead>
46+
<tbody class="divide-y divide-border/50">
47+
<template v-if="fetchStatus === 'error'">
48+
<tr>
49+
<td colspan="2" class="py-4 px-2 text-center text-red-500">
50+
{{ $t('translation_status.table.error') }}
51+
</td>
52+
</tr>
53+
</template>
54+
<template v-else-if="fetchStatus === 'pending' || fetchStatus === 'idle'">
55+
<tr>
56+
<td colspan="2" class="py-4 px-2 text-center text-fg-muted">
57+
<SkeletonBlock class="h-10 w-full mb-4" />
58+
<SkeletonBlock class="h-10 w-full mb-4" />
59+
<SkeletonBlock class="h-10 w-full mb-4" />
60+
</td>
61+
</tr>
62+
</template>
63+
<template v-else-if="!localeEntries || localeEntries.length === 0">
64+
<tr>
65+
<td colspan="2" class="py-4 px-2 text-center text-fg-muted">
66+
{{ $t('translation_status.table.empty') }}
67+
</td>
68+
</tr>
69+
</template>
70+
<template v-else>
71+
<tr>
72+
<td class="py-3 px-2 font-mono text-sm">
73+
<LinkBase to="https://github.com/npmx-dev/npmx.dev/blob/main/i18n/locales/en.json">
74+
<i18n-t
75+
keypath="translation_status.table.file_link"
76+
scope="global"
77+
tag="span"
78+
:class="locale === 'en-US' ? 'font-bold' : undefined"
79+
>
80+
<template #file>en.json</template>
81+
<template #lang>en-US</template>
82+
</i18n-t>
83+
</LinkBase>
84+
</td>
85+
<td class="py-3 px-2">
86+
<div class="flex items-center gap-2">
87+
<progress
88+
class="done w-24 h-1.5 rounded-full overflow-hidden"
89+
max="100"
90+
value="100"
91+
></progress>
92+
<span class="text-xs font-mono text-fg-muted">
93+
{{ $n(1, 'percentage') }}
94+
</span>
95+
</div>
96+
</td>
97+
</tr>
98+
<tr v-for="file in localeEntries" :key="file.lang">
99+
<td class="py-3 px-2 font-mono text-sm">
100+
<LinkBase :to="file.githubEditUrl">
101+
<i18n-t
102+
keypath="translation_status.table.file_link"
103+
scope="global"
104+
tag="span"
105+
:class="locale === file.lang ? 'font-bold' : undefined"
106+
>
107+
<template #file>
108+
{{ file.file }}
109+
</template>
110+
<template #lang>
111+
{{ file.lang }}
112+
</template>
113+
</i18n-t>
114+
</LinkBase>
115+
</td>
116+
<td class="py-3 px-2">
117+
<div class="flex items-center gap-2">
118+
<ProgressBar
119+
:val="file.percentComplete"
120+
:label="$t('translation_status.progress_label', { locale: file.label })"
121+
/>
122+
<span class="text-xs font-mono text-fg-muted">{{
123+
$n(file.percentComplete / 100, 'percentage')
124+
}}</span>
125+
</div>
126+
</td>
127+
</tr>
128+
</template>
129+
</tbody>
130+
</table>
131+
</section>
132+
</template>

app/components/global/BlogPostFederatedArticles.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const federatedArticles = computed(() => {
4141
</script>
4242

4343
<template>
44-
<aside class="px-4 sm:-mx-6 sm:px-6 sm:-my-3 sm:py-3 sm:rounded-md">
44+
<aside class="sm:-mx-6 sm:px-6 sm:-my-3 sm:py-3 sm:rounded-md">
4545
<h2 class="font-mono text-xl font-medium text-fg mt-0">
4646
{{ headline }}
4747
</h2>
@@ -55,7 +55,7 @@ const federatedArticles = computed(() => {
5555
rel="noopener noreferrer"
5656
v-for="article in federatedArticles"
5757
:key="article.url"
58-
class="grid grid-cols-[auto_1fr] gap-x-5 no-underline hover:no-underline rounded-lg border border-border p-4 transition-shadow hover:shadow-lg hover:shadow-gray-500/50"
58+
class="grid grid-cols-[auto_1fr] gap-x-5 no-underline hover:no-underline rounded-lg border border-border p-4 transition-all hover:shadow-md hover:shadow-fg/5 hover:border-border-hover"
5959
>
6060
<AuthorAvatar v-if="article.author" :author="article.author" size="md" class="row-span-2" />
6161
<div class="flex flex-col">

0 commit comments

Comments
 (0)