forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1<script setup lang="ts">
2import type { I18nLocaleStatus } from '#shared/types'
3
4const props = defineProps<{
5 status: I18nLocaleStatus
6}>()
7
8// Show first N missing keys by default
9const INITIAL_SHOW_COUNT = 5
10const showAll = shallowRef(false)
11
12const missingKeysToShow = computed(() => {
13 if (showAll.value || props.status.missingKeys.length <= INITIAL_SHOW_COUNT) {
14 return props.status.missingKeys
15 }
16 return props.status.missingKeys.slice(0, INITIAL_SHOW_COUNT)
17})
18
19const hasMoreKeys = computed(
20 () => props.status.missingKeys.length > INITIAL_SHOW_COUNT && !showAll.value,
21)
22
23const remainingCount = computed(() => props.status.missingKeys.length - INITIAL_SHOW_COUNT)
24
25// Generate a GitHub URL that pre-fills the edit with guidance
26const contributionGuideUrl =
27 'https://github.com/npmx-dev/npmx.dev/blob/main/CONTRIBUTING.md#localization-i18n'
28
29// Copy missing keys as JSON template to clipboard
30const { copy, copied } = useClipboard()
31
32const numberFormatter = useNumberFormatter()
33const percentageFormatter = useNumberFormatter({ style: 'percent' })
34
35function copyMissingKeysTemplate() {
36 // Create a template showing what needs to be added
37 const template = props.status.missingKeys.map(key => ` "${key}": ""`).join(',\n')
38
39 const fullTemplate = `// Missing translations for ${props.status.label} (${props.status.lang})
40// Add these keys to: i18n/locales/${props.status.lang}.json
41
42${template}`
43
44 copy(fullTemplate)
45}
46</script>
47
48<template>
49 <div class="space-y-3">
50 <!-- Progress section -->
51 <div class="space-y-1.5">
52 <div class="flex items-center justify-between text-xs text-fg-muted">
53 <span>{{ $t('settings.translation_progress') }}</span>
54 <span class="tabular-nums"
55 >{{ numberFormatter.format(status.completedKeys) }}/{{
56 numberFormatter.format(status.totalKeys)
57 }}
58 ({{ percentageFormatter.format(status.percentComplete / 100) }})</span
59 >
60 </div>
61 <div class="h-1.5 bg-bg rounded-full overflow-hidden">
62 <div
63 class="h-full bg-accent transition-all duration-300 motion-reduce:transition-none"
64 :style="{ width: `${status.percentComplete}%` }"
65 />
66 </div>
67 </div>
68
69 <!-- Missing keys section -->
70 <div v-if="status.missingKeys.length > 0" class="space-y-2">
71 <div class="flex items-center justify-between">
72 <h4 class="text-xs text-fg-muted font-medium">
73 {{
74 $t(
75 'i18n.missing_keys',
76 { count: numberFormatter.format(status.missingKeys.length) },
77 status.missingKeys.length,
78 )
79 }}
80 </h4>
81 <button
82 type="button"
83 class="text-xs text-accent hover:underline rounded focus-visible:outline-accent/70"
84 @click="copyMissingKeysTemplate"
85 >
86 {{ copied ? $t('common.copied') : $t('i18n.copy_keys') }}
87 </button>
88 </div>
89
90 <ul class="space-y-1 text-xs font-mono bg-bg rounded-md p-2 max-h-32 overflow-y-auto">
91 <li v-for="key in missingKeysToShow" :key="key" class="text-fg-muted truncate" :title="key">
92 {{ key }}
93 </li>
94 </ul>
95
96 <button
97 v-if="hasMoreKeys"
98 type="button"
99 class="text-xs text-fg-muted hover:text-fg rounded focus-visible:outline-accent/70"
100 @click="showAll = true"
101 >
102 {{
103 $t(
104 'i18n.show_more_keys',
105 { count: numberFormatter.format(remainingCount) },
106 remainingCount,
107 )
108 }}
109 </button>
110 </div>
111
112 <!-- Contribution guidance -->
113 <div class="pt-2 border-t border-border space-y-2">
114 <p class="text-xs text-fg-muted">
115 {{ $t('i18n.contribute_hint') }}
116 </p>
117
118 <div class="flex flex-wrap gap-2">
119 <a
120 :href="status.githubEditUrl"
121 target="_blank"
122 rel="noopener noreferrer"
123 class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs bg-bg hover:bg-bg-subtle border border-border rounded-md transition-colors focus-visible:outline-accent/70"
124 >
125 <span class="i-lucide:pen w-3.5 h-3.5" aria-hidden="true" />
126 {{ $t('i18n.edit_on_github') }}
127 </a>
128
129 <a
130 :href="contributionGuideUrl"
131 target="_blank"
132 rel="noopener noreferrer"
133 class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs text-fg-muted hover:text-fg rounded transition-colors focus-visible:outline-accent/70"
134 >
135 <span class="i-lucide:file-text w-3.5 h-3.5" aria-hidden="true" />
136 {{ $t('i18n.view_guide') }}
137 </a>
138 </div>
139 </div>
140 </div>
141</template>