feat(docs): add translation status (#1689)
Co-authored-by: Michel EDIGHOFFER <edimitchel@gmail.com>zio/stable
parent
0eefcfa281
commit
972a13499f
|
@ -9,3 +9,4 @@ public/
|
||||||
https-dev-config/localhost.crt
|
https-dev-config/localhost.crt
|
||||||
https-dev-config/localhost.key
|
https-dev-config/localhost.key
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
docs/translation-status.json
|
||||||
|
|
|
@ -198,7 +198,7 @@ const buildLocales = () => {
|
||||||
return useLocales.sort((a, b) => a.code.localeCompare(b.code))
|
return useLocales.sort((a, b) => a.code.localeCompare(b.code))
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLocales = buildLocales()
|
export const currentLocales = buildLocales()
|
||||||
|
|
||||||
const datetimeFormats = Object.values(currentLocales).reduce((acc, data) => {
|
const datetimeFormats = Object.values(currentLocales).reduce((acc, data) => {
|
||||||
const dateTimeFormats = data.dateTimeFormats
|
const dateTimeFormats = data.dateTimeFormats
|
||||||
|
|
|
@ -10,3 +10,4 @@ dist
|
||||||
sw.*
|
sw.*
|
||||||
.env
|
.env
|
||||||
.output
|
.output
|
||||||
|
translation-status.json
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ClipboardIcon',
|
||||||
|
props: { copy: Boolean },
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg v-if="copy" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-7 0a1 1 0 0 1 1 1a1 1 0 0 1-1 1a1 1 0 0 1-1-1a1 1 0 0 1 1-1M7 7h10V5h2v14H5V5h2v2Z" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-7 0a1 1 0 0 1 1 1a1 1 0 0 1-1 1a1 1 0 0 1-1-1a1 1 0 0 1 1-1M7 7h10V5h2v14H5V5h2v2m.5 6.5L9 12l2 2l4.5-4.5L17 11l-6 6l-3.5-3.5Z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ToogleIcon',
|
||||||
|
props: { up: Boolean },
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg v-if="up" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="m12 10.828l-4.95 4.95l-1.414-1.414L12 8l6.364 6.364l-1.414 1.414z" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="m12 13.172l4.95-4.95l1.414 1.414L12 16L5.636 9.636L7.05 8.222z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
|
@ -0,0 +1,338 @@
|
||||||
|
<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>
|
|
@ -34,6 +34,10 @@ Elk uses [Vitest](https://vitest.dev). You can run the test suite with:
|
||||||
nr test
|
nr test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Translation status
|
||||||
|
|
||||||
|
<TranslationState />
|
||||||
|
|
||||||
# Stack
|
# Stack
|
||||||
|
|
||||||
- [Vite](https://vitejs.dev/) - Next Generation Frontend Tooling
|
- [Vite](https://vitejs.dev/) - Next Generation Frontend Tooling
|
||||||
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
{
|
||||||
|
"name": "elk-docs",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "elk-docs",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@nuxt-themes/docus": "^1.6.1",
|
||||||
|
"@types/flat": "^5.0.2",
|
||||||
|
"flat": "^5.0.2",
|
||||||
|
"flatten": "^1.0.3",
|
||||||
|
"iso-639-1": "^2.1.15",
|
||||||
|
"nuxt": "^3.1.1",
|
||||||
|
"vite-plugin-virtual": "^0.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../node_modules/.pnpm/@nuxt-themes+docus@1.6.3_nuxt@3.1.1/node_modules/@nuxt-themes/docus": {
|
||||||
|
"version": "1.6.3",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxt-themes/elements": "^0.5.2",
|
||||||
|
"@nuxt-themes/tokens": "^1.6.2",
|
||||||
|
"@nuxt-themes/typography": "^0.6.0",
|
||||||
|
"@nuxt/content": "^2.4.1",
|
||||||
|
"@nuxthq/studio": "^0.6.5",
|
||||||
|
"@vueuse/nuxt": "^9.11.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@algolia/client-search": "^4.14.3",
|
||||||
|
"@docsearch/css": "^3.3.2",
|
||||||
|
"@docsearch/js": "^3.3.2",
|
||||||
|
"@nuxtjs/algolia": "^1.5.0",
|
||||||
|
"@nuxtjs/eslint-config-typescript": "^12.0.0",
|
||||||
|
"eslint": "^8.32.0",
|
||||||
|
"nuxt": "3.1.1",
|
||||||
|
"nuxt-plausible": "^0.1.2",
|
||||||
|
"release-it": "^15.6.0",
|
||||||
|
"typescript": "^4.9.4",
|
||||||
|
"vue": "^3.2.45"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../node_modules/.pnpm/flat@5.0.2/node_modules/flat": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"flat": "cli.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"mocha": "~8.1.1",
|
||||||
|
"standard": "^14.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../node_modules/.pnpm/flatten@1.0.3/node_modules/flatten": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {}
|
||||||
|
},
|
||||||
|
"../node_modules/.pnpm/iso-639-1@2.1.15/node_modules/iso-639-1": {
|
||||||
|
"version": "2.1.15",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-cli": "^6.26.0",
|
||||||
|
"babel-core": "^6.26.0",
|
||||||
|
"babel-loader": "^7.1.2",
|
||||||
|
"babel-plugin-add-module-exports": "^0.2.1",
|
||||||
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
|
"babel-preset-es2015": "^6.24.1",
|
||||||
|
"babel-preset-stage-0": "^6.24.1",
|
||||||
|
"clean-webpack-plugin": "^0.1.17",
|
||||||
|
"mocha": "^4.0.1",
|
||||||
|
"webpack": "^3.10.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../node_modules/.pnpm/nuxt@3.1.1/node_modules/nuxt": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxt/devalue": "^2.0.0",
|
||||||
|
"@nuxt/kit": "3.1.1",
|
||||||
|
"@nuxt/schema": "3.1.1",
|
||||||
|
"@nuxt/telemetry": "^2.1.9",
|
||||||
|
"@nuxt/ui-templates": "^1.1.0",
|
||||||
|
"@nuxt/vite-builder": "3.1.1",
|
||||||
|
"@unhead/ssr": "^1.0.18",
|
||||||
|
"@vue/reactivity": "^3.2.45",
|
||||||
|
"@vue/shared": "^3.2.45",
|
||||||
|
"@vueuse/head": "^1.0.23",
|
||||||
|
"chokidar": "^3.5.3",
|
||||||
|
"cookie-es": "^0.5.0",
|
||||||
|
"defu": "^6.1.2",
|
||||||
|
"destr": "^1.2.2",
|
||||||
|
"escape-string-regexp": "^5.0.0",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"fs-extra": "^11.1.0",
|
||||||
|
"globby": "^13.1.3",
|
||||||
|
"h3": "^1.0.2",
|
||||||
|
"hash-sum": "^2.0.0",
|
||||||
|
"hookable": "^5.4.2",
|
||||||
|
"jiti": "^1.16.2",
|
||||||
|
"knitwork": "^1.0.0",
|
||||||
|
"magic-string": "^0.27.0",
|
||||||
|
"mlly": "^1.1.0",
|
||||||
|
"nitropack": "^2.0.0",
|
||||||
|
"nuxi": "3.1.1",
|
||||||
|
"ofetch": "^1.0.0",
|
||||||
|
"ohash": "^1.0.0",
|
||||||
|
"pathe": "^1.1.0",
|
||||||
|
"perfect-debounce": "^0.1.3",
|
||||||
|
"scule": "^1.0.0",
|
||||||
|
"strip-literal": "^1.0.0",
|
||||||
|
"ufo": "^1.0.1",
|
||||||
|
"ultrahtml": "^1.2.0",
|
||||||
|
"unctx": "^2.1.1",
|
||||||
|
"unenv": "^1.0.1",
|
||||||
|
"unhead": "^1.0.18",
|
||||||
|
"unimport": "^2.0.1",
|
||||||
|
"unplugin": "^1.0.1",
|
||||||
|
"untyped": "^1.2.2",
|
||||||
|
"vue": "^3.2.45",
|
||||||
|
"vue-bundle-renderer": "^1.0.0",
|
||||||
|
"vue-devtools-stub": "^0.1.0",
|
||||||
|
"vue-router": "^4.1.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"nuxi": "bin/nuxt.mjs",
|
||||||
|
"nuxt": "bin/nuxt.mjs"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/fs-extra": "^11.0.1",
|
||||||
|
"@types/hash-sum": "^1.0.0",
|
||||||
|
"unbuild": "latest"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.16.0 || ^16.10.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../node_modules/.pnpm/vite-plugin-virtual@0.1.1/node_modules/vite-plugin-virtual": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^0.6.2",
|
||||||
|
"@types/jest": "^26.0.22",
|
||||||
|
"@types/node": "^14.14.37",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.20.0",
|
||||||
|
"eslint": "^7.23.0",
|
||||||
|
"jest": "^26.6.3",
|
||||||
|
"jest-esbuild": "^0.1.5",
|
||||||
|
"rollup": "^2.44.0",
|
||||||
|
"ts-node": "^9.1.1",
|
||||||
|
"tsup": "^4.8.21",
|
||||||
|
"typescript": "^4.2.3",
|
||||||
|
"vite": "^2.1.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt-themes/docus": {
|
||||||
|
"resolved": "../node_modules/.pnpm/@nuxt-themes+docus@1.6.3_nuxt@3.1.1/node_modules/@nuxt-themes/docus",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/flat": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/flat/-/flat-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-3zsplnP2djeps5P9OyarTxwRpMLoe5Ash8aL9iprw0JxB+FAHjY+ifn4yZUuW4/9hqtnmor6uvjSRzJhiVbrEQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/flat": {
|
||||||
|
"resolved": "../node_modules/.pnpm/flat@5.0.2/node_modules/flat",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/flatten": {
|
||||||
|
"resolved": "../node_modules/.pnpm/flatten@1.0.3/node_modules/flatten",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/iso-639-1": {
|
||||||
|
"resolved": "../node_modules/.pnpm/iso-639-1@2.1.15/node_modules/iso-639-1",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/nuxt": {
|
||||||
|
"resolved": "../node_modules/.pnpm/nuxt@3.1.1/node_modules/nuxt",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/vite-plugin-virtual": {
|
||||||
|
"resolved": "../node_modules/.pnpm/vite-plugin-virtual@0.1.1/node_modules/vite-plugin-virtual",
|
||||||
|
"link": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,10 +6,13 @@
|
||||||
"dev": "nuxi dev",
|
"dev": "nuxi dev",
|
||||||
"build": "nuxi build",
|
"build": "nuxi build",
|
||||||
"generate": "nuxi generate",
|
"generate": "nuxi generate",
|
||||||
"preview": "nuxi preview"
|
"preview": "nuxi preview",
|
||||||
|
"prepare-translation-status": "nuxi prepare && esno scripts/prepare-translation-status.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt-themes/docus": "^1.6.1",
|
"@nuxt-themes/docus": "^1.6.1",
|
||||||
|
"@types/flat": "^5.0.2",
|
||||||
|
"flat": "^5.0.2",
|
||||||
"nuxt": "^3.1.1"
|
"nuxt": "^3.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { flatten } from 'flat'
|
||||||
|
import { createResolver } from '@nuxt/kit'
|
||||||
|
import { readFile, writeFile } from 'fs-extra'
|
||||||
|
import { currentLocales } from '../../config/i18n'
|
||||||
|
import vsCodeConfig from '../../.vscode/settings.json'
|
||||||
|
import type { LocaleEntry } from '../types'
|
||||||
|
|
||||||
|
export const localeData: [code: string, file: string[], title: string][]
|
||||||
|
= currentLocales.map((l: any) => [l.code, l.files ? l.files : [l.file!], l.name ?? l.code])
|
||||||
|
|
||||||
|
function merge(src: Record<string, any>, dst: Record<string, any>) {
|
||||||
|
for (const key in src) {
|
||||||
|
if (typeof src[key] === 'object') {
|
||||||
|
if (!dst[key])
|
||||||
|
dst[key] = {}
|
||||||
|
|
||||||
|
merge(src[key], dst[key])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dst[key] = src[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readI18nFile(file: string | string[]) {
|
||||||
|
const resolver = createResolver(import.meta.url)
|
||||||
|
if (Array.isArray(file)) {
|
||||||
|
const files = await Promise.all(file.map(f => async () => {
|
||||||
|
return JSON.parse(Buffer.from(
|
||||||
|
await readFile(resolver.resolve(`../../locales/${f}`), 'utf-8'),
|
||||||
|
).toString())
|
||||||
|
})).then(f => f.map(f => f()))
|
||||||
|
const data: Record<string, any> = files[0]
|
||||||
|
files.splice(0, 1)
|
||||||
|
files.forEach(f => merge(f, data))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return JSON.parse(Buffer.from(
|
||||||
|
await readFile(resolver.resolve(`../../locales/${file}`), 'utf-8'),
|
||||||
|
).toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compare(
|
||||||
|
baseEntries: Record<string, string>,
|
||||||
|
file: string | string[],
|
||||||
|
data: LocaleEntry,
|
||||||
|
) {
|
||||||
|
const baseEntriesKeys = Object.keys(baseEntries)
|
||||||
|
const entries: Record<string, any> = await readI18nFile(file)
|
||||||
|
const flatEntriesKeys = Object.keys(flatten<typeof entries, Record<string, string>>(entries))
|
||||||
|
|
||||||
|
data.translated = flatEntriesKeys.filter(e => baseEntriesKeys.includes(e))
|
||||||
|
data.missing = baseEntriesKeys.filter(e => !flatEntriesKeys.includes(e))
|
||||||
|
data.outdated = flatEntriesKeys.filter(e => !baseEntriesKeys.includes(e))
|
||||||
|
data.total = flatEntriesKeys.length
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prepareTranslationStatus() {
|
||||||
|
const sourceLanguageLocale = localeData.find(l => l[0] === vsCodeConfig['i18n-ally.sourceLanguage'])!
|
||||||
|
const entries: Record<string, any> = await readI18nFile(sourceLanguageLocale[1])
|
||||||
|
const flatEntries = flatten<typeof entries, Record<string, string>>(entries)
|
||||||
|
const data: Record<string, LocaleEntry> = {
|
||||||
|
en: {
|
||||||
|
translated: [],
|
||||||
|
file: 'en.json',
|
||||||
|
missing: [],
|
||||||
|
outdated: [],
|
||||||
|
title: 'English (source)',
|
||||||
|
total: Object.keys(flatEntries).length,
|
||||||
|
isSource: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(localeData.filter(l => l[0] !== 'en-US').map(async ([code, file, title]) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.info(`Comparing ${code}...`, title)
|
||||||
|
data[code] = {
|
||||||
|
title,
|
||||||
|
file: Array.isArray(file) ? file[file.length - 1] : file,
|
||||||
|
translated: [],
|
||||||
|
missing: [],
|
||||||
|
outdated: [],
|
||||||
|
total: 0,
|
||||||
|
}
|
||||||
|
await compare(flatEntries, file, data[code])
|
||||||
|
}))
|
||||||
|
|
||||||
|
const sorted: Record<string, any> = { en: { ...data.en } }
|
||||||
|
|
||||||
|
Object.keys(data).filter(k => k !== 'en').sort((a, b) => {
|
||||||
|
return data[a].translated.length - data[b].translated.length
|
||||||
|
}).forEach((k) => {
|
||||||
|
sorted[k] = { ...data[k] }
|
||||||
|
})
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
createResolver(import.meta.url).resolve('../translation-status.json'),
|
||||||
|
JSON.stringify(sorted, null, 2),
|
||||||
|
{ encoding: 'utf-8' },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareTranslationStatus()
|
|
@ -0,0 +1,11 @@
|
||||||
|
export interface LocaleEntry {
|
||||||
|
title: string
|
||||||
|
file: string
|
||||||
|
translated: string[]
|
||||||
|
missing: string[]
|
||||||
|
outdated: string[]
|
||||||
|
total: number
|
||||||
|
isSource?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TranslationStatus = Record<string, LocaleEntry>
|
|
@ -23,7 +23,8 @@
|
||||||
"test:typecheck": "stale-dep && vue-tsc --noEmit && vue-tsc --noEmit --project service-worker/tsconfig.json",
|
"test:typecheck": "stale-dep && vue-tsc --noEmit && vue-tsc --noEmit --project service-worker/tsconfig.json",
|
||||||
"test": "nr test:unit",
|
"test": "nr test:unit",
|
||||||
"update:team:avatars": "esno scripts/avatars.ts",
|
"update:team:avatars": "esno scripts/avatars.ts",
|
||||||
"postinstall": "ignore-dependency-scripts \"stale-dep -u && simple-git-hooks && nuxi prepare\"",
|
"prepare-translation-status": "pnpm -C docs run prepare-translation-status",
|
||||||
|
"postinstall": "ignore-dependency-scripts \"stale-dep -u && simple-git-hooks && nuxi prepare && nr prepare-translation-status\"",
|
||||||
"release": "stale-dep && bumpp && esno scripts/release.ts"
|
"release": "stale-dep && bumpp && esno scripts/release.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -228,9 +228,13 @@ importers:
|
||||||
docs:
|
docs:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@nuxt-themes/docus': ^1.6.1
|
'@nuxt-themes/docus': ^1.6.1
|
||||||
|
'@types/flat': ^5.0.2
|
||||||
|
flat: ^5.0.2
|
||||||
nuxt: ^3.1.1
|
nuxt: ^3.1.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@nuxt-themes/docus': 1.6.3_nuxt@3.1.1
|
'@nuxt-themes/docus': 1.6.3_nuxt@3.1.1
|
||||||
|
'@types/flat': 5.0.2
|
||||||
|
flat: 5.0.2
|
||||||
nuxt: 3.1.1
|
nuxt: 3.1.1
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
@ -3328,6 +3332,10 @@ packages:
|
||||||
resolution: {integrity: sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==}
|
resolution: {integrity: sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/flat/5.0.2:
|
||||||
|
resolution: {integrity: sha512-3zsplnP2djeps5P9OyarTxwRpMLoe5Ash8aL9iprw0JxB+FAHjY+ifn4yZUuW4/9hqtnmor6uvjSRzJhiVbrEQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/fnando__sparkline/0.3.4:
|
/@types/fnando__sparkline/0.3.4:
|
||||||
resolution: {integrity: sha512-FWU1zw7CVJYVeDk77FGphTUabfPims4F/Yq+WFB0Gh647lLtiXHWn8vpfT95Fl65IsNBDOhEbxJdhmERMGubNQ==}
|
resolution: {integrity: sha512-FWU1zw7CVJYVeDk77FGphTUabfPims4F/Yq+WFB0Gh647lLtiXHWn8vpfT95Fl65IsNBDOhEbxJdhmERMGubNQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
Loading…
Reference in New Issue