From e7228b07805a40aa67ccb7e3592848218eabbca2 Mon Sep 17 00:00:00 2001 From: PiR1 Date: Fri, 8 May 2026 20:07:41 +0200 Subject: [PATCH] feat(localization): add i18next extraction helper & refactor backup configuration component to tsx --- frontend/i18next.config.ts | 5 +- frontend/package-lock.json | 540 ++++++++++++ frontend/package.json | 2 + frontend/scripts/i18nextExtractionHelper.ts | 251 ++++++ .../src/components/admin/AdminSidebar.tsx | 13 +- .../components/admin/BackupConfiguration.d.ts | 3 - ...figuration.jsx => BackupConfiguration.tsx} | 84 +- .../src/components/admin/BackupDashboard.d.ts | 3 - ...ackupDashboard.jsx => BackupDashboard.tsx} | 102 ++- .../src/components/admin/PhotoFilterPanel.tsx | 2 +- frontend/src/i18n/locales/de.json | 829 ++++++++++-------- frontend/src/i18n/locales/en.json | 125 ++- frontend/src/i18n/locales/fr.json | 127 ++- frontend/src/i18n/locales/nl.json | 127 ++- frontend/src/i18n/locales/pt.json | 127 ++- frontend/src/i18n/locales/ru.json | 127 ++- frontend/src/pages/admin/AdminDashboard.tsx | 13 +- .../src/pages/admin/BackupManagement.d.ts | 3 - ...kupManagement.jsx => BackupManagement.tsx} | 74 +- frontend/src/services/admin.service.ts | 53 +- 20 files changed, 2093 insertions(+), 517 deletions(-) create mode 100644 frontend/scripts/i18nextExtractionHelper.ts delete mode 100644 frontend/src/components/admin/BackupConfiguration.d.ts rename frontend/src/components/admin/{BackupConfiguration.jsx => BackupConfiguration.tsx} (94%) delete mode 100644 frontend/src/components/admin/BackupDashboard.d.ts rename frontend/src/components/admin/{BackupDashboard.jsx => BackupDashboard.tsx} (88%) delete mode 100644 frontend/src/pages/admin/BackupManagement.d.ts rename frontend/src/pages/admin/{BackupManagement.jsx => BackupManagement.tsx} (85%) diff --git a/frontend/i18next.config.ts b/frontend/i18next.config.ts index 38c090be..70080284 100644 --- a/frontend/i18next.config.ts +++ b/frontend/i18next.config.ts @@ -1,4 +1,6 @@ import { defineConfig } from 'i18next-cli'; +import { typescriptPlugin } from "./scripts/i18nextExtractionHelper"; + export default defineConfig({ locales: ['en', 'de', 'nl', 'pt', 'ru', 'fr'], @@ -18,6 +20,7 @@ export default defineConfig({ preserveContextVariants: true, indentation: 2, - sort:false + sort: false, }, + plugins: [typescriptPlugin(["./src/App.tsx"]) ] }); \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 57f7f305..997cbd90 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -53,6 +53,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.6.2", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.5.3", @@ -64,6 +65,7 @@ "globals": "^16.2.0", "i18next-cli": "^1.56.11", "jsdom": "^25.0.1", + "memfs": "^4.57.2", "postcss": "^8.5.10", "tailwindcss": "^3.3.0", "typescript": "~5.8.3", @@ -1618,6 +1620,436 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.2.tgz", + "integrity": "sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.2.tgz", + "integrity": "sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.2.tgz", + "integrity": "sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/fs-print": "4.57.2", + "@jsonjoy.com/fs-snapshot": "4.57.2", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.2.tgz", + "integrity": "sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.2.tgz", + "integrity": "sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.2.tgz", + "integrity": "sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.57.2" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.2.tgz", + "integrity": "sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.57.2", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.2.tgz", + "integrity": "sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3038,6 +3470,16 @@ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -5100,6 +5542,23 @@ "node": ">=10.13.0" } }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/glob/node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -5298,6 +5757,16 @@ "node": ">=18.18.0" } }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, "node_modules/i18next": { "version": "25.7.3", "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz", @@ -6190,6 +6659,36 @@ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "license": "MIT" }, + "node_modules/memfs": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.2.tgz", + "integrity": "sha512-2nWzSsJzrukurSDna4Z0WywuScK4Id3tSKejgu74u8KCdW4uNrseKRSIDg75C6Yw5ZRqBe0F0EtMNlTbUq8bAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-fsa": "4.57.2", + "@jsonjoy.com/fs-node": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-to-fsa": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/fs-print": "4.57.2", + "@jsonjoy.com/fs-snapshot": "4.57.2", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8003,6 +8502,23 @@ "node": ">=0.8" } }, + "node_modules/thingies": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", + "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -8125,6 +8641,23 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -8208,6 +8741,13 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + }, "node_modules/unicorn-magic": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index bcf50c7e..7ffafb96 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -60,6 +60,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.6.2", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.5.3", @@ -71,6 +72,7 @@ "globals": "^16.2.0", "i18next-cli": "^1.56.11", "jsdom": "^25.0.1", + "memfs": "^4.57.2", "postcss": "^8.5.10", "tailwindcss": "^3.3.0", "typescript": "~5.8.3", diff --git a/frontend/scripts/i18nextExtractionHelper.ts b/frontend/scripts/i18nextExtractionHelper.ts new file mode 100644 index 00000000..f3c07c3c --- /dev/null +++ b/frontend/scripts/i18nextExtractionHelper.ts @@ -0,0 +1,251 @@ +import type { ExtractedKey, Plugin } from 'i18next-cli' +import * as ts from 'typescript' +import { vol } from 'memfs' +import * as path from 'path' + +// based on https://github.com/i18next/i18next-cli/blob/49188621ae5534d940c7cc86eca1e3ece99506e7/test/plugin.typescript.test.ts + +// --- HELPER FUNCTIONS FOR PLUGIN --- +function isTranslationFunction (node: ts.CallExpression): boolean { + const expr = node.expression + // Matches t('...') + if (ts.isIdentifier(expr) && expr.text === 't') return true + + // Matches i18n.t('...') + if (ts.isPropertyAccessExpression(expr) && expr.name.text === 't') return true + + return false +} + +function extractStringsFromType (type: ts.Type): string[] { + if (type.isStringLiteral()) { + return [type.value] + } + if (type.isUnion()) { + return type.types.flatMap(t => extractStringsFromType(t)) + } + if (type.isIntersection()) { + return type.types.flatMap(t => extractStringsFromType(t)) + } + return [] +} + +export function typescriptPlugin ( + entryPoints: string[], + options: { defaultNS?: string } = {} +): Plugin { + const defaultNS = options.defaultNS + + return { + name: 'typescript-resolver', + async onEnd (keys: Map) { + // 1. Setup Compiler Options + const compilerOptions: ts.CompilerOptions = { + allowJs: true, + jsx: ts.JsxEmit.ReactJSX, + target: ts.ScriptTarget.ESNext, + module: ts.ModuleKind.CommonJS, + moduleResolution: ts.ModuleResolutionKind.NodeJs, + esModuleInterop: true + } + + // 2. Create a custom CompilerHost that reads from memfs (vol) + const host = ts.createCompilerHost(compilerOptions) + const originalReadFile = host.readFile + const originalFileExists = host.fileExists + + host.readFile = (fileName: string) => { + if (vol.existsSync(fileName)) { + return vol.readFileSync(fileName, 'utf-8') as string + } + return originalReadFile(fileName) + } + + host.fileExists = (fileName: string) => { + if (vol.existsSync(fileName)) return true + return originalFileExists(fileName) + } + + // Override module resolution to look in memfs + host.resolveModuleNameLiterals = (moduleLiterals, containingFile, redirectedReference, options, containingSourceFile, reusedNames) => { + return moduleLiterals.map(moduleLiteral => { + const moduleName = moduleLiteral.text + // Simple resolution for test: resolve relative paths against the containing file's directory + if (moduleName.startsWith('.')) { + const dir = path.dirname(containingFile) + const candidates: [string, ts.Extension][] = [ + [path.join(dir, moduleName + '.tsx'), ts.Extension.Tsx], + [path.join(dir, moduleName + '.ts'), ts.Extension.Ts], + ] + for (const [resolvedPath, ext] of candidates) { + if (vol.existsSync(resolvedPath)) { + return { + resolvedModule: { + resolvedFileName: resolvedPath, + extension: ext, + isExternalLibraryImport: false + } + } + } + } + } + // Fallback to standard resolution (for node_modules etc) + return ts.resolveModuleName(moduleName, containingFile, options, host) + }) + } + + // 3. Create Program + const program = ts.createProgram(entryPoints, compilerOptions, host) + const checker = program.getTypeChecker() + + // 4. Visit AST + for (const sourceFile of program.getSourceFiles()) { + if (sourceFile.isDeclarationFile) continue + ts.forEachChild(sourceFile, node => { visit(node, undefined) }) + } + + // Extract namespace from useTranslation call + function getUseTranslationNamespace (node: ts.CallExpression): string | undefined { + const expr = node.expression + if (ts.isIdentifier(expr) && expr.text === 'useTranslation') { + const arg = node.arguments[0] + if (arg && ts.isStringLiteral(arg)) { + return arg.text + } + // Handle array of namespaces: useTranslation(['ns1', 'ns2']) - use the first one + if (arg && ts.isArrayLiteralExpression(arg)) { + const firstElement = arg.elements[0] + if (firstElement && ts.isStringLiteral(firstElement)) { + return firstElement.text + } + } + } + return undefined + } + + function visit (node: ts.Node, scopeNs: string | undefined) { + // Track useTranslation namespace in current scope + let currentScopeNs = scopeNs + + // Check if this is a function with useTranslation + if (ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isArrowFunction(node) || + ts.isMethodDeclaration(node)) { + const body = 'body' in node ? node.body : undefined + if (body) { + // Look for useTranslation in this function + const searchForUseTranslation = (n: ts.Node): string | undefined => { + if (ts.isVariableDeclaration(n) && n.initializer && + ts.isCallExpression(n.initializer)) { + const ns = getUseTranslationNamespace(n.initializer) + if (ns) return ns + } + if (ts.isCallExpression(n)) { + const ns = getUseTranslationNamespace(n) + if (ns) return ns + } + let result: string | undefined + ts.forEachChild(n, child => { + if (!result) result = searchForUseTranslation(child) + }) + return result + } + currentScopeNs = searchForUseTranslation(body) ?? scopeNs + } + } + + if (ts.isCallExpression(node) && isTranslationFunction(node)) { + const arg = node.arguments[0] + if (arg) { + if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) { + ts.forEachChild(node, child => { visit(child, currentScopeNs) }) + return + } + + let values: string[] = [] + + // Handle function arguments (e.g. t(() => ...)) by checking return type + if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) { + const signature = checker.getSignatureFromDeclaration(arg) + if (signature) { + const returnType = signature.getReturnType() + values = extractStringsFromType(returnType) + } + } else { + // Try standard type resolution first + const type = checker.getTypeAtLocation(arg) + values = extractStringsFromType(type) + } + + // Fallback: If type resolution failed (generic string) but it's a TemplateExpression, + // try to manually resolve the parts. This helps in test environments where full type inference is flaky. + if (values.length === 0 && ts.isTemplateExpression(arg)) { + const head = arg.head.text + // Only handle simple case: `prefix.${var}` + if (arg.templateSpans.length === 1) { + const span = arg.templateSpans[0] + const spanType = checker.getTypeAtLocation(span.expression) + const spanValues = extractStringsFromType(spanType) + + if (spanValues.length > 0) { + values = spanValues.map(v => head + v + span.literal.text) + } + } + } + + // Extract namespace from options (second argument) + let optionsNs: string | undefined + const optionsArg = node.arguments[1] + if (optionsArg && ts.isObjectLiteralExpression(optionsArg)) { + const nsProp = optionsArg.properties.find( + p => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'ns' + ) + if (nsProp && ts.isPropertyAssignment(nsProp)) { + if (ts.isStringLiteral(nsProp.initializer)) { + optionsNs = nsProp.initializer.text + } + } + } + + values.forEach(val => { + let key = val + // Priority: 1. options ns, 2. key contains ns, 3. useTranslation ns, 4. default 'all' + let ns = optionsNs + + // Check if key contains namespace separator (e.g., "namespace:key") + const separatorIndex = val.indexOf(':') + if (separatorIndex > 0) { + ns = val.substring(0, separatorIndex) + key = val.substring(separatorIndex + 1) + } + + let uniqueKey = key + + // Fallback to useTranslation namespace or default + if (!ns) { + if(currentScopeNs) { + ns = currentScopeNs + } else if (defaultNS) { + ns = defaultNS + } + } + if (ns) { + uniqueKey = `${ns}:${key}` + } + + if (!keys.has(uniqueKey)) { + keys.set(uniqueKey, { + key, + defaultValue: key, + nsIsImplicit: !optionsNs && !val.includes(':') && !currentScopeNs, + }) + } + }) + } + } + ts.forEachChild(node, child => { visit(child, currentScopeNs) }) + } + } + } +} diff --git a/frontend/src/components/admin/AdminSidebar.tsx b/frontend/src/components/admin/AdminSidebar.tsx index 03e88795..441a84d4 100644 --- a/frontend/src/components/admin/AdminSidebar.tsx +++ b/frontend/src/components/admin/AdminSidebar.tsx @@ -25,15 +25,8 @@ interface AdminSidebarProps { onClose: () => void; } -interface NavItem { - nameKey: string; - href: string; - icon: React.ComponentType<{ className?: string }>; - permission?: string; -} - -const navigation: NavItem[] = [ - { nameKey: 'navigation.dashboard', href: '/admin/dashboard', icon: LayoutDashboard }, +const navigation = [ + { nameKey: 'navigation.dashboard', href: '/admin/dashboard', icon: LayoutDashboard, permission: false }, { nameKey: 'navigation.events', href: '/admin/events', icon: Calendar, permission: 'events.view' }, { nameKey: 'navigation.archives', href: '/admin/archives', icon: Archive, permission: 'archives.view' }, { nameKey: 'admin.analytics', href: '/admin/analytics', icon: BarChart3, permission: 'analytics.view' }, @@ -44,7 +37,7 @@ const navigation: NavItem[] = [ { nameKey: 'navigation.backup', href: '/admin/backup', icon: HardDrive, permission: 'backup.view' }, { nameKey: 'navigation.cmsPages', href: '/admin/cms', icon: FileText, permission: 'cms.view' }, { nameKey: 'navigation.users', href: '/admin/users', icon: Users, permission: 'users.view' }, -]; +] as const; export const AdminSidebar: React.FC = ({ isOpen, onClose }) => { const location = useLocation(); diff --git a/frontend/src/components/admin/BackupConfiguration.d.ts b/frontend/src/components/admin/BackupConfiguration.d.ts deleted file mode 100644 index 78d4eca5..00000000 --- a/frontend/src/components/admin/BackupConfiguration.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { ComponentType } from 'react'; - -export const BackupConfiguration: ComponentType; diff --git a/frontend/src/components/admin/BackupConfiguration.jsx b/frontend/src/components/admin/BackupConfiguration.tsx similarity index 94% rename from frontend/src/components/admin/BackupConfiguration.jsx rename to frontend/src/components/admin/BackupConfiguration.tsx index 050ffad7..19890291 100644 --- a/frontend/src/components/admin/BackupConfiguration.jsx +++ b/frontend/src/components/admin/BackupConfiguration.tsx @@ -5,18 +5,11 @@ import { Server, Cloud, HardDrive, - Clock, - Calendar, - Shield, AlertCircle, - Info, Eye, EyeOff, Wifi, - CheckCircle, - XCircle, Loader2, - FolderOpen, Database, Image, FileArchive @@ -24,26 +17,58 @@ import { import { toast } from 'react-toastify'; import { Button, Card, Input } from '../common'; -export const BackupConfiguration = ({ config, onSave, isSaving }) => { +interface BackupFormData { + backup_enabled: boolean; + backup_destination_type: 'local' | 'rsync' | 's3'; + backup_destination_path: string; + backup_rsync_host: string; + backup_rsync_user: string; + backup_rsync_path: string; + backup_rsync_ssh_key: string; + backup_s3_endpoint: string; + backup_s3_bucket: string; + backup_s3_access_key: string; + backup_s3_secret_key: string; + backup_s3_region: string; + backup_schedule: string; + backup_schedule_cron: string; + backup_retention_days: number; + backup_include_database: boolean; + backup_include_photos: boolean; + backup_include_archives: boolean; + backup_include_thumbnails: boolean; + backup_include_temp: boolean; + backup_compression: boolean; + backup_encryption: boolean; + backup_encryption_passphrase: string; +} + +interface BackupConfigurationProps { + config?: Partial; + onSave: (data: BackupFormData) => void; + isSaving: boolean; +} + +export const BackupConfiguration: React.FC = ({ config, onSave, isSaving }) => { const { t } = useTranslation(); - + const destinationTypes = [ { - id: 'local', + id: 'local' as const, name: t('backup.configuration.destinationTypes.local.name'), icon: HardDrive, description: t('backup.configuration.destinationTypes.local.description'), fields: ['backup_destination_path'] }, { - id: 'rsync', + id: 'rsync' as const, name: t('backup.configuration.destinationTypes.rsync.name'), icon: Server, description: t('backup.configuration.destinationTypes.rsync.description'), fields: ['backup_rsync_host', 'backup_rsync_user', 'backup_rsync_path', 'backup_rsync_ssh_key'] }, { - id: 's3', + id: 's3' as const, name: t('backup.configuration.destinationTypes.s3.name'), icon: Cloud, description: t('backup.configuration.destinationTypes.s3.description'), @@ -57,8 +82,8 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => { { value: 'weekly', label: t('backup.configuration.schedule.options.weekly') }, { value: 'custom', label: t('backup.configuration.schedule.options.custom') } ]; - - const [formData, setFormData] = useState({ + + const [formData, setFormData] = useState({ backup_enabled: false, backup_destination_type: 'local', backup_destination_path: '', @@ -101,33 +126,32 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => { } }, [config]); - const handleChange = (field, value) => { + const handleChange = (field: K, value: BackupFormData[K]) => { setFormData(prev => ({ ...prev, [field]: value })); }; - const handleSubmit = (e) => { + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - - // Validate required fields - const destinationType = destinationTypes.find(t => t.id === formData.backup_destination_type); - const missingFields = []; - + + const destinationType = destinationTypes.find(dt => dt.id === formData.backup_destination_type); + const missingFields: string[] = []; + if (formData.backup_enabled && destinationType) { destinationType.fields.forEach(field => { - if (!formData[field] && !field.includes('optional')) { + if (!formData[field as keyof BackupFormData] && !field.includes('optional')) { missingFields.push(field); } }); } - + if (missingFields.length > 0) { toast.error(t('backup.configuration.messages.requiredFields')); return; } - + onSave(formData); }; @@ -138,14 +162,12 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => { await new Promise(resolve => setTimeout(resolve, 2000)); toast.success(t('backup.configuration.messages.connectionSuccess')); } catch (error) { - toast.error(t('backup.configuration.messages.connectionFailed') + ': ' + error.message); + toast.error(t('backup.configuration.messages.connectionFailed') + ': ' + (error as Error).message); } finally { setTestingConnection(false); } }; - const selectedDestination = destinationTypes.find(t => t.id === formData.backup_destination_type); - return (
{/* Enable/Disable Toggle */} @@ -399,7 +421,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => { {/* Schedule Configuration */}

{t('backup.configuration.schedule.title')}

- +