<script setup lang="ts"> import type { TranslationStatus } from '../../types' const localesStatuses: TranslationStatus = await import('../../translation-status.json').then(m => m.default) const totalReference = localesStatuses.en.total type Tab = 'missing' | 'outdated' const hidden = ref(true) const locale = ref() const localeTab = ref<Tab>('missing') const copied = ref(false) const currentLocale = computed(() => { if (hidden.value || !locale.value) return undefined return localesStatuses as Record<string, any> }) const localeTitle = computed(() => { if (hidden.value || !locale.value) return undefined return localeTab.value === 'missing' ? `Missing keys in ${locale.value.file}` : `Outdated keys in ${locale.value.file}` }) const missingEntries = computed<string[]>(() => { if (hidden.value || !currentLocale.value || localeTab.value !== 'missing') return [] return localesStatuses[locale.value].missing }) const outdatedEntries = computed<string[]>(() => { if (hidden.value || !currentLocale.value || localeTab.value !== 'outdated') return [] return localesStatuses[locale.value]!.outdated }) const showDetail = (key: string, tab: Tab = 'missing', fromTab = false) => { if (key === locale.value && tab === localeTab.value) { if (fromTab) return nextTick().then(() => hidden.value = !hidden.value) return } locale.value = key localeTab.value = tab nextTick().then(() => hidden.value = false) } const copyToClipboard = async () => { try { await navigator.clipboard.writeText([ `# ${localeTitle.value}`, (localeTab.value === 'missing' ? missingEntries.value : outdatedEntries.value).join('\n')].join('\n'), ) copied.value = true setTimeout(() => copied.value = false, 750) } catch {} } </script> <template> <div> <table class="w-full"> <caption> <div>You can see the detail (missing and outdated keys) by clicking on the corresponding row.</div> <div> If you want to send a PR, click on <strong>Edit</strong> link on the corresponding translation file, it will open <strong>Codeflow</strong>: <NuxtLink target="_blank" href="https://developer.stackblitz.com/codeflow/working-in-codeflow-ide#making-a-pr-with-codeflow-ide" title="How to make a PR with Codeflow IDE (opens in new window)" > read the following guide </NuxtLink> </div> </caption> <thead> <tr> <th>Language</th> <th title="Keys correctly translated"> Translated </th> <th title="Keys missing from source which need translation for the language"> Missing </th> <th title="Keys which could be safely removed"> Outdated </th> <th>Total</th> <th>Actions</th> </tr> </thead> <tbody> <template v-for="({ title, file, translated, missing, outdated, total, isSource }, key) in localesStatuses" :key="key"> <tr v-if="totalReference > 0" :class="[{ expandable: !isSource }]" :title="!isSource ? 'Click to show detail' : undefined" @click="!isSource && showDetail(key, 'missing')" > <td :class="[{ expandable: !isSource }]"> <div> <ToogleIcon v-if="!isSource" :up="hidden || key !== locale" /> {{ title }} </div> </td> <template v-if="isSource"> <td colspan="5" class="source-text"> <div> {{ total }} keys as source </div> </td> </template> <template v-else> <td> <strong>{{ `${translated?.length ?? 0}` }}</strong> {{ `(${(100 * (translated?.length ?? 0) / totalReference).toFixed(1)}%)` }} </td> <td> <strong>{{ `${missing?.length ?? 0}` }}</strong> {{ `(${(100 * (missing?.length ?? 0) / totalReference).toFixed(1)}%)` }} </td> <td> <strong>{{ `${outdated?.length ?? 0}` }}</strong> {{ `(${(100 * (outdated?.length ?? 0) / totalReference).toFixed(1)}%)` }} </td> <td><strong>{{ `${total}` }}</strong></td> <td> <NuxtLink v-if="outdated.length > 0 || missing.length > 0" :href="`https://pr.new/github.com/elk-zone/elk/tree/main/locales/${file}`" target="_blank" class="codeflow" title="Raise a PR with Codeflow (opens in new window)" @click.stop > Edit <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"> <path fill="currentColor" d="M5 21q-.825 0-1.413-.587Q3 19.825 3 19V5q0-.825.587-1.413Q4.175 3 5 3h7v2H5v14h14v-7h2v7q0 .825-.587 1.413Q19.825 21 19 21Zm4.7-5.3l-1.4-1.4L17.6 5H14V3h7v7h-2V6.4Z" /> </svg> </NuxtLink> </td> </template> </tr> <template v-if="key === locale && !hidden"> <tr> <td colspan="6"> <div class="detail"> <header> <h2 class="tabs"> <button :class="localeTab === 'missing' ? 'current' : null" @click="showDetail(key, 'missing', true)" > Missing keys </button> <button :class="localeTab === 'outdated' ? 'current' : null" @click="showDetail(key, 'outdated', true)" > Outdated keys </button> </h2> </header> <ul v-if="localeTab === 'missing'"> <li v-for="entry in missingEntries" :key="entry"> <pre>{{ entry }}</pre> </li> </ul> <ul v-else> <li v-for="entry in outdatedEntries" :key="entry"> <pre>{{ entry }}</pre> </li> </ul> <button @click="copyToClipboard()"> <ClipboardIcon :copy="!copied" /> Copy to clipboard </button> </div> </td> </tr> </template> </template> </tbody> </table> </div> </template> <style scoped> table { font-size: 0.9rem; width: 100%; border-collapse: collapse; border: 1px solid #ccc; } pre { font-size: 0.75rem; } caption { padding: 0.3rem; background: #eee; border: 1px solid #ccc; border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom: none; } caption a { text-decoration: underline; } th { text-align: left; border-bottom: 1px solid #ccc; padding: 0.5rem; } th:not(:first-of-type), td:not(:first-of-type) { border-left: 1px solid #eee; } td { padding: 0.5rem; } tr.expandable td:first-of-type { padding-left: 4px; } tr.expandable, tr.expandable td { cursor: pointer; } a.codeflow, td.expandable div { display: flex; align-items: center; flex-direction: row; column-gap: 4px; } td.expandable > svg { color: currentColor; } tbody tr td { border-bottom: 1px solid #eee; } th[title] { text-decoration: underline dotted white; } .source-text { text-align: center; font-weight: bold; padding: 10px 0; text-transform: uppercase; } .detail { border: 1px solid #ccc; border-radius: 3px; } .detail header { padding: 0 0.3rem; display: flex; background: #eee; justify-content: space-between; align-items: center; } .detail header h2 button { font-weight: bold; padding: 0.5rem; background-color: #eee; } .detail header .tabs button.current { background-color: white; } .detail header .tabs + .heading-buttons { display: flex; flex-direction: row; column-gap: 0.4rem; align-items: center; } .detail ul { padding: 0.3rem 0.5rem; max-height: 250px; min-height: 250px; overflow-y: auto; border-bottom: 1px solid #eee; } .detail > button { display: flex; /*justify-content: space-between;*/ align-items: center; column-gap: 0.3rem; padding: 0.3rem 0.5rem; background: transparent; cursor: pointer; border: 1px solid #ccc; border-radius: 3px; margin: 0.5rem; } @media (prefers-color-scheme: dark) { .detail header { background: #333; color: #fff; } .detail header h2 button { background-color: #333; color: #fff; } .detail header .tabs button.current { background-color: white; color: #333; } table caption { background: #333; color: #fff; } } </style>