Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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">
export 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> {
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,
}
}
22 changes: 10 additions & 12 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 All @@ -269,17 +269,15 @@ const setLocale: typeof setNuxti18nLocale = newLocale => {
</template>

<!-- Simple help link for source locale -->
<template v-else>
<a
href="https://i18n.npmx.dev/"
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"
<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.help_translate') }}
</a>
</template>
{{ $t('settings.translation_status') }}
</LinkBase>
</div>
</div>
</section>

Expand Down
Loading
Loading