diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 3f650167a..a42404ea2 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -10,6 +10,7 @@ "./svelte-app/node_modules/.bin/eslint --fix" ], "svelte-app/src/languages/*.json": [ - "deno run --no-npm --allow-read --allow-write ./svelte-app/scripts/sort-json.ts" + "bun ./svelte-app/scripts/lint-json.ts", + "bun ./svelte-app/scripts/generate-types.ts" ] } diff --git a/svelte-app/package.json b/svelte-app/package.json index d3346c8cc..150c516a4 100644 --- a/svelte-app/package.json +++ b/svelte-app/package.json @@ -9,6 +9,7 @@ "dev:backed": "vite dev --mode backed --host", "package": "svelte-kit package", "preview": "vite preview", + "generate": "bun ./scripts/generate-types && pnpm eslint --fix \"./types/**/*.ts\"", "test": "test:vitest; test:playwright", "test:vitest": "MODE=testing vitest --run", "test:playwright": "playwright test", @@ -16,8 +17,8 @@ "check": "svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write ./src/**/*.{ts,svelte}", - "format:json": "deno run --no-npm --allow-read --allow-write ./scripts/sort-json.ts ./src/languages/en.json ./src/languages/fr.json --fix", - "lint": "pnpm format && pnpm eslint --fix \"./src/**/*.{ts,svelte}\"" + "format:json": "bun ./scripts/lint-json.ts ./src/languages/en.json ./src/languages/fr.json", + "lint": "pnpm format && pnpm eslint --fix \"./src/**/*.{ts,svelte}\" \"./types/**/*.ts\"" }, "devDependencies": { "@playwright/test": "1.41.2", diff --git a/svelte-app/scripts/check-translations.ts b/svelte-app/scripts/check-translations.ts deleted file mode 100644 index fae789dd2..000000000 --- a/svelte-app/scripts/check-translations.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Short Deno script to check for unused translation strings. - -const translationDir = './src/languages'; -const sourceDir = './src'; - -const getFiles = async (dir: string, exts = ['ts', 'svelte']) => { - const files: string[] = []; - for await (const dirEntry of Deno.readDir(dir)) { - const entryPath = `${dir}/${dirEntry.name}`; - if (dirEntry.isDirectory) { - files.push(...(await getFiles(entryPath))); - } - if ( - dirEntry.isFile && - entryPath.match(new RegExp(`\\.(${exts.join('|')})$`)) - ) { - files.push(entryPath); - } - } - return files; -}; - -const getTranslationKeys = async (dir: string) => { - const translationKeys: Set = new Set(); - const translationFiles = await getFiles(dir, ['json']); - translationFiles.forEach((file) => { - const contents = JSON.parse(Deno.readTextFileSync(file)); - Object.keys(contents).forEach((key) => translationKeys.add(key)); - }); - return translationKeys; -}; - -const sourceFiles = await getFiles(sourceDir); -const translationKeys = await getTranslationKeys(translationDir); - -const unusedTranslationKeys: string[] = []; - -translationKeys.forEach((key) => { - const isUsed = sourceFiles.some((file) => { - const fileContents = Deno.readTextFileSync(file); - return fileContents.includes(key); - }); - if (!isUsed) { - unusedTranslationKeys.push(key); - } -}); - -if (unusedTranslationKeys.length) { - console.error('Unused translation keys:', unusedTranslationKeys); - Deno.exit(1); -} diff --git a/svelte-app/scripts/generate-types.ts b/svelte-app/scripts/generate-types.ts new file mode 100644 index 000000000..e72095894 --- /dev/null +++ b/svelte-app/scripts/generate-types.ts @@ -0,0 +1,45 @@ +import fs from 'fs'; +import path from 'path'; + +const sourceDir = path.join(__dirname, '../src'); +const translationDir = path.join(sourceDir, 'languages'); +const typesDir = path.join(__dirname, '../types/generated'); + +const readLocaleFile = (locale: string) => { + const filePath = path.join(translationDir, `${locale}.json`); + const fileContent = fs.readFileSync(filePath, 'utf-8'); + return JSON.parse(fileContent); +}; + +const extractKeys = (obj: object, prefix = ''): string[] => + Object.entries(obj).flatMap(([key, value]) => { + const newKey = prefix ? `${prefix}.${key}` : key; + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + return extractKeys(value, newKey); + } + return newKey; + }); + +const generateTypes = (keys: string[]) => { + const unionType = keys + .map((key) => (key.includes("'") ? `"${key}"` : `'${key}'`)) + .join(' | '); + + return `export type LocaleKey = ${unionType};`; +}; + +const compareLocales = (locale1: object, locale2: object) => { + const keys1 = extractKeys(locale1); + const keys2 = extractKeys(locale2); + return keys1.filter((key) => keys2.includes(key)); +}; + +const enLocale = readLocaleFile('en'); +const frLocale = readLocaleFile('fr'); + +const commonKeys = compareLocales(enLocale, frLocale); +const typesContent = generateTypes(commonKeys); + +fs.writeFileSync(path.join(typesDir, 'index.ts'), typesContent); + +console.log('Generated types for locales: ./types/generated/index.ts'); diff --git a/svelte-app/scripts/lint-json.ts b/svelte-app/scripts/lint-json.ts new file mode 100644 index 000000000..ff16981b4 --- /dev/null +++ b/svelte-app/scripts/lint-json.ts @@ -0,0 +1,34 @@ +import { readFileSync, writeFileSync } from 'fs'; + +const args = Bun.argv.slice(2); +const filePaths: string[] = args; + +if (!filePaths.length) { + throw new Error('[sort-json] No file path(s) provided'); +} else if (filePaths.some((p) => !p.match(/\.json$/i))) { + throw new Error('[sort-json] File(s) must be JSON'); +} + +const sortObjectKeys = (obj: Record) => { + const sortedKeys = Object.keys(obj).sort(); + + return sortedKeys.reduce((acc, key) => { + if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { + acc[key] = sortObjectKeys(obj[key] as Record); + } + acc[key] = obj[key]; + return acc; + }, {}); +}; + +filePaths.forEach((filePath) => { + if (!filePath.match(/languages\/[a-z]{2}\.json/i)) { + return; + } + + const contents = JSON.parse(readFileSync(filePath, 'utf-8')); + const sortedContents = sortObjectKeys(contents); + + writeFileSync(filePath, JSON.stringify(sortedContents, null, 2), 'utf-8'); + console.log(`[sort-json] Sorted '${filePath}'`); +}); diff --git a/svelte-app/scripts/sort-json.ts b/svelte-app/scripts/sort-json.ts deleted file mode 100644 index 74622bd34..000000000 --- a/svelte-app/scripts/sort-json.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-nocheck - -/** - * Run with Deno: deno run --allow-read --allow-write lint-json.ts - */ - -import { parse } from 'https://deno.land/std@0.174.0/flags/mod.ts'; - -const { fix, _: filePaths } = parse(Deno.args); - -if (!filePaths?.length) { - throw new Error('[sort-json] No file path(s) provided'); -} else if (filePaths.some((p: string) => !p.match(/\.json$/gi))) { - throw new Error('[sort-json] File(s) must be JSON'); -} - -const shouldFix = fix, - errors = [] as string[]; - -filePaths.forEach((filePath: string) => { - if (!filePath.match(/languages\/[a-z]{2}\.json/gi)) { - return; - } - - const contents = JSON.parse(Deno.readTextFileSync(filePath)), - sortedKeys = Object.keys(contents).sort(); - - if (shouldFix) { - const sortedContents = sortedKeys.reduce((acc, key) => { - acc[key] = contents[key]; - return acc; - }, {} as Record); - - Deno.writeTextFileSync(filePath, JSON.stringify(sortedContents, null, 2)); - console.log(`[sort-json] Sorted '${filePath}'`); - } else { - const keys = Object.keys(contents); - for (let i = 0; i < sortedKeys.length; i++) { - sortedKeys[i] !== keys[i] && - errors.push(`- '${filePath}': Key is unsorted: '${keys[i]}'`); - } - - if (errors.length) { - console.error(`\n[sort-json] Format errors:\n\n${errors.join('\n')}`); - } else { - console.log('[sort-json] All files are formatted'); - } - } -}); diff --git a/svelte-app/src/components/about/timeline-section.svelte b/svelte-app/src/components/about/timeline-section.svelte index 19fe9e668..bdffa5d9b 100644 --- a/svelte-app/src/components/about/timeline-section.svelte +++ b/svelte-app/src/components/about/timeline-section.svelte @@ -1,5 +1,6 @@ -
+
-

{title}

-

+

{title}

+

{$displayRange(section[section.length - 1].range.start, section[0].range.end)} • {$displayMonthDuration( section[section.length - 1].range.start, diff --git a/svelte-app/src/components/document/content/footer.svelte b/svelte-app/src/components/document/content/footer.svelte index 7c190e26a..80fe9200f 100644 --- a/svelte-app/src/components/document/content/footer.svelte +++ b/svelte-app/src/components/document/content/footer.svelte @@ -27,7 +27,7 @@ {/if}