feat(docs): add translation status (#1689)
Co-authored-by: Michel EDIGHOFFER <edimitchel@gmail.com>
This commit is contained in:
parent
0eefcfa281
commit
972a13499f
|
@ -9,3 +9,4 @@ public/
|
|||
https-dev-config/localhost.crt
|
||||
https-dev-config/localhost.key
|
||||
Dockerfile
|
||||
docs/translation-status.json
|
||||
|
|
|
@ -198,7 +198,7 @@ const buildLocales = () => {
|
|||
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 = data.dateTimeFormats
|
||||
|
|
1
docs/.gitignore
vendored
1
docs/.gitignore
vendored
|
@ -10,3 +10,4 @@ dist
|
|||
sw.*
|
||||
.env
|
||||
.output
|
||||
translation-status.json
|
||||
|
|
15
docs/components/global/ClipboardIcon.vue
Normal file
15
docs/components/global/ClipboardIcon.vue
Normal file
|
@ -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>
|
15
docs/components/global/ToogleIcon.vue
Normal file
15
docs/components/global/ToogleIcon.vue
Normal file
|
@ -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>
|
338
docs/components/global/TranslationState.vue
Normal file
338
docs/components/global/TranslationState.vue
Normal file
|
@ -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
|
||||
```
|
||||
|
||||
## Translation status
|
||||
|
||||
<TranslationState />
|
||||
|
||||
# Stack
|
||||
|
||||
- [Vite](https://vitejs.dev/) - Next Generation Frontend Tooling
|
||||
|
|
200
docs/package-lock.json
generated
Normal file
200
docs/package-lock.json
generated
Normal file
|
@ -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",
|
||||
"build": "nuxi build",
|
||||
"generate": "nuxi generate",
|
||||
"preview": "nuxi preview"
|
||||
"preview": "nuxi preview",
|
||||
"prepare-translation-status": "nuxi prepare && esno scripts/prepare-translation-status.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt-themes/docus": "^1.6.1",
|
||||
"@types/flat": "^5.0.2",
|
||||
"flat": "^5.0.2",
|
||||
"nuxt": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
|
105
docs/scripts/prepare-translation-status.ts
Normal file
105
docs/scripts/prepare-translation-status.ts
Normal file
|
@ -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()
|
11
docs/types.ts
Normal file
11
docs/types.ts
Normal file
|
@ -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": "nr test:unit",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
@ -228,9 +228,13 @@ importers:
|
|||
docs:
|
||||
specifiers:
|
||||
'@nuxt-themes/docus': ^1.6.1
|
||||
'@types/flat': ^5.0.2
|
||||
flat: ^5.0.2
|
||||
nuxt: ^3.1.1
|
||||
devDependencies:
|
||||
'@nuxt-themes/docus': 1.6.3_nuxt@3.1.1
|
||||
'@types/flat': 5.0.2
|
||||
flat: 5.0.2
|
||||
nuxt: 3.1.1
|
||||
|
||||
packages:
|
||||
|
@ -3328,6 +3332,10 @@ packages:
|
|||
resolution: {integrity: sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==}
|
||||
dev: true
|
||||
|
||||
/@types/flat/5.0.2:
|
||||
resolution: {integrity: sha512-3zsplnP2djeps5P9OyarTxwRpMLoe5Ash8aL9iprw0JxB+FAHjY+ifn4yZUuW4/9hqtnmor6uvjSRzJhiVbrEQ==}
|
||||
dev: true
|
||||
|
||||
/@types/fnando__sparkline/0.3.4:
|
||||
resolution: {integrity: sha512-FWU1zw7CVJYVeDk77FGphTUabfPims4F/Yq+WFB0Gh647lLtiXHWn8vpfT95Fl65IsNBDOhEbxJdhmERMGubNQ==}
|
||||
dev: true
|
||||
|
|
Loading…
Reference in a new issue