diff --git a/.eslintignore b/.eslintignore index b2365300..0b00eb07 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,3 +9,4 @@ public/ https-dev-config/localhost.crt https-dev-config/localhost.key Dockerfile +docs/translation-status.json diff --git a/config/i18n.ts b/config/i18n.ts index b7907532..944c06f7 100644 --- a/config/i18n.ts +++ b/config/i18n.ts @@ -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 diff --git a/docs/.gitignore b/docs/.gitignore index 69f6b69d..23c55c82 100755 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -10,3 +10,4 @@ dist sw.* .env .output +translation-status.json diff --git a/docs/components/global/ClipboardIcon.vue b/docs/components/global/ClipboardIcon.vue new file mode 100644 index 00000000..edf2696a --- /dev/null +++ b/docs/components/global/ClipboardIcon.vue @@ -0,0 +1,15 @@ + + + diff --git a/docs/components/global/ToogleIcon.vue b/docs/components/global/ToogleIcon.vue new file mode 100644 index 00000000..84b66bf7 --- /dev/null +++ b/docs/components/global/ToogleIcon.vue @@ -0,0 +1,15 @@ + + + diff --git a/docs/components/global/TranslationState.vue b/docs/components/global/TranslationState.vue new file mode 100644 index 00000000..7df72efd --- /dev/null +++ b/docs/components/global/TranslationState.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/docs/content/1.guide/3.contributing.md b/docs/content/1.guide/3.contributing.md index c358d4f9..1fa87808 100644 --- a/docs/content/1.guide/3.contributing.md +++ b/docs/content/1.guide/3.contributing.md @@ -34,6 +34,10 @@ Elk uses [Vitest](https://vitest.dev). You can run the test suite with: nr test ``` +## Translation status + + + # Stack - [Vite](https://vitejs.dev/) - Next Generation Frontend Tooling diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 00000000..6bdb4f73 --- /dev/null +++ b/docs/package-lock.json @@ -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 + } + } +} diff --git a/docs/package.json b/docs/package.json index 66edfbd8..f061e718 100755 --- a/docs/package.json +++ b/docs/package.json @@ -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" } } diff --git a/docs/scripts/prepare-translation-status.ts b/docs/scripts/prepare-translation-status.ts new file mode 100644 index 00000000..5d4c6d58 --- /dev/null +++ b/docs/scripts/prepare-translation-status.ts @@ -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, dst: Record) { + 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 = 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, + file: string | string[], + data: LocaleEntry, +) { + const baseEntriesKeys = Object.keys(baseEntries) + const entries: Record = await readI18nFile(file) + const flatEntriesKeys = Object.keys(flatten>(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 = await readI18nFile(sourceLanguageLocale[1]) + const flatEntries = flatten>(entries) + const data: Record = { + 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 = { 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() diff --git a/docs/types.ts b/docs/types.ts new file mode 100644 index 00000000..c5a98da5 --- /dev/null +++ b/docs/types.ts @@ -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 diff --git a/package.json b/package.json index e54298c5..e313a703 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2cd20754..2707adcc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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