Skip to content

Commit d2b21b1

Browse files
authored
feat: improve i18n (lunaria) status page (#2064)
1 parent 882dcac commit d2b21b1

File tree

20 files changed

+910
-246
lines changed

20 files changed

+910
-246
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/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/composables/useI18nStatus.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Provides information about translation progress for each locale.
44
*/
55
export function useI18nStatus() {
6-
const { locale } = useI18n()
6+
const { locale: currentLocale } = useI18n()
77

88
const {
99
data: status,
@@ -16,20 +16,26 @@ export function useI18nStatus() {
1616
getCachedData: (key, nuxtApp) => nuxtApp.payload.data[key] ?? nuxtApp.static.data[key],
1717
})
1818

19+
const localesMap = computed<Map<string, I18nLocaleStatus> | undefined>(() => {
20+
return status.value?.locales.reduce((acc, locale) => {
21+
acc.set(locale.lang, locale)
22+
return acc
23+
}, new Map())
24+
})
25+
1926
/**
2027
* Get the translation status for a specific locale
2128
*/
2229
function getLocaleStatus(langCode: string): I18nLocaleStatus | null {
23-
if (!status.value) return null
24-
return status.value.locales.find(l => l.lang === langCode) ?? null
30+
return localesMap.value?.get(langCode) ?? null
2531
}
2632

2733
/**
2834
* Translation status for the current locale
2935
*/
30-
const currentLocaleStatus = computed<I18nLocaleStatus | null>(() => {
31-
return getLocaleStatus(locale.value)
32-
})
36+
const currentLocaleStatus = computed<I18nLocaleStatus | null>(() =>
37+
getLocaleStatus(currentLocale.value),
38+
)
3339

3440
/**
3541
* Whether the current locale's translation is 100% complete
@@ -47,7 +53,7 @@ export function useI18nStatus() {
4753
*/
4854
const isSourceLocale = computed(() => {
4955
const sourceLang = status.value?.sourceLocale.lang ?? 'en'
50-
return locale.value === sourceLang || locale.value.startsWith(`${sourceLang}-`)
56+
return currentLocale.value === sourceLang || currentLocale.value.startsWith(`${sourceLang}-`)
5157
})
5258

5359
/**
@@ -74,5 +80,7 @@ export function useI18nStatus() {
7480
isSourceLocale,
7581
/** GitHub edit URL for current locale */
7682
githubEditUrl,
83+
/** locale info map by lang */
84+
localesMap,
7785
}
7886
}

app/pages/settings.vue

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
const router = useRouter()
33
const canGoBack = useCanGoBack()
44
const { settings } = useSettings()
5-
const { locale, locales, setLocale: setNuxti18nLocale } = useI18n()
5+
const { locale: currentLocale, locales, setLocale: setNuxti18nLocale } = useI18n()
66
const colorMode = useColorMode()
77
const { currentLocaleStatus, isSourceLocale } = useI18nStatus()
88
const keyboardShortcutsEnabled = useKeyboardShortcuts()
@@ -242,8 +242,8 @@ const setLocale: typeof setNuxti18nLocale = newLocale => {
242242
<SelectField
243243
id="language-select"
244244
:items="locales.map(loc => ({ label: loc.name ?? '', value: loc.code }))"
245-
v-model="locale"
246-
@update:modelValue="setLocale($event as typeof locale)"
245+
v-model="currentLocale"
246+
@update:modelValue="setLocale($event as typeof currentLocale)"
247247
block
248248
size="sm"
249249
class="max-w-48"
@@ -271,15 +271,24 @@ const setLocale: typeof setNuxti18nLocale = newLocale => {
271271
<!-- Simple help link for source locale -->
272272
<template v-else>
273273
<a
274-
href="https://i18n.npmx.dev/"
274+
href="https://github.com/npmx-dev/npmx.dev/tree/main/i18n/locales"
275275
target="_blank"
276276
rel="noopener noreferrer"
277277
class="inline-flex items-center gap-2 text-sm text-fg-muted hover:text-fg transition-colors duration-200 focus-visible:outline-accent/70 rounded"
278278
>
279-
<span class="i-lucide:languages w-4 h-4" aria-hidden="true" />
279+
<span class="i-simple-icons:github w-4 h-4" aria-hidden="true" />
280280
{{ $t('settings.help_translate') }}
281281
</a>
282282
</template>
283+
<div>
284+
<LinkBase
285+
:to="{ name: 'translation-status' }"
286+
class="font-sans text-fg-muted text-sm"
287+
>
288+
<span class="i-lucide:languages w-4 h-4" aria-hidden="true" />
289+
{{ $t('settings.translation_status') }}
290+
</LinkBase>
291+
</div>
283292
</div>
284293
</section>
285294

0 commit comments

Comments
 (0)