Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/components/AppFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ const closeModal = () => modalRef.value?.close?.()
<LinkBase :to="{ name: 'accessibility' }">
{{ $t('a11y.footer_title') }}
</LinkBase>
<LinkBase :to="{ name: 'translation-status' }">
{{ $t('translation_status.title') }}
</LinkBase>
<button
type="button"
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"
Expand Down
108 changes: 108 additions & 0 deletions app/components/ProgressBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<script setup lang="ts">
type CompletionColorScheme = {
low: number
medium: number
high: number
full?: boolean
}

const props = withDefaults(
defineProps<{
val: number
label: string
scheme?: CompletionColorScheme
}>(),
{
scheme: () => ({
low: 50,
medium: 75,
high: 90,
full: true,
}),
},
)

const completionClass = computed<string>(() => {
if (props.scheme.full && props.val === 100) {
return 'full'
} else if (props.val > props.scheme.high) {
return 'high'
} else if (props.val > props.scheme.medium) {
return 'medium'
} else if (props.val > props.scheme.low) {
return 'low'
}

return ''
})
</script>

<template>
<progress
class="flex-1 h-3 rounded-full overflow-hidden"
max="100"
:value="val"
:class="completionClass"
:aria-label="label"
></progress>
</template>

<style scoped>
/* Reset & Base */
progress {
-webkit-appearance: none;
appearance: none;
border: none;
@apply bg-bg-muted; /* Background for container */
}

/* Webkit Container */
progress::-webkit-progress-bar {
@apply bg-bg-muted;
}

/* Value Bar */
/* Default <= 50 */
progress::-webkit-progress-value {
@apply bg-red-800 dark:bg-red-900;
}
progress::-moz-progress-bar {
@apply bg-red-800 dark:bg-red-900;
}

/* Low > scheme.low (default: 50) */
progress.low::-webkit-progress-value {
@apply bg-red-500 dark:bg-red-700;
}
progress.low::-moz-progress-bar {
@apply bg-red-500 dark:bg-red-700;
}

/* Medium scheme.medium (default: 75) */
progress.medium::-webkit-progress-value {
@apply bg-orange-500;
}
progress.medium::-moz-progress-bar {
@apply bg-orange-500;
}

/* Good > scheme.high (default: 90) */
progress.high::-webkit-progress-value {
@apply bg-green-500 dark:bg-green-700;
}
progress.high::-moz-progress-bar {
@apply bg-green-500 dark:bg-green-700;
}

/* Completed = 100 */
progress.full::-webkit-progress-value {
@apply bg-green-700 dark:bg-green-500;
}
progress.full::-moz-progress-bar {
@apply bg-green-700 dark:bg-green-500;
}

details[dir='rtl']:not([open]) .icon-rtl {
transform: scale(-1, 1);
}
</style>
132 changes: 132 additions & 0 deletions app/components/Translation/StatusByFile.unused.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<script setup lang="ts">
/** This component is not used at the moment, but we keep it not to lose code
* produced to output report for translations per file. As we might need if
* we split single translation files into multiple as it grows significantly
*/
const { locale } = useI18n()
const { fetchStatus, localesMap } = useI18nStatus()

const localeEntries = computed(() => {
const l = localesMap.value?.values()
if (!l) return []
return [...mapFiles(l)]
})

function* mapFiles(
map: MapIterator<I18nLocaleStatus>,
): Generator<FileEntryStatus, undefined, void> {
Comment thread
alex-key marked this conversation as resolved.
for (const entry of map) {
yield {
...entry,
lang: entry.lang,
done: entry.completedKeys,
missing: entry.missingKeys.length,
file: entry.githubEditUrl.split('/').pop() ?? entry.lang,
}
}
}
</script>

<template>
<section class="prose prose-invert max-w-none space-y-8 pt-8">
<h2 id="by-file" tabindex="-1" class="text-xs text-fg-muted uppercase tracking-wider mb-4">
{{ $t('translation_status.by_file') }}
</h2>
<table class="w-full text-start border-collapse">
<thead class="border-b border-border text-start">
<tr>
<th scope="col" class="py-2 px-2 font-medium text-fg-subtle text-sm">
{{ $t('translation_status.table.file') }}
</th>
<th scope="col" class="py-2 px-2 font-medium text-fg-subtle text-sm">
{{ $t('translation_status.table.status') }}
</th>
</tr>
</thead>
<tbody class="divide-y divide-border/50">
<template v-if="fetchStatus === 'error'">
<tr>
<td colspan="2" class="py-4 px-2 text-center text-red-500">
{{ $t('translation_status.table.error') }}
</td>
</tr>
</template>
<template v-else-if="fetchStatus === 'pending' || fetchStatus === 'idle'">
<tr>
<td colspan="2" class="py-4 px-2 text-center text-fg-muted">
<SkeletonBlock class="h-10 w-full mb-4" />
<SkeletonBlock class="h-10 w-full mb-4" />
<SkeletonBlock class="h-10 w-full mb-4" />
</td>
</tr>
</template>
<template v-else-if="!localeEntries || localeEntries.length === 0">
<tr>
<td colspan="2" class="py-4 px-2 text-center text-fg-muted">
{{ $t('translation_status.table.empty') }}
</td>
</tr>
</template>
<template v-else>
<tr>
<td class="py-3 px-2 font-mono text-sm">
<LinkBase to="https://github.com/npmx-dev/npmx.dev/blob/main/i18n/locales/en.json">
<i18n-t
keypath="translation_status.table.file_link"
scope="global"
tag="span"
:class="locale === 'en-US' ? 'font-bold' : undefined"
>
<template #file>en.json</template>
<template #lang>en-US</template>
</i18n-t>
</LinkBase>
</td>
<td class="py-3 px-2">
<div class="flex items-center gap-2">
<progress
class="done w-24 h-1.5 rounded-full overflow-hidden"
max="100"
value="100"
></progress>
<span class="text-xs font-mono text-fg-muted">
{{ $n(1, 'percentage') }}
</span>
</div>
</td>
</tr>
<tr v-for="file in localeEntries" :key="file.lang">
<td class="py-3 px-2 font-mono text-sm">
<LinkBase :to="file.githubEditUrl">
<i18n-t
keypath="translation_status.table.file_link"
scope="global"
tag="span"
:class="locale === file.lang ? 'font-bold' : undefined"
>
<template #file>
{{ file.file }}
</template>
<template #lang>
{{ file.lang }}
</template>
</i18n-t>
</LinkBase>
</td>
<td class="py-3 px-2">
<div class="flex items-center gap-2">
<ProgressBar
:val="file.percentComplete"
:label="$t('translation_status.progress_label', { locale: file.label })"
/>
<span class="text-xs font-mono text-fg-muted">{{
$n(file.percentComplete / 100, 'percentage')
}}</span>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</section>
</template>
22 changes: 15 additions & 7 deletions app/composables/useI18nStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Provides information about translation progress for each locale.
*/
export function useI18nStatus() {
const { locale } = useI18n()
const { locale: currentLocale } = useI18n()

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

const localesMap = computed<Map<string, I18nLocaleStatus> | undefined>(() => {
return status.value?.locales.reduce((acc, locale) => {
acc.set(locale.lang, locale)
return acc
}, new Map())
})

/**
* Get the translation status for a specific locale
*/
function getLocaleStatus(langCode: string): I18nLocaleStatus | null {
if (!status.value) return null
return status.value.locales.find(l => l.lang === langCode) ?? null
return localesMap.value?.get(langCode) ?? null
}

/**
* Translation status for the current locale
*/
const currentLocaleStatus = computed<I18nLocaleStatus | null>(() => {
return getLocaleStatus(locale.value)
})
const currentLocaleStatus = computed<I18nLocaleStatus | null>(() =>
getLocaleStatus(currentLocale.value),
)

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

/**
Expand All @@ -74,5 +80,7 @@ export function useI18nStatus() {
isSourceLocale,
/** GitHub edit URL for current locale */
githubEditUrl,
/** locale info map by lang */
localesMap,
}
}
19 changes: 14 additions & 5 deletions app/pages/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const router = useRouter()
const canGoBack = useCanGoBack()
const { settings } = useSettings()
const { locale, locales, setLocale: setNuxti18nLocale } = useI18n()
const { locale: currentLocale, locales, setLocale: setNuxti18nLocale } = useI18n()
const colorMode = useColorMode()
const { currentLocaleStatus, isSourceLocale } = useI18nStatus()
const keyboardShortcutsEnabled = useKeyboardShortcuts()
Expand Down Expand Up @@ -242,8 +242,8 @@ const setLocale: typeof setNuxti18nLocale = newLocale => {
<SelectField
id="language-select"
:items="locales.map(loc => ({ label: loc.name ?? '', value: loc.code }))"
v-model="locale"
@update:modelValue="setLocale($event as typeof locale)"
v-model="currentLocale"
@update:modelValue="setLocale($event as typeof currentLocale)"
block
size="sm"
class="max-w-48"
Expand Down Expand Up @@ -271,15 +271,24 @@ const setLocale: typeof setNuxti18nLocale = newLocale => {
<!-- Simple help link for source locale -->
<template v-else>
<a
href="https://i18n.npmx.dev/"
href="https://github.com/npmx-dev/npmx.dev/tree/main/i18n/locales"
target="_blank"
rel="noopener noreferrer"
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"
>
<span class="i-lucide:languages w-4 h-4" aria-hidden="true" />
<span class="i-simple-icons:github w-4 h-4" aria-hidden="true" />
{{ $t('settings.help_translate') }}
</a>
</template>
<div>
<LinkBase
:to="{ name: 'translation-status' }"
class="font-sans text-fg-muted text-sm"
>
<span class="i-lucide:languages w-4 h-4" aria-hidden="true" />
{{ $t('settings.translation_status') }}
</LinkBase>
</div>
</div>
</section>

Expand Down
Loading
Loading