feat(localization): add i18next extraction helper & refactor backup configuration component to tsx

This commit is contained in:
PiR1
2026-05-09 18:23:12 +02:00
parent 86ee6c80aa
commit e7228b0780
20 changed files with 2093 additions and 517 deletions
+4 -1
View File
@@ -1,4 +1,6 @@
import { defineConfig } from 'i18next-cli'; import { defineConfig } from 'i18next-cli';
import { typescriptPlugin } from "./scripts/i18nextExtractionHelper";
export default defineConfig({ export default defineConfig({
locales: ['en', 'de', 'nl', 'pt', 'ru', 'fr'], locales: ['en', 'de', 'nl', 'pt', 'ru', 'fr'],
@@ -18,6 +20,7 @@ export default defineConfig({
preserveContextVariants: true, preserveContextVariants: true,
indentation: 2, indentation: 2,
sort:false sort: false,
}, },
plugins: [typescriptPlugin(["./src/App.tsx"]) ]
}); });
+540
View File
@@ -53,6 +53,7 @@
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0", "@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/node": "^25.6.2",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.5.3", "@vitejs/plugin-react": "^4.5.3",
@@ -64,6 +65,7 @@
"globals": "^16.2.0", "globals": "^16.2.0",
"i18next-cli": "^1.56.11", "i18next-cli": "^1.56.11",
"jsdom": "^25.0.1", "jsdom": "^25.0.1",
"memfs": "^4.57.2",
"postcss": "^8.5.10", "postcss": "^8.5.10",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",
@@ -1618,6 +1620,436 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3038,6 +3470,16 @@
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"license": "MIT" "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": { "node_modules/@types/prop-types": {
"version": "15.7.15", "version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -5100,6 +5542,23 @@
"node": ">=10.13.0" "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": { "node_modules/glob/node_modules/balanced-match": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -5298,6 +5757,16 @@
"node": ">=18.18.0" "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": { "node_modules/i18next": {
"version": "25.7.3", "version": "25.7.3",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz", "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz",
@@ -6190,6 +6659,36 @@
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT" "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": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -8003,6 +8502,23 @@
"node": ">=0.8" "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": { "node_modules/tinybench": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -8125,6 +8641,23 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT" "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": { "node_modules/ts-api-utils": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "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==", "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT" "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": { "node_modules/unicorn-magic": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+2
View File
@@ -60,6 +60,7 @@
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0", "@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/node": "^25.6.2",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.5.3", "@vitejs/plugin-react": "^4.5.3",
@@ -71,6 +72,7 @@
"globals": "^16.2.0", "globals": "^16.2.0",
"i18next-cli": "^1.56.11", "i18next-cli": "^1.56.11",
"jsdom": "^25.0.1", "jsdom": "^25.0.1",
"memfs": "^4.57.2",
"postcss": "^8.5.10", "postcss": "^8.5.10",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",
+251
View File
@@ -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<string, ExtractedKey>) {
// 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) })
}
}
}
}
+3 -10
View File
@@ -25,15 +25,8 @@ interface AdminSidebarProps {
onClose: () => void; onClose: () => void;
} }
interface NavItem { const navigation = [
nameKey: string; { nameKey: 'navigation.dashboard', href: '/admin/dashboard', icon: LayoutDashboard, permission: false },
href: string;
icon: React.ComponentType<{ className?: string }>;
permission?: string;
}
const navigation: NavItem[] = [
{ nameKey: 'navigation.dashboard', href: '/admin/dashboard', icon: LayoutDashboard },
{ nameKey: 'navigation.events', href: '/admin/events', icon: Calendar, permission: 'events.view' }, { nameKey: 'navigation.events', href: '/admin/events', icon: Calendar, permission: 'events.view' },
{ nameKey: 'navigation.archives', href: '/admin/archives', icon: Archive, permission: 'archives.view' }, { nameKey: 'navigation.archives', href: '/admin/archives', icon: Archive, permission: 'archives.view' },
{ nameKey: 'admin.analytics', href: '/admin/analytics', icon: BarChart3, permission: 'analytics.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.backup', href: '/admin/backup', icon: HardDrive, permission: 'backup.view' },
{ nameKey: 'navigation.cmsPages', href: '/admin/cms', icon: FileText, permission: 'cms.view' }, { nameKey: 'navigation.cmsPages', href: '/admin/cms', icon: FileText, permission: 'cms.view' },
{ nameKey: 'navigation.users', href: '/admin/users', icon: Users, permission: 'users.view' }, { nameKey: 'navigation.users', href: '/admin/users', icon: Users, permission: 'users.view' },
]; ] as const;
export const AdminSidebar: React.FC<AdminSidebarProps> = ({ isOpen, onClose }) => { export const AdminSidebar: React.FC<AdminSidebarProps> = ({ isOpen, onClose }) => {
const location = useLocation(); const location = useLocation();
@@ -1,3 +0,0 @@
import type { ComponentType } from 'react';
export const BackupConfiguration: ComponentType<any>;
@@ -5,18 +5,11 @@ import {
Server, Server,
Cloud, Cloud,
HardDrive, HardDrive,
Clock,
Calendar,
Shield,
AlertCircle, AlertCircle,
Info,
Eye, Eye,
EyeOff, EyeOff,
Wifi, Wifi,
CheckCircle,
XCircle,
Loader2, Loader2,
FolderOpen,
Database, Database,
Image, Image,
FileArchive FileArchive
@@ -24,26 +17,58 @@ import {
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { Button, Card, Input } from '../common'; 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<BackupFormData>;
onSave: (data: BackupFormData) => void;
isSaving: boolean;
}
export const BackupConfiguration: React.FC<BackupConfigurationProps> = ({ config, onSave, isSaving }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const destinationTypes = [ const destinationTypes = [
{ {
id: 'local', id: 'local' as const,
name: t('backup.configuration.destinationTypes.local.name'), name: t('backup.configuration.destinationTypes.local.name'),
icon: HardDrive, icon: HardDrive,
description: t('backup.configuration.destinationTypes.local.description'), description: t('backup.configuration.destinationTypes.local.description'),
fields: ['backup_destination_path'] fields: ['backup_destination_path']
}, },
{ {
id: 'rsync', id: 'rsync' as const,
name: t('backup.configuration.destinationTypes.rsync.name'), name: t('backup.configuration.destinationTypes.rsync.name'),
icon: Server, icon: Server,
description: t('backup.configuration.destinationTypes.rsync.description'), description: t('backup.configuration.destinationTypes.rsync.description'),
fields: ['backup_rsync_host', 'backup_rsync_user', 'backup_rsync_path', 'backup_rsync_ssh_key'] 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'), name: t('backup.configuration.destinationTypes.s3.name'),
icon: Cloud, icon: Cloud,
description: t('backup.configuration.destinationTypes.s3.description'), description: t('backup.configuration.destinationTypes.s3.description'),
@@ -58,7 +83,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{ value: 'custom', label: t('backup.configuration.schedule.options.custom') } { value: 'custom', label: t('backup.configuration.schedule.options.custom') }
]; ];
const [formData, setFormData] = useState({ const [formData, setFormData] = useState<BackupFormData>({
backup_enabled: false, backup_enabled: false,
backup_destination_type: 'local', backup_destination_type: 'local',
backup_destination_path: '', backup_destination_path: '',
@@ -101,23 +126,22 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
} }
}, [config]); }, [config]);
const handleChange = (field, value) => { const handleChange = <K extends keyof BackupFormData>(field: K, value: BackupFormData[K]) => {
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
[field]: value [field]: value
})); }));
}; };
const handleSubmit = (e) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
// Validate required fields const destinationType = destinationTypes.find(dt => dt.id === formData.backup_destination_type);
const destinationType = destinationTypes.find(t => t.id === formData.backup_destination_type); const missingFields: string[] = [];
const missingFields = [];
if (formData.backup_enabled && destinationType) { if (formData.backup_enabled && destinationType) {
destinationType.fields.forEach(field => { destinationType.fields.forEach(field => {
if (!formData[field] && !field.includes('optional')) { if (!formData[field as keyof BackupFormData] && !field.includes('optional')) {
missingFields.push(field); missingFields.push(field);
} }
}); });
@@ -138,14 +162,12 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 2000));
toast.success(t('backup.configuration.messages.connectionSuccess')); toast.success(t('backup.configuration.messages.connectionSuccess'));
} catch (error) { } catch (error) {
toast.error(t('backup.configuration.messages.connectionFailed') + ': ' + error.message); toast.error(t('backup.configuration.messages.connectionFailed') + ': ' + (error as Error).message);
} finally { } finally {
setTestingConnection(false); setTestingConnection(false);
} }
}; };
const selectedDestination = destinationTypes.find(t => t.id === formData.backup_destination_type);
return ( return (
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{/* Enable/Disable Toggle */} {/* Enable/Disable Toggle */}
-3
View File
@@ -1,3 +0,0 @@
import type { ComponentType } from 'react';
export const BackupDashboard: ComponentType<any>;
@@ -8,11 +8,9 @@ import {
Clock, Clock,
CheckCircle, CheckCircle,
AlertCircle, AlertCircle,
TrendingUp,
Shield, Shield,
Server, Server,
Cloud, Cloud,
Calendar,
Play, Play,
Loader2, Loader2,
AlertTriangle, AlertTriangle,
@@ -21,7 +19,59 @@ import {
import { format, formatDistanceToNow } from 'date-fns'; import { format, formatDistanceToNow } from 'date-fns';
import { Card, Button } from '../common'; import { Card, Button } from '../common';
const StatCard = ({ icon: Icon, label, value, color = 'blue', subtext }) => ( export type HealthStatus = 'excellent' | 'good' | 'warning' | 'critical';
export type BackupDestinationType = 's3' | 'rsync' | 'local';
interface BackupStatistics {
total_size?: number;
files_processed?: number;
database_backed_up?: boolean;
photos_backed_up?: number;
total_photos?: number;
archives_backed_up?: number;
photo_count?: number;
}
interface BackupRecord {
id: number;
status: 'completed' | 'failed' | 'running';
backup_type: string;
created_at: string;
duration_seconds: number;
statistics?: BackupStatistics;
}
interface BackupStatus {
lastBackup?: BackupRecord;
totalBackups?: number;
recentBackups?: BackupRecord[];
}
interface BackupConfig {
backup_destination_type?: BackupDestinationType;
backup_enabled?: boolean;
backup_s3_bucket?: string;
backup_destination_path?: string;
backup_rsync_host?: string;
backup_retention_days?: number;
}
interface StatCardProps {
icon: React.ComponentType<{ className?: string }>;
label: string;
value: string | number;
color?: string;
subtext?: string;
}
interface BackupDashboardProps {
status?: BackupStatus;
config?: BackupConfig;
onRunBackup: () => void;
isBackupRunning: boolean;
}
const StatCard: React.FC<StatCardProps> = ({ icon: Icon, label, value, color = 'blue', subtext }) => (
<Card className="p-6"> <Card className="p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex-1"> <div className="flex-1">
@@ -38,7 +88,7 @@ const StatCard = ({ icon: Icon, label, value, color = 'blue', subtext }) => (
</Card> </Card>
); );
const formatBytes = (bytes) => { const formatBytes = (bytes: number): string => {
if (!bytes) return '0 B'; if (!bytes) return '0 B';
const k = 1024; const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
@@ -46,18 +96,24 @@ const formatBytes = (bytes) => {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
}; };
export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }) => { const healthColors: Record<HealthStatus, string> = {
excellent: 'green',
good: 'blue',
warning: 'amber',
critical: 'red',
};
export const BackupDashboard: React.FC<BackupDashboardProps> = ({ status, config, onRunBackup, isBackupRunning }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const lastBackup = status?.lastBackup; const lastBackup = status?.lastBackup;
const statistics = lastBackup?.statistics || {}; const statistics = lastBackup?.statistics ?? {};
const isConfigured = config && config.backup_destination_type; const isConfigured = config && config.backup_destination_type;
const isEnabled = config?.backup_enabled; const isEnabled = config?.backup_enabled;
// Calculate backup health score const getHealthScore = (): { score: number; status: HealthStatus; message: string } => {
const getHealthScore = () => {
if (!lastBackup) return { score: 0, status: 'critical', message: t('backup.dashboard.healthMessages.noBackups') }; if (!lastBackup) return { score: 0, status: 'critical', message: t('backup.dashboard.healthMessages.noBackups') };
const hoursSinceBackup = (Date.now() - new Date(lastBackup.created_at)) / (1000 * 60 * 60); const hoursSinceBackup = (Date.now() - new Date(lastBackup.created_at).getTime()) / (1000 * 60 * 60);
if (lastBackup.status === 'failed') { if (lastBackup.status === 'failed') {
return { score: 0, status: 'critical', message: t('backup.dashboard.healthMessages.lastBackupFailed') }; return { score: 0, status: 'critical', message: t('backup.dashboard.healthMessages.lastBackupFailed') };
@@ -67,7 +123,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
return { score: 100, status: 'excellent', message: t('backup.dashboard.healthMessages.upToDate') }; return { score: 100, status: 'excellent', message: t('backup.dashboard.healthMessages.upToDate') };
} else if (hoursSinceBackup < 48) { } else if (hoursSinceBackup < 48) {
return { score: 75, status: 'good', message: t('backup.dashboard.healthMessages.recent') }; return { score: 75, status: 'good', message: t('backup.dashboard.healthMessages.recent') };
} else if (hoursSinceBackup < 168) { // 1 week } else if (hoursSinceBackup < 168) {
return { score: 50, status: 'warning', message: t('backup.dashboard.healthMessages.gettingOld') }; return { score: 50, status: 'warning', message: t('backup.dashboard.healthMessages.gettingOld') };
} else { } else {
return { score: 25, status: 'critical', message: t('backup.dashboard.healthMessages.outdated') }; return { score: 25, status: 'critical', message: t('backup.dashboard.healthMessages.outdated') };
@@ -75,12 +131,6 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
}; };
const health = getHealthScore(); const health = getHealthScore();
const healthColors = {
excellent: 'green',
good: 'blue',
warning: 'amber',
critical: 'red'
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -19,7 +19,7 @@ const RATING_OPTIONS = [
{ value: 3, label: 'filter.threeStarsPlus' }, { value: 3, label: 'filter.threeStarsPlus' },
{ value: 4, label: 'filter.fourStarsPlus' }, { value: 4, label: 'filter.fourStarsPlus' },
{ value: 5, label: 'filter.fiveStarsOnly' }, { value: 5, label: 'filter.fiveStarsOnly' },
]; ] as const;
export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({ export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
filters, filters,
+472 -357
View File
@@ -158,7 +158,14 @@
"navigation": { "navigation": {
"dashboard": "Dashboard", "dashboard": "Dashboard",
"events": "Veranstaltungen", "events": "Veranstaltungen",
"settings": "Einstellungen" "settings": "Einstellungen",
"archives": "Archive",
"emailSettings": "E-Mail-Einstellungen",
"branding": "Markenidentität",
"eventTypes": "Veranstaltungstypen",
"backup": "Backup & Wiederherstellung",
"cmsPages": "CMS-Seiten",
"users": "Benutzer"
}, },
"eventTypes": { "eventTypes": {
"title": "Veranstaltungsarten", "title": "Veranstaltungsarten",
@@ -275,6 +282,12 @@
"actions": { "actions": {
"runBackupNow": "Backup jetzt ausführen", "runBackupNow": "Backup jetzt ausführen",
"running": "Wird ausgeführt..." "running": "Wird ausgeführt..."
},
"healthStatus": {
"warning": "Warnung",
"excellent": "Ausgezeichnet",
"good": "Gut",
"critical": "Kritisch"
} }
}, },
"configuration": { "configuration": {
@@ -634,51 +647,51 @@
"anonymous": "Anonym" "anonymous": "Anonym"
}, },
"favorites": "Favoriten", "favorites": "Favoriten",
"expiresIn_one": "", "expiresIn_one": "Galerie läuft in {{count}} Tag ab",
"expiresIn_other": "", "expiresIn_other": "Galerie läuft in {{count}} Tagen ab",
"downloading_one": "", "downloading_one": "1 Foto wird heruntergeladen…",
"downloading_other": "", "downloading_other": "{{count}} Fotos werden heruntergeladen…",
"photosSelected_one": "", "photosSelected_one": "{{count}} Foto ausgewählt",
"photosSelected_other": "", "photosSelected_other": "{{count}} Fotos ausgewählt",
"downloadSelected_one": "", "downloadSelected_one": "{{count}} Foto herunterladen",
"downloadSelected_other": "", "downloadSelected_other": "{{count}} Fotos herunterladen",
"guestRecovery": { "guestRecovery": {
"invalidEmail": "", "invalidEmail": "Gültige E-Mail-Adresse eingeben",
"codeSent": "", "codeSent": "Prüfen Sie Ihren Posteingang auf einen Bestätigungscode.",
"requestError": "", "requestError": "Code konnte nicht gesendet werden. Bitte erneut versuchen.",
"invalidCode": "", "invalidCode": "6-stelligen Code eingeben",
"verifyError": "", "verifyError": "Ungültiger oder abgelaufener Code.",
"back": "", "back": "Zurück",
"title": "", "title": "Ihre Auswahl wiederherstellen",
"emailStepDescription": "", "emailStepDescription": "Geben Sie die E-Mail-Adresse ein, die Sie zuvor verwendet haben. Wir senden Ihnen einen 6-stelligen Bestätigungscode.",
"codeStepDescription": "", "codeStepDescription": "Geben Sie den 6-stelligen Code ein, den wir an Ihre E-Mail-Adresse gesendet haben.",
"emailLabel": "", "emailLabel": "E-Mail",
"sendCode": "", "sendCode": "Code senden",
"codeLabel": "", "codeLabel": "Bestätigungscode",
"verifyCode": "" "verifyCode": "Verifizieren und fortfahren"
}, },
"guestPrompt": { "guestPrompt": {
"nameRequired": "", "nameRequired": "Name ist erforderlich",
"invalidEmail": "", "invalidEmail": "Ungültige E-Mail-Adresse",
"emailRequired": "", "emailRequired": "E-Mail ist erforderlich",
"error": "", "error": "Registrierung fehlgeschlagen",
"title": "", "title": "Willkommen — wie heißen Sie?",
"description": "", "description": "Ihre Auswahl wird unter diesem Namen gespeichert, damit der Fotograf weiß, welche Fotos Ihnen gefallen.",
"nameLabel": "", "nameLabel": "Ihr Name",
"namePlaceholder": "", "namePlaceholder": "Name eingeben",
"emailLabelRequired": "", "emailLabelRequired": "E-Mail",
"emailLabel": "", "emailLabel": "E-Mail (optional)",
"emailPlaceholder": "", "emailPlaceholder": "[email protected]",
"submit": "", "submit": "Weiter",
"alreadyHere": "" "alreadyHere": "Ich war schon einmal hier"
}, },
"footer": { "footer": {
"forgetMeConfirm": "", "forgetMeConfirm": "Ihr Name und Ihre Auswahl werden aus dieser Galerie entfernt.",
"forgetMe": "" "forgetMe": "Vergiss mich ({{name}})"
}, },
"photosCount_one": "", "photosCount_one": "{{count}} Foto",
"photosCount_other": "", "photosCount_other": "{{count}} Fotos",
"poweredBy": "" "poweredBy": "Bereitgestellt von PicPeak"
}, },
"categories": { "categories": {
"title": "Fotokategorien", "title": "Fotokategorien",
@@ -907,14 +920,14 @@
"successAll": "{{count}} Veranstaltungen endgültig gelöscht", "successAll": "{{count}} Veranstaltungen endgültig gelöscht",
"successPartial": "{{success}} Veranstaltungen gelöscht, {{failed}} fehlgeschlagen", "successPartial": "{{success}} Veranstaltungen gelöscht, {{failed}} fehlgeschlagen",
"errorGeneric": "Veranstaltungen konnten nicht gelöscht werden", "errorGeneric": "Veranstaltungen konnten nicht gelöscht werden",
"successAll_one": "", "successAll_one": "{{count}} Veranstaltung dauerhaft gelöscht",
"successAll_other": "", "successAll_other": "{{count}} Veranstaltungen dauerhaft gelöscht",
"title_one": "", "title_one": "{{count}} Veranstaltung dauerhaft löschen?",
"title_other": "", "title_other": "{{count}} Veranstaltungen dauerhaft löschen?",
"processing_one": "", "processing_one": "{{count}} Veranstaltung wird gelöscht. Dies kann einige Minuten dauern — bitte schließen Sie dieses Fenster nicht.",
"processing_other": "", "processing_other": "{{count}} Veranstaltungen werden gelöscht. Dies kann einige Minuten dauern — bitte schließen Sie dieses Fenster nicht.",
"submit_one": "", "submit_one": "{{count}} Veranstaltung löschen",
"submit_other": "" "submit_other": "{{count}} Veranstaltungen löschen"
}, },
"searchEventsPlaceholder": "Veranstaltungen suchen...", "searchEventsPlaceholder": "Veranstaltungen suchen...",
"all": "Alle", "all": "Alle",
@@ -941,21 +954,21 @@
"complete": "Fertig!", "complete": "Fertig!",
"failed": "Umbenennung fehlgeschlagen", "failed": "Umbenennung fehlgeschlagen",
"confirm": "Veranstaltung umbenennen", "confirm": "Veranstaltung umbenennen",
"success": "", "success": "Veranstaltung erfolgreich umbenannt!",
"filesRenamed_one": "", "filesRenamed_one": "{{count}} Datei aktualisiert",
"filesRenamed_other": "", "filesRenamed_other": "{{count}} Dateien aktualisiert",
"newLink": "", "newLink": "Neuer Galerie-Link",
"currentName": "", "currentName": "Aktueller Name:",
"newName": "", "newName": "Neuer Veranstaltungsname",
"enterNewName": "", "enterNewName": "Neuen Veranstaltungsnamen eingeben",
"newUrl": "", "newUrl": "Neue URL:",
"checkingAvailability": "", "checkingAvailability": "Verfügbarkeit wird geprüft...",
"resendEmail": "", "resendEmail": "Einladungs-E-Mail mit neuem Galerie-Link erneut senden",
"emailTo": "", "emailTo": "Aktualisierte Galerie-Zugangsemail senden an",
"warningTitle": "", "warningTitle": "Bitte beachten:",
"warning1": "", "warning1": "Die Galerie-URL wird sich ändern",
"warning2": "", "warning2": "Alte URLs werden automatisch zur neuen URL weitergeleitet",
"warning3": "" "warning3": "Fotodateien können umbenannt werden"
}, },
"stats": { "stats": {
"totalEvents": "Gesamtveranstaltungen", "totalEvents": "Gesamtveranstaltungen",
@@ -972,27 +985,27 @@
"downloadingArchive": "{{name}}-Archiv wird heruntergeladen...", "downloadingArchive": "{{name}}-Archiv wird heruntergeladen...",
"downloadStarted": "Download gestartet", "downloadStarted": "Download gestartet",
"failedToDownloadArchive": "Archiv-Download fehlgeschlagen", "failedToDownloadArchive": "Archiv-Download fehlgeschlagen",
"bulkArchiveSuccess_one": "", "bulkArchiveSuccess_one": "{{count}} Veranstaltung erfolgreich archiviert",
"bulkArchiveSuccess_other": "", "bulkArchiveSuccess_other": "{{count}} Veranstaltungen erfolgreich archiviert",
"daysLeft_one": "", "daysLeft_one": "({{count}} Tag verbleibend)",
"daysLeft_other": "", "daysLeft_other": "({{count}} Tage verbleibend)",
"eventsSelected_one": "", "eventsSelected_one": "{{count}} Veranstaltung ausgewählt",
"eventsSelected_other": "", "eventsSelected_other": "{{count}} Veranstaltungen ausgewählt",
"paginationLabel": "", "paginationLabel": "{{from}}{{to}} von {{total}}",
"filtered": "", "filtered": "gefiltert",
"pageOf": "", "pageOf": "Seite {{page}} von {{totalPages}}",
"notFound": "", "notFound": "Veranstaltung nicht gefunden",
"customerPhone": "", "customerPhone": "Kundentelefon",
"customerPhonePlaceholder": "", "customerPhonePlaceholder": "+49 555 555 1234",
"allowPresignedDownload": "", "allowPresignedDownload": "Direkten S3-Download erlauben (kein Wasserzeichen, nur S3-Modus)",
"neverExpires": "", "neverExpires": "Nie",
"rightClickBlocked": "", "rightClickBlocked": "Rechtsklick gesperrt",
"devtoolsDetection": "", "devtoolsDetection": "DevTools-Erkennung",
"watermarked": "", "watermarked": "Mit Wasserzeichen",
"importExternal": "", "importExternal": "Aus externem Ordner importieren",
"externalImportInfo": "", "externalImportInfo": "Alle Bilder aus dem ausgewählten Ordner werden importiert.",
"selectExternalFolder": "", "selectExternalFolder": "Externen Ordner unter /external-media auswählen",
"importFromSelectedFolder": "" "importFromSelectedFolder": "Aus ausgewähltem Ordner importieren"
}, },
"settings": { "settings": {
"title": "Systemeinstellungen", "title": "Systemeinstellungen",
@@ -1139,12 +1152,12 @@
"saveSettings": "Veranstaltungseinstellungen speichern", "saveSettings": "Veranstaltungseinstellungen speichern",
"noteTitle": "Hinweis", "noteTitle": "Hinweis",
"noteText": "Diese Einstellungen betreffen nur die Erstellung neuer Veranstaltungen. Bestehende Veranstaltungen sind nicht betroffen. Standardmäßig sind alle Felder erforderlich.", "noteText": "Diese Einstellungen betreffen nur die Erstellung neuer Veranstaltungen. Bestehende Veranstaltungen sind nicht betroffen. Standardmäßig sind alle Felder erforderlich.",
"defaultRequirePassword": "", "defaultRequirePassword": "Standardmäßig Passwort erforderlich",
"defaultRequirePasswordHelp": "", "defaultRequirePasswordHelp": "\"Passwort erforderlich\" beim Erstellen neuer Veranstaltungen vorausgewählt. Deaktivieren für schnellere Erstellung öffentlicher Galerien.",
"showGalleryFilterBar": "", "showGalleryFilterBar": "Filterleiste in Galerien anzeigen",
"showGalleryFilterBarHelp": "", "showGalleryFilterBarHelp": "Dateiname-Suche und Sortiersteuerungen über Raster-Galerien anzeigen. Deaktivieren für ein übersichtlicheres Layout.",
"enablePhoneField": "", "enablePhoneField": "Telefonnummer-Feld aktivieren",
"enablePhoneFieldHelp": "" "enablePhoneFieldHelp": "Fügt ein optionales Telefonnummer-Eingabefeld zum Veranstaltungsformular hinzu. Nützlich für Folgeautomatisierungen wie WhatsApp-Zustellung via n8n. Immer optional, auch wenn aktiviert."
}, },
"imageSecurity": { "imageSecurity": {
"title": "Bildschutz", "title": "Bildschutz",
@@ -1284,8 +1297,8 @@
"checkSuccess": "Benachrichtigung für neue Version gesendet", "checkSuccess": "Benachrichtigung für neue Version gesendet",
"checkNoAction": "Keine Benachrichtigung erforderlich: {{reason}}", "checkNoAction": "Keine Benachrichtigung erforderlich: {{reason}}",
"checkError": "Fehler beim Prüfen auf Updates", "checkError": "Fehler beim Prüfen auf Updates",
"emailSent_one": "", "emailSent_one": "Benachrichtigungs-E-Mail an {{count}} Empfänger gesendet",
"emailSent_other": "" "emailSent_other": "Benachrichtigungs-E-Mail an {{count}} Empfänger gesendet"
}, },
"moderation": { "moderation": {
"title": "Moderation", "title": "Moderation",
@@ -1344,52 +1357,52 @@
"privacyText": "IP-Adressen werden aus Datenschutzgründen gehasht. Es werden keine persönlichen Daten gespeichert. Analysedaten werden 90 Tage aufbewahrt." "privacyText": "IP-Adressen werden aus Datenschutzgründen gehasht. Es werden keine persönlichen Daten gespeichert. Analysedaten werden 90 Tage aufbewahrt."
}, },
"groups": { "groups": {
"general": "", "general": "Allgemein",
"display": "", "display": "Anzeige",
"privacySecurity": "", "privacySecurity": "Datenschutz & Sicherheit",
"integrations": "", "integrations": "Integrationen",
"system": "" "system": "System"
}, },
"apiTokens": { "apiTokens": {
"title": "", "title": "API-Tokens",
"createError": "", "createError": "Token konnte nicht erstellt werden",
"revoked": "", "revoked": "Token widerrufen",
"subtitle": "", "subtitle": "Langlebige Bearer-Tokens für die öffentliche /api/v1-Oberfläche — n8n-Integrationen, eigene Apps, Skripte. Tokens agieren als der Admin-Benutzer, der sie erstellt hat, eingeschränkt auf die gewählten Berechtigungen.",
"copyNow": "", "copyNow": "Kopieren Sie diesen Token jetzt — er wird nicht erneut angezeigt.",
"copied": "", "copied": "Kopiert",
"copyFailed": "", "copyFailed": "Kopieren fehlgeschlagen",
"name": "", "name": "Name",
"namePlaceholder": "", "namePlaceholder": "z.B. n8n Produktion",
"scopes": "", "scopes": "Berechtigungen",
"generate": "", "generate": "Token generieren",
"scopeHint": "", "scopeHint": "admin > write > read. Ein reiner Lese-Token kann keine Änderungen vornehmen, auch wenn der Besitzer super_admin ist.",
"existing": "", "existing": "Vorhandene Tokens",
"lastUsed": "", "lastUsed": "Zuletzt verwendet",
"created": "", "created": "Erstellt",
"status": "", "status": "Status",
"statusRevoked": "", "statusRevoked": "Widerrufen",
"statusExpired": "", "statusExpired": "Abgelaufen",
"statusActive": "", "statusActive": "Aktiv",
"confirmRevoke": "", "confirmRevoke": "Diesen Token wirklich widerrufen?",
"revoke": "", "revoke": "Widerrufen",
"empty": "" "empty": "Noch keine Tokens. Generieren Sie oben einen, um zu beginnen."
}, },
"webhooks": { "webhooks": {
"title": "", "title": "Webhooks",
"subtitle": "", "subtitle": "Senden Sie POST-Ereignisbenachrichtigungen an Ihre URL, sobald etwas passiert — Galerie veröffentlicht, Foto hochgeladen, Veranstaltung archiviert usw. Signiert mit HMAC-SHA256 im X-PicPeak-Signature-Header.",
"piiNotice": "", "piiNotice": "event.*-Payloads enthalten Kundenkontaktdaten (Name, E-Mail, Telefon) und den Galerie-Share-Token, sofern gespeichert. Richten Sie Webhooks nur auf vertrauenswürdige Empfänger — sie haben alles, um den Kunden zu kontaktieren oder die Galerie zu öffnen.",
"copyNow": "", "copyNow": "Kopieren Sie dieses Signaturgeheimnis jetzt — es wird nicht erneut angezeigt.",
"name": "", "name": "Name",
"url": "", "url": "Empfänger-URL",
"events": "", "events": "Ereignisse abonnieren",
"filter": "", "filter": "Filter (JSON, optional)",
"template": "", "template": "Vorlage (optional)",
"create": "", "create": "Webhook erstellen",
"existing": "", "existing": "Vorhandene Webhooks",
"empty": "" "empty": "Noch keine Webhooks. Erstellen Sie oben einen, um Ereignisbenachrichtigungen zu erhalten."
}, },
"sectionLabel": "", "sectionLabel": "Einstellungsbereich",
"navAriaLabel": "" "navAriaLabel": "Einstellungsnavigation"
}, },
"branding": { "branding": {
"title": "Branding & Themen", "title": "Branding & Themen",
@@ -1588,38 +1601,68 @@
"betaThumbnailWarningTitle": "Niedrige Thumbnail-Auflösung erkannt", "betaThumbnailWarningTitle": "Niedrige Thumbnail-Auflösung erkannt",
"betaThumbnailWarningText": "Ihre Thumbnails sind derzeit {{width}}×{{height}}px. Beta-Themes zeigen Fotos in größeren Formaten an und benötigen mindestens {{recommended}}×{{recommended}}px für gute Qualität. Erhöhen Sie die Thumbnail-Abmessungen unter Einstellungen > Thumbnails und regenerieren Sie diese.", "betaThumbnailWarningText": "Ihre Thumbnails sind derzeit {{width}}×{{height}}px. Beta-Themes zeigen Fotos in größeren Formaten an und benötigen mindestens {{recommended}}×{{recommended}}px für gute Qualität. Erhöhen Sie die Thumbnail-Abmessungen unter Einstellungen > Thumbnails und regenerieren Sie diese.",
"betaThumbnailWarningLink": "Zu den Thumbnail-Einstellungen", "betaThumbnailWarningLink": "Zu den Thumbnail-Einstellungen",
"logoSize": "", "logoSize": "Logo-Größe",
"surfaceColor": "", "surfaceColor": "Oberfläche",
"elevatedColor": "", "elevatedColor": "Erhöht",
"borderColor": "", "borderColor": "Rahmen",
"mutedTextColor": "", "mutedTextColor": "Gedämpfter Text",
"accentDarkColor": "", "accentDarkColor": "Akzent (gefüllt)",
"syncFromBranding": "", "syncFromBranding": "Vom Branding synchronisieren",
"forceColorMode": "", "forceColorMode": "Farbmodus erzwingen",
"forceColorModeHelp": "", "forceColorModeHelp": "Sperrt die gesamte Admin- und öffentliche Website auf Dunkel- oder Hellmodus. Die Dunkel-/Hell-Umschalttaste wird ausgeblendet, wenn eine Sperre aktiv ist. Veranstaltungsthemen, die den Farbmodus überschreiben möchten, werden ebenfalls gezwungen, diesem zu folgen.",
"forceColorModeNone": "", "forceColorModeNone": "Kein Zwang (Benutzerauswahl)",
"forceColorModeDark": "", "forceColorModeDark": "Dunkelmodus erzwingen",
"forceColorModeLight": "", "forceColorModeLight": "Hellmodus erzwingen",
"colorGroupSurfaces": "", "colorGroupSurfaces": "Oberflächen",
"colorGroupSurfacesHelp": "", "colorGroupSurfacesHelp": "Die neutralen Ebenen hinter Ihrem Inhalt. Hintergrund liegt am weitesten zurück; Oberfläche und Erhöht stapeln sich darüber.",
"backgroundColorHelp": "", "backgroundColorHelp": "Die Seite selbst — Hintergrundfarbe jeder Galerie, Admin-Seite und CMS-Seite.",
"surfaceColorHelp": "", "surfaceColorHelp": "Karten, Seitenleiste, Kopfzeile und Navigation. Die erste Ebene über dem Hintergrund.",
"elevatedColorHelp": "", "elevatedColorHelp": "Panels, die über Karten schweben: Bild-Platzhalter, Hover-/Aktiv-Zeilen, Modal-Kopfzeilen, Code-Blöcke.",
"borderColorHelp": "", "borderColorHelp": "Trennlinien, Tabellengitterlinien, Kartenumrisse, Eingabe-Rahmen.",
"colorGroupText": "", "colorGroupText": "Text",
"colorGroupTextHelp": "", "colorGroupTextHelp": "Vordergrund-Textfarben. Primär für alles, worauf Leser sich konzentrieren; Sekundär für unterstützenden Text.",
"textColorHelp": "", "textColorHelp": "Überschriften, Fließtext, Tabellenzellen, Formulareingabewerte, Navigationsbezeichnungen — die Haupttextfarbe.",
"mutedTextColorHelp": "", "mutedTextColorHelp": "Bildunterschriften, Hilfetext unter Eingaben, Tabellen-Spaltenüberschriften, Fußzeilenlinks, Daten und Metadaten.",
"colorGroupAccent": "", "colorGroupAccent": "Akzent",
"colorGroupAccentHelp": "", "colorGroupAccentHelp": "Markenfarben, die interaktive Elemente hervorheben. Verwenden Sie ein starkes Farbpaar — Akzent für Umrisse/Text, Akzent Dunkel für gefüllte Buttons.",
"accentColorHelp": "", "accentColorHelp": "Links, Symbole, Fokusringe, Hover-Zustände bei Primär-Buttons, aktive Seitenleisten-Unterstreichung. Sollte auf Hintergrund und Oberfläche gut lesbar sein.",
"accentDarkColorHelp": "", "accentDarkColorHelp": "Gefüllte CTA-Buttons, aktiver Seitenleisten-Eintrag-Hintergrund, Abzeichen und Tags. Muss genug Kontrast für weißen Text bieten.",
"cssTemplate": "", "cssTemplate": "CSS-Vorlage",
"cssTemplateDescription": "", "cssTemplateDescription": "Wählen Sie eine vorgefertigte CSS-Vorlage, um dieser Galerie ein anwendungsweites Styling zu geben. Vorlagen können unter Einstellungen > CSS-Vorlagen verwaltet werden.",
"noTemplate": "", "noTemplate": "Keine Vorlage",
"noTemplateDescription": "", "noTemplateDescription": "Nur Theme-Einstellungen ohne CSS-Vorlage verwenden",
"templateSlot": "", "templateSlot": "Slot {{slot}}",
"eventCustomCSS": "" "eventCustomCSS": "Veranstaltungsspezifisches benutzerdefiniertes CSS",
"layoutDescriptions": {
"grid": "Klassisches Raster mit einheitlichen Fotogrößen",
"masonry": "Pinterest-Stil mit variablen Höhen",
"carousel": "Vollbild-Diashow mit Navigation",
"timeline": "Nach Datum geordnete Fotos",
"mosaic": "Künstlerisches Layout mit gemischten Größen",
"gallery-premium": "Elegantes helles Thema mit Hero und Masonry (Beta)",
"gallery-story": "Cinematisches dunkles Thema mit Szenenabschnitten (Beta)"
},
"headerStyleOptions": {
"hero": "Hero-Bild",
"standard": "Standard",
"banner": "Banner",
"minimal": "Minimal",
"none": "Kein Header"
},
"headerStyleDescriptions": {
"hero": "Vollhöhenbild mit Veranstaltungsinfo-Overlay",
"standard": "Kompakter Header mit Veranstaltungsdetails",
"banner": "Standard-Header plus farbiges Banner darüber",
"minimal": "Kompakter Header mit wesentlichen Infos",
"none": "Header vollständig ausblenden"
},
"dividerOptions": {
"none": "Keiner",
"wave": "Welle",
"straight": "Gerade",
"angle": "Winkel",
"curve": "Kurve"
}
}, },
"admin": { "admin": {
"title": "Admin-Panel", "title": "Admin-Panel",
@@ -1635,7 +1678,10 @@
"archivedEvents": "Archivierte Veranstaltungen", "archivedEvents": "Archivierte Veranstaltungen",
"systemHealth": "Systemstatus", "systemHealth": "Systemstatus",
"health": { "health": {
"checking": "Prüfe..." "checking": "Prüfe...",
"healthy": "Gesund",
"warning": "Warnung",
"error": "Fehler"
}, },
"updates": { "updates": {
"available": "Update verfügbar", "available": "Update verfügbar",
@@ -1694,17 +1740,17 @@
"archiveRestored": "Archiv für \"{{eventName}}\" wiederhergestellt", "archiveRestored": "Archiv für \"{{eventName}}\" wiederhergestellt",
"systemActivity": "Systemaktivität: {{type}}", "systemActivity": "Systemaktivität: {{type}}",
"adminProfileUpdated": "Admin-Profil aktualisiert von {{actorName}}", "adminProfileUpdated": "Admin-Profil aktualisiert von {{actorName}}",
"photosUploaded_one": "", "photosUploaded_one": "{{count}} Foto in \"{{eventName}}\" hochgeladen",
"photosUploaded_other": "", "photosUploaded_other": "{{count}} Fotos in \"{{eventName}}\" hochgeladen",
"photosBulkDeleted_one": "", "photosBulkDeleted_one": "{{count}} Foto aus \"{{eventName}}\" gelöscht",
"photosBulkDeleted_other": "", "photosBulkDeleted_other": "{{count}} Fotos aus \"{{eventName}}\" gelöscht",
"bulkDownload_one": "", "bulkDownload_one": "{{count}} Foto aus \"{{eventName}}\" heruntergeladen",
"bulkDownload_other": "" "bulkDownload_other": "{{count}} Fotos aus \"{{eventName}}\" heruntergeladen"
}, },
"notificationToasts": { "notificationToasts": {
"markedAllRead": "Alle Benachrichtigungen als gelesen markiert", "markedAllRead": "Alle Benachrichtigungen als gelesen markiert",
"clearedAll_one": "", "clearedAll_one": "{{count}} Benachrichtigung gelöscht",
"clearedAll_other": "" "clearedAll_other": "{{count}} Benachrichtigungen gelöscht"
}, },
"changePassword": "Passwort ändern", "changePassword": "Passwort ändern",
"loadingDashboard": "Dashboard wird geladen...", "loadingDashboard": "Dashboard wird geladen...",
@@ -1724,73 +1770,135 @@
"showSelected": "Einblenden", "showSelected": "Einblenden",
"hiddenSuccess": "Fotos für Gäste ausgeblendet", "hiddenSuccess": "Fotos für Gäste ausgeblendet",
"visibleSuccess": "Fotos jetzt für Gäste sichtbar", "visibleSuccess": "Fotos jetzt für Gäste sichtbar",
"processingStatus": "", "processingStatus": "Wird verarbeitet…",
"processingFailed": "", "processingFailed": "Fehlgeschlagen",
"retryQueued": "" "retryQueued": "Wiederholung in Warteschlange"
}, },
"events": { "events": {
"tabs": { "tabs": {
"guests": "" "guests": "Gäste"
} }
}, },
"daysLeft_one": "", "daysLeft_one": "{{count}} Tag verbleibend",
"daysLeft_other": "", "daysLeft_other": "{{count}} Tage verbleibend",
"viewAllExpiringEvents_one": "", "viewAllExpiringEvents_one": "Alle {{count}} ablaufenden Veranstaltungen anzeigen",
"viewAllExpiringEvents_other": "", "viewAllExpiringEvents_other": "Alle {{count}} ablaufenden Veranstaltungen anzeigen",
"guests": { "guests": {
"loading": "", "loading": "Wird geladen...",
"aggregate": { "aggregate": {
"empty": "", "empty": "Noch keine Gastauswahl.",
"description": "" "description": "Fotos sortiert nach der Anzahl der Gäste, die sie geliket oder favorisiert haben."
}, },
"inviteCreated": "", "inviteCreated": "Einladung erstellt",
"inviteCreateError": "", "inviteCreateError": "Einladung konnte nicht erstellt werden",
"inviteRevoked": "", "inviteRevoked": "Einladung widerrufen",
"inviteRevokeError": "", "inviteRevokeError": "Einladung konnte nicht widerrufen werden",
"invitesTitle": "", "invitesTitle": "Gasteinladungen",
"createInvite": "", "createInvite": "Einladung erstellen",
"inviteName": "", "inviteName": "Gastname",
"inviteEmail": "", "inviteEmail": "E-Mail (optional)",
"generateInvite": "", "generateInvite": "Einladungslink generieren",
"existingInvites": "", "existingInvites": "Vorhandene Einladungen",
"noInvites": "", "noInvites": "Noch keine Einladungen",
"copyLink": "", "copyLink": "Link kopieren",
"revokeInvite": "", "revokeInvite": "Widerrufen",
"deletedToast": "", "deletedToast": "Gast entfernt",
"deletedError": "", "deletedError": "Gast konnte nicht entfernt werden",
"mergedToast": "", "mergedToast": "Gäste zusammengeführt",
"mergedError": "", "mergedError": "Gäste konnten nicht zusammengeführt werden",
"forgetGuestConfirm": "", "forgetGuestConfirm": "Diesen Gast entfernen? Ihre Auswahl wird anonymisiert, aber in den Gesamtzahlen behalten.",
"exportError": "", "exportError": "Export fehlgeschlagen",
"mergeSelectAtLeastTwo": "", "mergeSelectAtLeastTwo": "Mindestens 2 Gäste zum Zusammenführen auswählen",
"mergeConfirm_one": "", "mergeConfirm_one": "{{count}} Gast in {{name}} zusammenführen? Dies kann nicht rückgängig gemacht werden.",
"mergeConfirm_other": "", "mergeConfirm_other": "{{count}} Gäste in {{name}} zusammenführen? Dies kann nicht rückgängig gemacht werden.",
"backToList": "", "backToList": "Zurück zur Liste",
"title": "", "title": "Gäste",
"mergeSelected_one": "", "mergeSelected_one": "{{count}} ausgewählt",
"mergeSelected_other": "", "mergeSelected_other": "{{count}} ausgewählt",
"mergeNow": "", "mergeNow": "Auswahl zusammenführen",
"aggregateView": "", "aggregateView": "Nach Beliebtheit",
"mergeMode": "", "mergeMode": "Zusammenführen",
"exportAll": "", "exportAll": "Alle exportieren",
"empty": "", "empty": "Noch keine Gäste registriert.",
"columns": { "columns": {
"name": "", "name": "Name",
"email": "", "email": "E-Mail",
"likes": "", "likes": "Likes",
"favorites": "", "favorites": "Favoriten",
"comments": "", "comments": "Kommentare",
"ratings": "", "ratings": "Bewertungen",
"lastSeen": "" "lastSeen": "Zuletzt gesehen"
}, },
"view": "", "view": "Details anzeigen",
"export": "", "export": "Exportieren",
"forgetGuest": "", "forgetGuest": "Gast entfernen",
"loadingDetail": "", "loadingDetail": "Auswahl wird geladen...",
"detail": { "detail": {
"noComments": "", "noComments": "Keine Kommentare",
"empty": "" "empty": "Keine Auswahl in dieser Kategorie",
"all": "Alle",
"liked": "Gemocht",
"favorited": "Favorisiert",
"rated": "Bewertet",
"commented": "Kommentiert"
},
"inviteStatus": {
"pending": "Ausstehend",
"redeemed": "Eingelöst",
"revoked": "Widerrufen"
} }
},
"analytics": "Analytik",
"activities": {
"event_created": "Neues Ereignis erstellt: {{eventName}}",
"photos_uploaded": "{{count}} Fotos hochgeladen in {{eventName}}",
"event_archived": "Ereignis archiviert: {{eventName}}",
"archive_restored": "Archiv wiederhergestellt: {{eventName}}",
"archive_deleted": "Archiv gelöscht: {{eventName}}",
"archive_downloaded": "Archiv heruntergeladen: {{eventName}}",
"email_config_updated": "E-Mail-Konfiguration aktualisiert",
"email_template_updated": "E-Mail-Vorlage aktualisiert: {{template}}",
"branding_updated": "Branding-Einstellungen aktualisiert",
"theme_updated": "Theme-Einstellungen aktualisiert",
"bulk_download": "{{count}} Fotos heruntergeladen von {{eventName}}",
"gallery_password_entry": "Passwort eingegeben für {{eventName}}",
"expiration_warning_viewed": "Ablaufwarnung angesehen für {{eventName}}",
"event_updated": "Ereignis aktualisiert: {{eventName}}",
"event_deleted": "Ereignis gelöscht: {{eventName}}",
"event_activated": "Ereignis aktiviert: {{eventName}}",
"event_deactivated": "Ereignis deaktiviert: {{eventName}}",
"bulk_archive_completed": "Massenarchivierung abgeschlossen",
"photo_deleted": "Foto gelöscht von {{eventName}}",
"photos_bulk_deleted": "{{count}} Fotos gelöscht von {{eventName}}",
"password_changed": "Passwort geändert",
"admin_logout": "Admin {{actorName}} abgemeldet",
"category_created": "Kategorie erstellt: {{categoryName}}",
"category_updated": "Kategorie aktualisiert: {{categoryName}}",
"category_deleted": "Kategorie gelöscht: {{categoryName}}",
"feedback_settings_updated": "Feedback-Einstellungen aktualisiert",
"feedback_moderated": "Feedback moderiert",
"feedback_deleted": "Feedback gelöscht",
"word_filter_added": "Wortfilter hinzugefügt",
"cms_page_updated": "CMS-Seite aktualisiert: {{page}}",
"email_resent": "Erstellungs-E-Mail erneut gesendet für: {{eventName}}",
"settings_updated": "Einstellungen aktualisiert",
"favicon_uploaded": "Favicon hochgeladen",
"password_reset": "Passwort zurückgesetzt für: {{eventName}}",
"external_import_completed": "Externer Medienimport abgeschlossen ({{imported}} importiert, {{skipped}} übersprungen)",
"photo_like": "Foto geliked in {{eventName}}",
"photo_favorite": "Foto als Favorit markiert in {{eventName}}",
"general_settings_updated": "Allgemeine Einstellungen aktualisiert",
"analytics_settings_updated": "Analytik-Einstellungen aktualisiert",
"security_settings_updated": "Sicherheitseinstellungen aktualisiert",
"event_renamed": "Ereignis umbenannt: {{eventName}}",
"photo_rating": "Foto bewertet in {{eventName}}",
"photo_comment": "Foto kommentiert in {{eventName}}",
"guest_feedback_like": "Gast hat ein Foto geliked in {{eventName}}",
"guest_feedback_favorite": "Gast hat ein Foto als Favorit markiert in {{eventName}}",
"guest_feedback_rating": "Gast hat ein Foto bewertet in {{eventName}}",
"guest_feedback_comment": "Gast hat ein Foto kommentiert in {{eventName}}",
"system_activity": "Systemaktivität: {{type}}",
"unknown": "Unbekannte Aktivität"
} }
}, },
"acceptInvitation": { "acceptInvitation": {
@@ -1857,8 +1965,8 @@
"requiredFields": "Bitte füllen Sie alle Pflichtfelder aus", "requiredFields": "Bitte füllen Sie alle Pflichtfelder aus",
"enterTestEmail": "Bitte geben Sie eine Test-E-Mail-Adresse ein", "enterTestEmail": "Bitte geben Sie eine Test-E-Mail-Adresse ein",
"eventCreationFailed": "Veranstaltung konnte nicht erstellt werden", "eventCreationFailed": "Veranstaltung konnte nicht erstellt werden",
"noShareLink": "", "noShareLink": "Kein Freigabe-Link verfügbar",
"copyFailed": "" "copyFailed": "Link konnte nicht kopiert werden"
}, },
"legal": { "legal": {
"impressum": "Impressum", "impressum": "Impressum",
@@ -1880,8 +1988,8 @@
"brandingUpdated": "Branding erfolgreich aktualisiert", "brandingUpdated": "Branding erfolgreich aktualisiert",
"categoryUpdated": "Kategorie erfolgreich aktualisiert", "categoryUpdated": "Kategorie erfolgreich aktualisiert",
"emailConfigSaved": "E-Mail-Konfiguration erfolgreich gespeichert", "emailConfigSaved": "E-Mail-Konfiguration erfolgreich gespeichert",
"brandingThemeMissing": "", "brandingThemeMissing": "Es wurde noch kein Branding-Theme gespeichert.",
"brandingPaletteSynced": "" "brandingPaletteSynced": "Palette vom Branding synchronisiert."
}, },
"analytics": { "analytics": {
"title": "Analytics Dashboard", "title": "Analytics Dashboard",
@@ -1978,22 +2086,22 @@
"noTranslation": "Noch keine Übersetzung", "noTranslation": "Noch keine Übersetzung",
"noTranslationYet": "Für diese Sprache existiert noch keine Übersetzung. Kopieren Sie von einer vorhandenen Sprache:", "noTranslationYet": "Für diese Sprache existiert noch keine Übersetzung. Kopieren Sie von einer vorhandenen Sprache:",
"copyFrom": "Kopieren von", "copyFrom": "Kopieren von",
"syncedFromBranding": "", "syncedFromBranding": "E-Mail-Farben vom Branding synchronisiert. Klicken Sie auf Speichern, um sie anzuwenden.",
"syncFromBranding": "", "syncFromBranding": "Vom Branding synchronisieren",
"primaryColorHelp": "", "primaryColorHelp": "Kopfzeile, H2-Überschriften, Button-Hintergrund, Linkfarbe. Entspricht Branding → Akzent (gefüllt).",
"secondaryColorHelp": "", "secondaryColorHelp": "Fußzeilen-Hintergrund. Entspricht Branding → Oberfläche.",
"bodyBgColor": "", "bodyBgColor": "Seitenhintergrund",
"bodyBgColorHelp": "", "bodyBgColorHelp": "Der Rahmen um die E-Mail-Karte — was der Empfänger hinter der E-Mail sieht. Entspricht Branding → Hintergrund.",
"containerBgColor": "", "containerBgColor": "E-Mail-Karte",
"containerBgColorHelp": "", "containerBgColorHelp": "Die weiße Karte, die den E-Mail-Inhalt enthält. Entspricht Branding → Oberfläche.",
"listBgColor": "", "listBgColor": "Infopanel",
"listBgColorHelp": "", "listBgColorHelp": "Hintergrund der Aufzählungs-Infopanels im E-Mail-Text. Entspricht Branding → Erhöht.",
"bodyTextColor": "", "bodyTextColor": "Fließtext",
"bodyTextColorHelp": "", "bodyTextColorHelp": "Absatz- und Fetttext-Farbe. Entspricht Branding → Primärer Text.",
"mutedTextColor": "", "mutedTextColor": "Fußzeilentext",
"mutedTextColorHelp": "", "mutedTextColorHelp": "Fußzeilentext und Copyright-Zeile. Entspricht Branding → Sekundärer Text.",
"buttonTextColor": "", "buttonTextColor": "Button-Text",
"buttonTextColorHelp": "" "buttonTextColorHelp": "Textfarbe auf gefüllten Buttons. Sollte klar gegen die Primärfarbe kontrastieren. Kein Branding-Äquivalent — meistens weiß."
}, },
"cms": { "cms": {
"title": "CMS-Seiten", "title": "CMS-Seiten",
@@ -2015,14 +2123,14 @@
"externalUrlPlaceholder": "https://example.com/impressum", "externalUrlPlaceholder": "https://example.com/impressum",
"externalUrlInvalid": "Muss eine gültige https://-URL sein", "externalUrlInvalid": "Muss eine gültige https://-URL sein",
"externalUrlActive": "Externe URL ist aktiv — interne Inhalte bleiben gespeichert, werden Besuchern aber nicht angezeigt.", "externalUrlActive": "Externe URL ist aktiv — interne Inhalte bleiben gespeichert, werden Besuchern aber nicht angezeigt.",
"logoUploaded": "", "logoUploaded": "Logo hochgeladen",
"logoCleared": "", "logoCleared": "Logo entfernt",
"pageLogo": "", "pageLogo": "Seitenlogo",
"pageLogoHelp": "", "pageLogoHelp": "Optional. Falls gesetzt, wird es anstelle des globalen Branding-Logos auf dieser Seite verwendet.",
"noLogo": "", "noLogo": "keine Überschreibung",
"replaceLogo": "", "replaceLogo": "Logo ersetzen",
"uploadLogo": "", "uploadLogo": "Logo hochladen",
"clearLogo": "" "clearLogo": "Seitenstandard verwenden"
}, },
"validation": { "validation": {
"eventNameRequired": "Veranstaltungsname ist erforderlich", "eventNameRequired": "Veranstaltungsname ist erforderlich",
@@ -2034,10 +2142,10 @@
"passwordMinLength": "Passwort muss mindestens 6 Zeichen lang sein", "passwordMinLength": "Passwort muss mindestens 6 Zeichen lang sein",
"passwordsDoNotMatch": "Passwörter stimmen nicht überein", "passwordsDoNotMatch": "Passwörter stimmen nicht überein",
"expirationRange": "Ablauf muss zwischen 1 und 365 Tagen liegen", "expirationRange": "Ablauf muss zwischen 1 und 365 Tagen liegen",
"required": "", "required": "Dieses Feld ist erforderlich",
"expirationRequired": "", "expirationRequired": "Ablaufdatum ist erforderlich.",
"eventDateRequired": "", "eventDateRequired": "Veranstaltungsdatum ist erforderlich",
"passwordTooSimple": "" "passwordTooSimple": "Das Passwort darf nicht nur aus Zahlen bestehen. Verwenden Sie z.B. ein Datumsformat wie \"04.07.2025\""
}, },
"maintenance": { "maintenance": {
"title": "Systemwartung", "title": "Systemwartung",
@@ -2133,96 +2241,96 @@
"commentsDesc": "Textkommentare auf Fotos", "commentsDesc": "Textkommentare auf Fotos",
"favorites": "Favoriten", "favorites": "Favoriten",
"favoritesDesc": "Fotos als Favoriten markieren", "favoritesDesc": "Fotos als Favoriten markieren",
"identityMode": "", "identityMode": "Identitätsmodus",
"identityModeSimple": "", "identityModeSimple": "Einfaches Feedback",
"identityModeSimpleDesc": "", "identityModeSimpleDesc": "Anonym, gerätebasiert. Alle Besucher auf demselben Gerät teilen den Zustand.",
"identityModeGuest": "", "identityModeGuest": "Gastbezogene Auswahl",
"identityModeGuestDesc": "", "identityModeGuestDesc": "Jeder Besucher gibt seinen Namen ein. Ermöglicht gastbezogenes Tracking und Admin-Einblicke.",
"privacyModeration": "", "privacyModeration": "Datenschutz & Moderation",
"requireInfo": "", "requireInfo": "Name & E-Mail erforderlich",
"requireInfoDesc": "", "requireInfoDesc": "Gäste müssen Name und E-Mail angeben, um Feedback zu hinterlassen",
"moderateComments": "", "moderateComments": "Kommentare moderieren",
"moderateCommentsDesc": "", "moderateCommentsDesc": "Kommentare benötigen eine Genehmigung, bevor sie sichtbar sind",
"showToGuests": "", "showToGuests": "Feedback für Gäste anzeigen",
"showToGuestsDesc": "", "showToGuestsDesc": "Andere Gäste können Bewertungen, Likes und genehmigte Kommentare sehen",
"enableRateLimiting": "", "enableRateLimiting": "Ratenbegrenzung aktivieren",
"rateLimitingDesc": "", "rateLimitingDesc": "Spam durch Begrenzung der Feedback-Häufigkeit verhindern",
"timeWindow": "", "timeWindow": "Zeitfenster (Minuten)",
"maxRequests": "" "maxRequests": "Maximale Anfragen"
}, },
"settingsUpdated": "", "settingsUpdated": "Feedback-Einstellungen aktualisiert",
"settingsUpdateError": "", "settingsUpdateError": "Einstellungen konnten nicht aktualisiert werden",
"moderated": "", "moderated": "Feedback moderiert",
"deleted": "", "deleted": "Feedback gelöscht",
"exported": "", "exported": "Feedback exportiert",
"exportError": "", "exportError": "Feedback konnte nicht exportiert werden",
"title": "", "title": "Feedback-Verwaltung",
"exportCSV": "", "exportCSV": "CSV exportieren",
"exportJSON": "", "exportJSON": "JSON exportieren",
"tabs": { "tabs": {
"settings": "", "settings": "Einstellungen",
"feedback": "", "feedback": "Feedback",
"analytics": "", "analytics": "Analytik",
"moderation": "" "moderation": "Moderation"
}, },
"allTypes": "", "allTypes": "Alle Typen",
"types": { "types": {
"rating": "", "rating": "Bewertungen",
"like": "", "like": "Likes",
"comment": "", "comment": "Kommentare",
"favorite": "" "favorite": "Favoriten"
}, },
"allStatuses": "", "allStatuses": "Alle Status",
"status": { "status": {
"pending": "", "pending": "Ausstehend",
"approved": "", "approved": "Genehmigt",
"hidden": "" "hidden": "Versteckt"
}, },
"noFeedback": "", "noFeedback": "Kein Feedback gefunden",
"approve": "", "approve": "Genehmigen",
"hide": "", "hide": "Ausblenden",
"unhide": "", "unhide": "Einblenden",
"confirmDelete": "", "confirmDelete": "Dieses Feedback wirklich löschen?",
"avgRating": "", "avgRating": "Durchschnittsbewertung",
"totalRatings_one": "", "totalRatings_one": "{{count}} Bewertungen",
"totalRatings_other": "", "totalRatings_other": "{{count}} Bewertungen",
"totalLikes": "", "totalLikes": "Gesamte Likes",
"totalComments": "", "totalComments": "Gesamte Kommentare",
"pendingModeration_one": "", "pendingModeration_one": "{{count}} ausstehend",
"pendingModeration_other": "", "pendingModeration_other": "{{count}} ausstehend",
"totalInteractions": "", "totalInteractions": "Gesamte Interaktionen",
"topRated": "", "topRated": "Bestbewertete Fotos",
"recentComments": "", "recentComments": "Neueste Kommentare",
"wordFilters": "", "wordFilters": "Wortfilter",
"wordFiltersDesc": "", "wordFiltersDesc": "Gesperrte Wörter für die Kommentarmoderation verwalten",
"manageFilters": "", "manageFilters": "Wortfilter verwalten",
"manage": "", "manage": "Feedback verwalten",
"ratingSubmitted": "", "ratingSubmitted": "Bewertung abgegeben",
"ratingError": "", "ratingError": "Bewertung konnte nicht abgegeben werden",
"rateStar_one": "", "rateStar_one": "{{count}} Stern bewerten",
"rateStar_other": "", "rateStar_other": "{{count}} Sterne bewerten",
"ratingsCount_one": "", "ratingsCount_one": "{{count}} Bewertungen",
"ratingsCount_other": "", "ratingsCount_other": "{{count}} Bewertungen",
"likeError": "", "likeError": "Like konnte nicht aktualisiert werden",
"unlike": "", "unlike": "Nicht mehr mögen",
"like": "", "like": "Gefällt mir",
"favoriteError": "", "favoriteError": "Favorit konnte nicht aktualisiert werden",
"unfavorite": "", "unfavorite": "Aus Favoriten entfernen",
"favorite": "", "favorite": "Zu Favoriten hinzufügen",
"invalidEmail": "", "invalidEmail": "Ungültige E-Mail-Adresse",
"identityRequired": "", "identityRequired": "Ihre Informationen erforderlich",
"identityReason": "", "identityReason": "Bitte geben Sie Ihren Namen und Ihre E-Mail an, um {{type}} zu senden.",
"namePlaceholder": "", "namePlaceholder": "Ihren Namen eingeben",
"emailPlaceholder": "", "emailPlaceholder": "Ihre E-Mail eingeben",
"submitFeedback": "", "submitFeedback": "Feedback senden",
"moderationSuccess": "", "moderationSuccess": "Feedback moderiert",
"pendingModeration": "", "pendingModeration": "Ausstehende Moderation",
"pending": "", "pending": "ausstehend",
"noPendingComments": "", "noPendingComments": "Keine Kommentare warten auf Moderation",
"onPhoto": "", "onPhoto": "Auf Foto",
"showAll_one": "", "showAll_one": "Alle {{count}} ausstehenden Kommentare anzeigen",
"showAll_other": "", "showAll_other": "Alle {{count}} ausstehenden Kommentare anzeigen",
"viewAllFeedback": "" "viewAllFeedback": "Gesamtes Feedback & Einstellungen anzeigen"
}, },
"filter": { "filter": {
"feedbackFilters": "Feedback-Filter", "feedbackFilters": "Feedback-Filter",
@@ -2233,7 +2341,14 @@
"hasComments": "Hat Kommentare", "hasComments": "Hat Kommentare",
"showingPhotos": "Fotos gesamt", "showingPhotos": "Fotos gesamt",
"withRatings": "Mit Bewertungen", "withRatings": "Mit Bewertungen",
"combineWith": "" "combineWith": "Kombinieren mit",
"allPhotos": "Alle Fotos",
"anyRating": "Jede Bewertung",
"oneStarPlus": "1+ Stern",
"twoStarsPlus": "2+ Sterne",
"threeStarsPlus": "3+ Sterne",
"fourStarsPlus": "4+ Sterne",
"fiveStarsOnly": "Nur 5 Sterne"
}, },
"export": { "export": {
"button": "Exportieren", "button": "Exportieren",
@@ -2241,8 +2356,8 @@
"error": "Export fehlgeschlagen: ", "error": "Export fehlgeschlagen: ",
"exportFiltered": "Gefilterte Fotos exportieren", "exportFiltered": "Gefilterte Fotos exportieren",
"hint": "Fotos auswählen oder Filter anwenden zum Exportieren", "hint": "Fotos auswählen oder Filter anwenden zum Exportieren",
"exportSelected_one": "", "exportSelected_one": "{{count}} ausgewähltes exportieren",
"exportSelected_other": "" "exportSelected_other": "{{count}} ausgewählte exportieren"
}, },
"clientAccess": { "clientAccess": {
"title": "Kundenzugang", "title": "Kundenzugang",
@@ -2324,15 +2439,15 @@
"dateTaken": "Aufnahmedatum" "dateTaken": "Aufnahmedatum"
}, },
"photos": { "photos": {
"moveToCategory_one": "", "moveToCategory_one": "{{count}} Foto in Kategorie verschieben",
"moveToCategory_other": "", "moveToCategory_other": "{{count}} Fotos in Kategorie verschieben",
"selectCategory": "", "selectCategory": "Kategorie auswählen",
"uncategorized": "", "uncategorized": "Ohne Kategorie",
"movePhotos": "", "movePhotos": "Fotos verschieben",
"selectedCategory": "", "selectedCategory": "Ausgewählte Kategorie",
"movedToCategory_one": "", "movedToCategory_one": "{{count}} Foto nach {{category}} verschoben",
"movedToCategory_other": "", "movedToCategory_other": "{{count}} Fotos nach {{category}} verschoben",
"moveToCategoryFailed": "", "moveToCategoryFailed": "Fotos konnten nicht in die Kategorie verschoben werden",
"moveToCategory": "" "moveToCategory": "In Kategorie verschieben"
} }
} }
+120 -5
View File
@@ -158,7 +158,14 @@
"navigation": { "navigation": {
"dashboard": "Dashboard", "dashboard": "Dashboard",
"events": "Events", "events": "Events",
"settings": "Settings" "archives": "Archives",
"settings": "Settings",
"eventTypes": "Event Types",
"branding": "Branding",
"emailSettings": "Email Settings",
"backup": "Backup & Restore",
"cmsPages": "CMS Pages",
"users": "Users"
}, },
"archives": { "archives": {
"title": "Archives", "title": "Archives",
@@ -1295,7 +1302,37 @@
"noTemplate": "No Template", "noTemplate": "No Template",
"noTemplateDescription": "Use only theme settings without a CSS template", "noTemplateDescription": "Use only theme settings without a CSS template",
"templateSlot": "Slot {{slot}}", "templateSlot": "Slot {{slot}}",
"eventCustomCSS": "Event-specific Custom CSS" "eventCustomCSS": "Event-specific Custom CSS",
"layoutDescriptions": {
"grid": "Classic grid layout with consistent photo sizes",
"masonry": "Pinterest-style layout with varied heights",
"carousel": "Full-screen slideshow with navigation",
"timeline": "Photos organized by date",
"mosaic": "Artistic layout with mixed sizes",
"gallery-premium": "Elegant light theme with hero and masonry (Beta)",
"gallery-story": "Cinematic dark theme with scene sections (Beta)"
},
"headerStyleOptions": {
"hero": "Hero Image",
"standard": "Standard",
"banner": "Banner",
"minimal": "Minimal",
"none": "No Header"
},
"headerStyleDescriptions": {
"hero": "Full-height image with event info overlay",
"standard": "Compact inline header with event details",
"banner": "Standard header plus a colored banner above",
"minimal": "Compact header with essential info",
"none": "Hide header completely"
},
"dividerOptions": {
"wave": "Wave",
"straight": "Straight",
"angle": "Angle",
"curve": "Curve",
"none": "None"
}
}, },
"admin": { "admin": {
"title": "Admin Panel", "title": "Admin Panel",
@@ -1309,6 +1346,9 @@
"archivedEvents": "Archived Events", "archivedEvents": "Archived Events",
"systemHealth": "System Health", "systemHealth": "System Health",
"health": { "health": {
"healthy": "Healthy",
"warning": "Warning",
"error": "Error",
"checking": "Checking..." "checking": "Checking..."
}, },
"updates": { "updates": {
@@ -1465,8 +1505,70 @@
"loadingDetail": "Loading selections...", "loadingDetail": "Loading selections...",
"detail": { "detail": {
"noComments": "No comments", "noComments": "No comments",
"empty": "No selections in this category" "empty": "No selections in this category",
"all": "All",
"liked": "Liked",
"favorited": "Favorited",
"rated": "Rated",
"commented": "Commented"
},
"inviteStatus": {
"pending": "Pending",
"redeemed": "Redeemed",
"revoked": "Revoked"
} }
},
"analytics": "Analytics",
"activities": {
"event_created": "New event created: {{eventName}}",
"photos_uploaded": "{{count}} photos uploaded to {{eventName}}",
"event_archived": "Event archived: {{eventName}}",
"archive_restored": "Archive restored: {{eventName}}",
"archive_deleted": "Archive deleted: {{eventName}}",
"archive_downloaded": "Archive downloaded: {{eventName}}",
"email_config_updated": "Email configuration updated",
"email_template_updated": "Email template updated: {{template}}",
"branding_updated": "Branding settings updated",
"theme_updated": "Theme settings updated",
"bulk_download": "{{count}} photos downloaded from {{eventName}}",
"gallery_password_entry": "Password entered for {{eventName}}",
"expiration_warning_viewed": "Expiration warning viewed for {{eventName}}",
"feedback_settings_updated": "Feedback settings updated",
"feedback_moderated": "Feedback moderated",
"feedback_deleted": "Feedback deleted",
"photo_like": "Photo liked in {{eventName}}",
"photo_favorite": "Photo favorited in {{eventName}}",
"photo_rating": "Photo rated in {{eventName}}",
"photo_comment": "Photo commented in {{eventName}}",
"guest_feedback_like": "Guest liked a photo in {{eventName}}",
"guest_feedback_favorite": "Guest favorited a photo in {{eventName}}",
"guest_feedback_rating": "Guest rated a photo in {{eventName}}",
"guest_feedback_comment": "Guest commented on a photo in {{eventName}}",
"word_filter_added": "Word filter added",
"external_import_completed": "External media import completed ({{imported}} imported, {{skipped}} skipped)",
"bulk_archive_completed": "Bulk archive completed",
"event_activated": "Event activated: {{eventName}}",
"event_deactivated": "Event deactivated: {{eventName}}",
"photo_deleted": "Photo deleted from {{eventName}}",
"photos_bulk_deleted": "{{count}} photos deleted from {{eventName}}",
"settings_updated": "Settings updated",
"event_updated": "Event updated: {{eventName}}",
"event_renamed": "Event renamed: {{eventName}}",
"event_deleted": "Event deleted: {{eventName}}",
"password_changed": "Password changed",
"email_resent": "Creation email resent for: {{eventName}}",
"category_created": "Category created: {{categoryName}}",
"category_updated": "Category updated: {{categoryName}}",
"category_deleted": "Category deleted: {{categoryName}}",
"general_settings_updated": "General settings updated",
"favicon_uploaded": "Favicon uploaded",
"analytics_settings_updated": "Analytics settings updated",
"cms_page_updated": "CMS page updated: {{page}}",
"security_settings_updated": "Security settings updated",
"password_reset": "Password reset for: {{eventName}}",
"admin_logout": "Admin {{actorName}} logged out",
"system_activity": "System activity: {{type}}",
"unknown": "Unknown activity"
} }
}, },
"acceptInvitation": { "acceptInvitation": {
@@ -1799,7 +1901,13 @@
}, },
"storageDestination": "Storage Destination", "storageDestination": "Storage Destination",
"backupType": "{{type}} backup", "backupType": "{{type}} backup",
"noDestinationSet": "No destination set" "noDestinationSet": "No destination set",
"healthStatus": {
"excellent": "Excellent",
"good": "Good",
"warning": "Warning",
"critical": "Critical"
}
}, },
"configuration": { "configuration": {
"enableBackup": "Enable Automated Backups", "enableBackup": "Enable Automated Backups",
@@ -2254,7 +2362,14 @@
"hasComments": "Has comments", "hasComments": "Has comments",
"showingPhotos": "Total photos", "showingPhotos": "Total photos",
"withRatings": "With ratings", "withRatings": "With ratings",
"combineWith": "Combine with" "combineWith": "Combine with",
"allPhotos": "All Photos",
"anyRating": "Any Rating",
"oneStarPlus": "1+ Stars",
"twoStarsPlus": "2+ Stars",
"threeStarsPlus": "3+ Stars",
"fourStarsPlus": "4+ Stars",
"fiveStarsOnly": "5 Stars Only"
}, },
"adminLogin": { "adminLogin": {
"title": "Admin Login", "title": "Admin Login",
+121 -6
View File
@@ -161,7 +161,14 @@
"navigation": { "navigation": {
"dashboard": "Tableau de bord", "dashboard": "Tableau de bord",
"events": "Événements", "events": "Événements",
"settings": "Paramètres" "settings": "Paramètres",
"archives": "Archives",
"emailSettings": "Paramètres e-mail",
"branding": "Identité visuelle",
"eventTypes": "Types d'événements",
"backup": "Sauvegarde et Restauration",
"cmsPages": "Pages CMS",
"users": "Utilisateurs"
}, },
"archives": { "archives": {
"title": "Archives", "title": "Archives",
@@ -1307,7 +1314,37 @@
"noTemplate": "Aucun modèle", "noTemplate": "Aucun modèle",
"noTemplateDescription": "Utiliser uniquement les paramètres de thème sans modèle CSS", "noTemplateDescription": "Utiliser uniquement les paramètres de thème sans modèle CSS",
"templateSlot": "Emplacement {{slot}}", "templateSlot": "Emplacement {{slot}}",
"eventCustomCSS": "CSS personnalisé par événement" "eventCustomCSS": "CSS personnalisé par événement",
"layoutDescriptions": {
"grid": "Disposition en grille classique avec tailles de photos uniformes",
"masonry": "Disposition de type Pinterest avec hauteurs variées",
"carousel": "Diaporama plein écran avec navigation",
"timeline": "Photos organisées par date",
"mosaic": "Disposition artistique avec tailles mixtes",
"gallery-premium": "Thème clair élégant avec hero et masonry (Bêta)",
"gallery-story": "Thème sombre cinématographique avec sections de scènes (Bêta)"
},
"headerStyleOptions": {
"hero": "Image Hero",
"standard": "Standard",
"banner": "Bannière",
"minimal": "Minimal",
"none": "Pas d'en-tête"
},
"headerStyleDescriptions": {
"hero": "Image pleine hauteur avec superposition d'informations sur l'événement",
"standard": "En-tête compact avec détails de l'événement",
"banner": "En-tête standard avec bannière colorée au-dessus",
"minimal": "En-tête compact avec informations essentielles",
"none": "Masquer complètement l'en-tête"
},
"dividerOptions": {
"none": "Aucun",
"wave": "Vague",
"straight": "Droit",
"angle": "Angle",
"curve": "Courbe"
}
}, },
"admin": { "admin": {
"title": "Panneau d'administration", "title": "Panneau d'administration",
@@ -1321,7 +1358,10 @@
"archivedEvents": "Événements archivés", "archivedEvents": "Événements archivés",
"systemHealth": "Santé système", "systemHealth": "Santé système",
"health": { "health": {
"checking": "Vérification..." "checking": "Vérification...",
"healthy": "Sain",
"warning": "Avertissement",
"error": "Erreur"
}, },
"updates": { "updates": {
"available": "Mise à jour disponible", "available": "Mise à jour disponible",
@@ -1475,7 +1515,17 @@
"loadingDetail": "Chargement des sélections…", "loadingDetail": "Chargement des sélections…",
"detail": { "detail": {
"noComments": "Aucun commentaire", "noComments": "Aucun commentaire",
"empty": "Aucune sélection dans cette catégorie" "empty": "Aucune sélection dans cette catégorie",
"all": "Tout",
"liked": "Aimé",
"favorited": "Favori",
"rated": "Noté",
"commented": "Commenté"
},
"inviteStatus": {
"pending": "En attente",
"redeemed": "Utilisé",
"revoked": "Révoqué"
} }
}, },
"photos": { "photos": {
@@ -1487,6 +1537,58 @@
"processingStatus": "En cours…", "processingStatus": "En cours…",
"processingFailed": "Échoué", "processingFailed": "Échoué",
"retryQueued": "Nouvelle tentative en file d'attente" "retryQueued": "Nouvelle tentative en file d'attente"
},
"analytics": "Analytique",
"activities": {
"event_created": "Nouvel événement créé : {{eventName}}",
"photos_uploaded": "{{count}} photos téléversées vers {{eventName}}",
"event_archived": "Événement archivé : {{eventName}}",
"archive_restored": "Archive restaurée : {{eventName}}",
"archive_deleted": "Archive supprimée : {{eventName}}",
"archive_downloaded": "Archive téléchargée : {{eventName}}",
"email_config_updated": "Configuration e-mail mise à jour",
"email_template_updated": "Modèle e-mail mis à jour : {{template}}",
"branding_updated": "Paramètres d'identité visuelle mis à jour",
"theme_updated": "Paramètres de thème mis à jour",
"bulk_download": "{{count}} photos téléchargées depuis {{eventName}}",
"gallery_password_entry": "Mot de passe saisi pour {{eventName}}",
"expiration_warning_viewed": "Avertissement d'expiration consulté pour {{eventName}}",
"event_updated": "Événement mis à jour : {{eventName}}",
"event_deleted": "Événement supprimé : {{eventName}}",
"event_activated": "Événement activé : {{eventName}}",
"event_deactivated": "Événement désactivé : {{eventName}}",
"bulk_archive_completed": "Archivage en masse terminé",
"photo_deleted": "Photo supprimée de {{eventName}}",
"photos_bulk_deleted": "{{count}} photos supprimées de {{eventName}}",
"password_changed": "Mot de passe modifié",
"admin_logout": "Admin {{actorName}} déconnecté",
"category_created": "Catégorie créée : {{categoryName}}",
"category_updated": "Catégorie mise à jour : {{categoryName}}",
"category_deleted": "Catégorie supprimée : {{categoryName}}",
"feedback_settings_updated": "Paramètres d'avis mis à jour",
"feedback_moderated": "Avis modéré",
"feedback_deleted": "Avis supprimé",
"word_filter_added": "Filtre de mots ajouté",
"cms_page_updated": "Page CMS mise à jour : {{page}}",
"email_resent": "E-mail de création renvoyé pour : {{eventName}}",
"settings_updated": "Paramètres mis à jour",
"favicon_uploaded": "Favicon téléversé",
"password_reset": "Mot de passe réinitialisé pour : {{eventName}}",
"external_import_completed": "Importation de médias externes terminée ({{imported}} importés, {{skipped}} ignorés)",
"photo_like": "Photo aimée dans {{eventName}}",
"photo_favorite": "Photo ajoutée aux favoris dans {{eventName}}",
"general_settings_updated": "Paramètres généraux mis à jour",
"analytics_settings_updated": "Paramètres d'analytique mis à jour",
"security_settings_updated": "Paramètres de sécurité mis à jour",
"event_renamed": "Événement renommé : {{eventName}}",
"photo_rating": "Photo notée dans {{eventName}}",
"photo_comment": "Photo commentée dans {{eventName}}",
"guest_feedback_like": "Un invité a aimé une photo dans {{eventName}}",
"guest_feedback_favorite": "Un invité a mis en favori une photo dans {{eventName}}",
"guest_feedback_rating": "Un invité a noté une photo dans {{eventName}}",
"guest_feedback_comment": "Un invité a commenté une photo dans {{eventName}}",
"system_activity": "Activité système : {{type}}",
"unknown": "Activité inconnue"
} }
}, },
"acceptInvitation": { "acceptInvitation": {
@@ -1819,7 +1921,13 @@
}, },
"storageDestination": "Destination", "storageDestination": "Destination",
"backupType": "Sauvegarde {{type}}", "backupType": "Sauvegarde {{type}}",
"noDestinationSet": "Pas de destination" "noDestinationSet": "Pas de destination",
"healthStatus": {
"warning": "Avertissement",
"excellent": "Excellent",
"good": "Bon",
"critical": "Critique"
}
}, },
"configuration": { "configuration": {
"enableBackup": "Activer les sauvegardes auto", "enableBackup": "Activer les sauvegardes auto",
@@ -2279,7 +2387,14 @@
"hasComments": "Avec Commentaires", "hasComments": "Avec Commentaires",
"combineWith": "Combiner avec", "combineWith": "Combiner avec",
"showingPhotos": "Total photos", "showingPhotos": "Total photos",
"withRatings": "Avec notes" "withRatings": "Avec notes",
"allPhotos": "Toutes les photos",
"anyRating": "Toute note",
"oneStarPlus": "1+ étoile",
"twoStarsPlus": "2+ étoiles",
"threeStarsPlus": "3+ étoiles",
"fourStarsPlus": "4+ étoiles",
"fiveStarsOnly": "5 étoiles uniquement"
}, },
"adminLogin": { "adminLogin": {
"title": "Connexion Admin", "title": "Connexion Admin",
+121 -6
View File
@@ -158,7 +158,14 @@
"navigation": { "navigation": {
"dashboard": "Dashboard", "dashboard": "Dashboard",
"events": "Evenementen", "events": "Evenementen",
"settings": "Instellingen" "settings": "Instellingen",
"archives": "Archieven",
"emailSettings": "E-mailinstellingen",
"branding": "Huisstijl",
"eventTypes": "Evenementtypes",
"backup": "Back-up en herstel",
"cmsPages": "CMS-pagina's",
"users": "Gebruikers"
}, },
"archives": { "archives": {
"title": "Archieven", "title": "Archieven",
@@ -1295,7 +1302,37 @@
"noTemplate": "Geen sjabloon", "noTemplate": "Geen sjabloon",
"noTemplateDescription": "Alleen thema-instellingen gebruiken zonder CSS-sjabloon", "noTemplateDescription": "Alleen thema-instellingen gebruiken zonder CSS-sjabloon",
"templateSlot": "Slot {{slot}}", "templateSlot": "Slot {{slot}}",
"eventCustomCSS": "Evenement-specifieke aangepaste CSS" "eventCustomCSS": "Evenement-specifieke aangepaste CSS",
"layoutDescriptions": {
"grid": "Klassieke rasterindeling met uniforme fotogroottes",
"masonry": "Pinterest-stijl indeling met variabele hoogtes",
"carousel": "Volledig scherm diashow met navigatie",
"timeline": "Foto's gerangschikt op datum",
"mosaic": "Artistieke indeling met gemengde groottes",
"gallery-premium": "Elegant licht thema met hero en masonry (Beta)",
"gallery-story": "Cinematisch donker thema met scènesecties (Beta)"
},
"headerStyleOptions": {
"hero": "Hero-afbeelding",
"standard": "Standaard",
"banner": "Banner",
"minimal": "Minimaal",
"none": "Geen koptekst"
},
"headerStyleDescriptions": {
"hero": "Volledige hoogte afbeelding met evenementinfo overlay",
"standard": "Compacte inline koptekst met evenementdetails",
"banner": "Standaard koptekst plus een gekleurde banner erboven",
"minimal": "Compacte koptekst met essentiële info",
"none": "Koptekst volledig verbergen"
},
"dividerOptions": {
"none": "Geen",
"wave": "Golf",
"straight": "Recht",
"angle": "Hoek",
"curve": "Boog"
}
}, },
"admin": { "admin": {
"title": "Beheerpaneel", "title": "Beheerpaneel",
@@ -1309,7 +1346,10 @@
"archivedEvents": "Gearchiveerde evenementen", "archivedEvents": "Gearchiveerde evenementen",
"systemHealth": "Systeemgezondheid", "systemHealth": "Systeemgezondheid",
"health": { "health": {
"checking": "Controleren..." "checking": "Controleren...",
"healthy": "Gezond",
"warning": "Waarschuwing",
"error": "Fout"
}, },
"updates": { "updates": {
"available": "Update beschikbaar", "available": "Update beschikbaar",
@@ -1465,8 +1505,70 @@
"loadingDetail": "Selecties laden…", "loadingDetail": "Selecties laden…",
"detail": { "detail": {
"noComments": "Geen opmerkingen", "noComments": "Geen opmerkingen",
"empty": "Geen selecties in deze categorie" "empty": "Geen selecties in deze categorie",
"all": "Alle",
"liked": "Geliked",
"favorited": "Favoriet",
"rated": "Beoordeeld",
"commented": "Becommentarieerd"
},
"inviteStatus": {
"pending": "In behandeling",
"redeemed": "Ingewisseld",
"revoked": "Ingetrokken"
} }
},
"analytics": "Analyse",
"activities": {
"event_created": "Nieuw evenement aangemaakt: {{eventName}}",
"photos_uploaded": "{{count}} foto's geüpload naar {{eventName}}",
"event_archived": "Evenement gearchiveerd: {{eventName}}",
"archive_restored": "Archief hersteld: {{eventName}}",
"archive_deleted": "Archief verwijderd: {{eventName}}",
"archive_downloaded": "Archief gedownload: {{eventName}}",
"email_config_updated": "E-mailconfiguratie bijgewerkt",
"email_template_updated": "E-mailsjabloon bijgewerkt: {{template}}",
"branding_updated": "Huisstijlinstellingen bijgewerkt",
"theme_updated": "Thema-instellingen bijgewerkt",
"bulk_download": "{{count}} foto's gedownload van {{eventName}}",
"gallery_password_entry": "Wachtwoord ingevoerd voor {{eventName}}",
"expiration_warning_viewed": "Verloopwaarschuwing bekeken voor {{eventName}}",
"event_updated": "Evenement bijgewerkt: {{eventName}}",
"event_deleted": "Evenement verwijderd: {{eventName}}",
"event_activated": "Evenement geactiveerd: {{eventName}}",
"event_deactivated": "Evenement gedeactiveerd: {{eventName}}",
"bulk_archive_completed": "Bulkarchivering voltooid",
"photo_deleted": "Foto verwijderd van {{eventName}}",
"photos_bulk_deleted": "{{count}} foto's verwijderd van {{eventName}}",
"password_changed": "Wachtwoord gewijzigd",
"admin_logout": "Beheerder {{actorName}} uitgelogd",
"category_created": "Categorie aangemaakt: {{categoryName}}",
"category_updated": "Categorie bijgewerkt: {{categoryName}}",
"category_deleted": "Categorie verwijderd: {{categoryName}}",
"feedback_settings_updated": "Feedbackinstellingen bijgewerkt",
"feedback_moderated": "Feedback gemodereerd",
"feedback_deleted": "Feedback verwijderd",
"word_filter_added": "Woordfilter toegevoegd",
"cms_page_updated": "CMS-pagina bijgewerkt: {{page}}",
"email_resent": "Aanmaak-e-mail opnieuw verzonden voor: {{eventName}}",
"settings_updated": "Instellingen bijgewerkt",
"favicon_uploaded": "Favicon geüpload",
"password_reset": "Wachtwoord gereset voor: {{eventName}}",
"external_import_completed": "Externe media-import voltooid ({{imported}} geïmporteerd, {{skipped}} overgeslagen)",
"photo_like": "Foto geliked in {{eventName}}",
"photo_favorite": "Foto als favoriet gemarkeerd in {{eventName}}",
"general_settings_updated": "Algemene instellingen bijgewerkt",
"analytics_settings_updated": "Analyse-instellingen bijgewerkt",
"security_settings_updated": "Beveiligingsinstellingen bijgewerkt",
"event_renamed": "Evenement hernoemd: {{eventName}}",
"photo_rating": "Foto beoordeeld in {{eventName}}",
"photo_comment": "Foto becommentarieerd in {{eventName}}",
"guest_feedback_like": "Gast heeft een foto geliked in {{eventName}}",
"guest_feedback_favorite": "Gast heeft een foto als favoriet gemarkeerd in {{eventName}}",
"guest_feedback_rating": "Gast heeft een foto beoordeeld in {{eventName}}",
"guest_feedback_comment": "Gast heeft een opmerking geplaatst op een foto in {{eventName}}",
"system_activity": "Systeemactiviteit: {{type}}",
"unknown": "Onbekende activiteit"
} }
}, },
"acceptInvitation": { "acceptInvitation": {
@@ -1799,7 +1901,13 @@
}, },
"storageDestination": "Opslagbestemming", "storageDestination": "Opslagbestemming",
"backupType": "{{type}} back-up", "backupType": "{{type}} back-up",
"noDestinationSet": "Geen bestemming ingesteld" "noDestinationSet": "Geen bestemming ingesteld",
"healthStatus": {
"warning": "Waarschuwing",
"excellent": "Uitstekend",
"good": "Goed",
"critical": "Kritiek"
}
}, },
"configuration": { "configuration": {
"enableBackup": "Geautomatiseerde back-ups inschakelen", "enableBackup": "Geautomatiseerde back-ups inschakelen",
@@ -2254,7 +2362,14 @@
"hasComments": "Heeft opmerkingen", "hasComments": "Heeft opmerkingen",
"showingPhotos": "Totaal foto's", "showingPhotos": "Totaal foto's",
"withRatings": "Met beoordelingen", "withRatings": "Met beoordelingen",
"combineWith": "Combineren met" "combineWith": "Combineren met",
"allPhotos": "Alle foto's",
"anyRating": "Elke beoordeling",
"oneStarPlus": "1+ ster",
"twoStarsPlus": "2+ sterren",
"threeStarsPlus": "3+ sterren",
"fourStarsPlus": "4+ sterren",
"fiveStarsOnly": "Alleen 5 sterren"
}, },
"export": { "export": {
"button": "Exporteren", "button": "Exporteren",
+121 -6
View File
@@ -161,7 +161,14 @@
"navigation": { "navigation": {
"dashboard": "Painel", "dashboard": "Painel",
"events": "Eventos", "events": "Eventos",
"settings": "Configurações" "settings": "Configurações",
"archives": "Arquivos",
"emailSettings": "Configurações de e-mail",
"branding": "Identidade visual",
"eventTypes": "Tipos de evento",
"backup": "Backup e Restauração",
"cmsPages": "Páginas CMS",
"users": "Utilizadores"
}, },
"archives": { "archives": {
"title": "Arquivos", "title": "Arquivos",
@@ -1312,7 +1319,37 @@
"noTemplate": "Sem modelo", "noTemplate": "Sem modelo",
"noTemplateDescription": "Usar apenas as definições de tema sem modelo CSS", "noTemplateDescription": "Usar apenas as definições de tema sem modelo CSS",
"templateSlot": "Posição {{slot}}", "templateSlot": "Posição {{slot}}",
"eventCustomCSS": "CSS personalizado por evento" "eventCustomCSS": "CSS personalizado por evento",
"layoutDescriptions": {
"grid": "Grelha clássica com tamanhos de foto uniformes",
"masonry": "Estilo Pinterest com alturas variadas",
"carousel": "Apresentação a ecrã inteiro com navegação",
"timeline": "Fotos organizadas por data",
"mosaic": "Disposição artística com tamanhos mistos",
"gallery-premium": "Tema claro elegante com hero e masonry (Beta)",
"gallery-story": "Tema escuro cinemático com secções de cena (Beta)"
},
"headerStyleOptions": {
"hero": "Imagem Hero",
"standard": "Standard",
"banner": "Banner",
"minimal": "Mínimo",
"none": "Sem cabeçalho"
},
"headerStyleDescriptions": {
"hero": "Imagem de altura total com sobreposição de info do evento",
"standard": "Cabeçalho compacto com detalhes do evento",
"banner": "Cabeçalho standard com banner colorido acima",
"minimal": "Cabeçalho compacto com informações essenciais",
"none": "Ocultar cabeçalho completamente"
},
"dividerOptions": {
"none": "Nenhum",
"wave": "Onda",
"straight": "Reto",
"angle": "Ângulo",
"curve": "Curva"
}
}, },
"admin": { "admin": {
"title": "Painel Admin", "title": "Painel Admin",
@@ -1326,7 +1363,10 @@
"archivedEvents": "Eventos Arquivados", "archivedEvents": "Eventos Arquivados",
"systemHealth": "Saúde do Sistema", "systemHealth": "Saúde do Sistema",
"health": { "health": {
"checking": "Verificando..." "checking": "Verificando...",
"healthy": "Saudável",
"warning": "Aviso",
"error": "Erro"
}, },
"updates": { "updates": {
"available": "Atualização Disponível", "available": "Atualização Disponível",
@@ -1490,8 +1530,70 @@
"loadingDetail": "A carregar seleções…", "loadingDetail": "A carregar seleções…",
"detail": { "detail": {
"noComments": "Sem comentários", "noComments": "Sem comentários",
"empty": "Sem seleções nesta categoria" "empty": "Sem seleções nesta categoria",
"all": "Todos",
"liked": "Gostou",
"favorited": "Favorito",
"rated": "Avaliado",
"commented": "Comentado"
},
"inviteStatus": {
"pending": "Pendente",
"redeemed": "Utilizado",
"revoked": "Revogado"
} }
},
"analytics": "Análise",
"activities": {
"event_created": "Novo evento criado: {{eventName}}",
"photos_uploaded": "{{count}} fotos carregadas para {{eventName}}",
"event_archived": "Evento arquivado: {{eventName}}",
"archive_restored": "Arquivo restaurado: {{eventName}}",
"archive_deleted": "Arquivo eliminado: {{eventName}}",
"archive_downloaded": "Arquivo descarregado: {{eventName}}",
"email_config_updated": "Configuração de e-mail atualizada",
"email_template_updated": "Modelo de e-mail atualizado: {{template}}",
"branding_updated": "Definições de identidade visual atualizadas",
"theme_updated": "Definições de tema atualizadas",
"bulk_download": "{{count}} fotos descarregadas de {{eventName}}",
"gallery_password_entry": "Palavra-passe inserida para {{eventName}}",
"expiration_warning_viewed": "Aviso de expiração visualizado para {{eventName}}",
"event_updated": "Evento atualizado: {{eventName}}",
"event_deleted": "Evento eliminado: {{eventName}}",
"event_activated": "Evento ativado: {{eventName}}",
"event_deactivated": "Evento desativado: {{eventName}}",
"bulk_archive_completed": "Arquivamento em massa concluído",
"photo_deleted": "Foto eliminada de {{eventName}}",
"photos_bulk_deleted": "{{count}} fotos eliminadas de {{eventName}}",
"password_changed": "Palavra-passe alterada",
"admin_logout": "Admin {{actorName}} terminou sessão",
"category_created": "Categoria criada: {{categoryName}}",
"category_updated": "Categoria atualizada: {{categoryName}}",
"category_deleted": "Categoria eliminada: {{categoryName}}",
"feedback_settings_updated": "Definições de feedback atualizadas",
"feedback_moderated": "Feedback moderado",
"feedback_deleted": "Feedback eliminado",
"word_filter_added": "Filtro de palavras adicionado",
"cms_page_updated": "Página CMS atualizada: {{page}}",
"email_resent": "E-mail de criação reenviado para: {{eventName}}",
"settings_updated": "Definições atualizadas",
"favicon_uploaded": "Favicon carregado",
"password_reset": "Palavra-passe redefinida para: {{eventName}}",
"external_import_completed": "Importação de média externo concluída ({{imported}} importados, {{skipped}} ignorados)",
"photo_like": "Foto curtida em {{eventName}}",
"photo_favorite": "Foto adicionada aos favoritos em {{eventName}}",
"general_settings_updated": "Definições gerais atualizadas",
"analytics_settings_updated": "Definições de análise atualizadas",
"security_settings_updated": "Definições de segurança atualizadas",
"event_renamed": "Evento renomeado: {{eventName}}",
"photo_rating": "Foto avaliada em {{eventName}}",
"photo_comment": "Foto comentada em {{eventName}}",
"guest_feedback_like": "Convidado curtiu uma foto em {{eventName}}",
"guest_feedback_favorite": "Convidado adicionou uma foto aos favoritos em {{eventName}}",
"guest_feedback_rating": "Convidado avaliou uma foto em {{eventName}}",
"guest_feedback_comment": "Convidado comentou uma foto em {{eventName}}",
"system_activity": "Atividade do sistema: {{type}}",
"unknown": "Atividade desconhecida"
} }
}, },
"acceptInvitation": { "acceptInvitation": {
@@ -1824,7 +1926,13 @@
}, },
"storageDestination": "Destino do Armazenamento", "storageDestination": "Destino do Armazenamento",
"backupType": "Backup {{type}}", "backupType": "Backup {{type}}",
"noDestinationSet": "Nenhum destino definido" "noDestinationSet": "Nenhum destino definido",
"healthStatus": {
"warning": "Aviso",
"excellent": "Excelente",
"good": "Bom",
"critical": "Crítico"
}
}, },
"configuration": { "configuration": {
"enableBackup": "Ativar Backups Automáticos", "enableBackup": "Ativar Backups Automáticos",
@@ -2284,7 +2392,14 @@
"hasComments": "Possui comentários", "hasComments": "Possui comentários",
"showingPhotos": "Total de fotos", "showingPhotos": "Total de fotos",
"withRatings": "Com avaliações", "withRatings": "Com avaliações",
"combineWith": "Combinar com" "combineWith": "Combinar com",
"allPhotos": "Todas as fotos",
"anyRating": "Qualquer avaliação",
"oneStarPlus": "1+ estrela",
"twoStarsPlus": "2+ estrelas",
"threeStarsPlus": "3+ estrelas",
"fourStarsPlus": "4+ estrelas",
"fiveStarsOnly": "Apenas 5 estrelas"
}, },
"adminLogin": { "adminLogin": {
"title": "Login Administrativo", "title": "Login Administrativo",
+121 -6
View File
@@ -164,7 +164,14 @@
"navigation": { "navigation": {
"dashboard": "Панель управления", "dashboard": "Панель управления",
"events": "События", "events": "События",
"settings": "Настройки" "settings": "Настройки",
"archives": "Архивы",
"emailSettings": "Настройки e-mail",
"branding": "Брендинг",
"eventTypes": "Типы событий",
"backup": "Резервное копирование",
"cmsPages": "CMS-страницы",
"users": "Пользователи"
}, },
"archives": { "archives": {
"title": "Архивы", "title": "Архивы",
@@ -1329,7 +1336,37 @@
"noTemplate": "Без шаблона", "noTemplate": "Без шаблона",
"noTemplateDescription": "Использовать только настройки темы без CSS-шаблона", "noTemplateDescription": "Использовать только настройки темы без CSS-шаблона",
"templateSlot": "Слот {{slot}}", "templateSlot": "Слот {{slot}}",
"eventCustomCSS": "Пользовательский CSS для события" "eventCustomCSS": "Пользовательский CSS для события",
"layoutDescriptions": {
"grid": "Классическая сетка с одинаковыми размерами фото",
"masonry": "Макет в стиле Pinterest с разной высотой",
"carousel": "Полноэкранное слайд-шоу с навигацией",
"timeline": "Фото, упорядоченные по дате",
"mosaic": "Художественный макет с разными размерами",
"gallery-premium": "Элегантная светлая тема с hero и masonry (Бета)",
"gallery-story": "Кинематографическая тёмная тема с разделами сцен (Бета)"
},
"headerStyleOptions": {
"hero": "Изображение Hero",
"standard": "Стандартный",
"banner": "Баннер",
"minimal": "Минимальный",
"none": "Без заголовка"
},
"headerStyleDescriptions": {
"hero": "Изображение в полную высоту с наложением информации о событии",
"standard": "Компактный встроенный заголовок с деталями события",
"banner": "Стандартный заголовок плюс цветной баннер сверху",
"minimal": "Компактный заголовок с основной информацией",
"none": "Полностью скрыть заголовок"
},
"dividerOptions": {
"none": "Нет",
"wave": "Волна",
"straight": "Прямой",
"angle": "Угол",
"curve": "Кривая"
}
}, },
"admin": { "admin": {
"title": "Панель администратора", "title": "Панель администратора",
@@ -1343,7 +1380,10 @@
"archivedEvents": "Архивированных событий", "archivedEvents": "Архивированных событий",
"systemHealth": "Состояние системы", "systemHealth": "Состояние системы",
"health": { "health": {
"checking": "Проверка..." "checking": "Проверка...",
"healthy": "Исправно",
"warning": "Предупреждение",
"error": "Ошибка"
}, },
"updates": { "updates": {
"available": "Доступно обновление", "available": "Доступно обновление",
@@ -1515,8 +1555,70 @@
"loadingDetail": "Загрузка выборок…", "loadingDetail": "Загрузка выборок…",
"detail": { "detail": {
"noComments": "Нет комментариев", "noComments": "Нет комментариев",
"empty": "Нет выборок в этой категории" "empty": "Нет выборок в этой категории",
"all": "Все",
"liked": "Понравилось",
"favorited": "В избранном",
"rated": "Оценено",
"commented": "Прокомментировано"
},
"inviteStatus": {
"pending": "Ожидает",
"redeemed": "Использовано",
"revoked": "Отозвано"
} }
},
"analytics": "Аналитика",
"activities": {
"event_created": "Новое событие создано: {{eventName}}",
"photos_uploaded": "{{count}} фото загружено в {{eventName}}",
"event_archived": "Событие архивировано: {{eventName}}",
"archive_restored": "Архив восстановлен: {{eventName}}",
"archive_deleted": "Архив удалён: {{eventName}}",
"archive_downloaded": "Архив скачан: {{eventName}}",
"email_config_updated": "Конфигурация e-mail обновлена",
"email_template_updated": "Шаблон e-mail обновлён: {{template}}",
"branding_updated": "Настройки брендинга обновлены",
"theme_updated": "Настройки темы обновлены",
"bulk_download": "{{count}} фото скачано из {{eventName}}",
"gallery_password_entry": "Введён пароль для {{eventName}}",
"expiration_warning_viewed": "Предупреждение об истечении просмотрено для {{eventName}}",
"event_updated": "Событие обновлено: {{eventName}}",
"event_deleted": "Событие удалено: {{eventName}}",
"event_activated": "Событие активировано: {{eventName}}",
"event_deactivated": "Событие деактивировано: {{eventName}}",
"bulk_archive_completed": "Массовое архивирование завершено",
"photo_deleted": "Фото удалено из {{eventName}}",
"photos_bulk_deleted": "{{count}} фото удалено из {{eventName}}",
"password_changed": "Пароль изменён",
"admin_logout": "Администратор {{actorName}} вышел из системы",
"category_created": "Категория создана: {{categoryName}}",
"category_updated": "Категория обновлена: {{categoryName}}",
"category_deleted": "Категория удалена: {{categoryName}}",
"feedback_settings_updated": "Настройки отзывов обновлены",
"feedback_moderated": "Отзыв промодерирован",
"feedback_deleted": "Отзыв удалён",
"word_filter_added": "Фильтр слов добавлен",
"cms_page_updated": "Страница CMS обновлена: {{page}}",
"email_resent": "E-mail создания повторно отправлен для: {{eventName}}",
"settings_updated": "Настройки обновлены",
"favicon_uploaded": "Favicon загружен",
"password_reset": "Пароль сброшен для: {{eventName}}",
"external_import_completed": "Импорт внешних медиафайлов завершён ({{imported}} импортировано, {{skipped}} пропущено)",
"photo_like": "Фото понравилось в {{eventName}}",
"photo_favorite": "Фото добавлено в избранное в {{eventName}}",
"general_settings_updated": "Общие настройки обновлены",
"analytics_settings_updated": "Настройки аналитики обновлены",
"security_settings_updated": "Настройки безопасности обновлены",
"event_renamed": "Событие переименовано: {{eventName}}",
"photo_rating": "Фото оценено в {{eventName}}",
"photo_comment": "Фото прокомментировано в {{eventName}}",
"guest_feedback_like": "Гость поставил лайк фото в {{eventName}}",
"guest_feedback_favorite": "Гость добавил фото в избранное в {{eventName}}",
"guest_feedback_rating": "Гость оценил фото в {{eventName}}",
"guest_feedback_comment": "Гость прокомментировал фото в {{eventName}}",
"system_activity": "Системная активность: {{type}}",
"unknown": "Неизвестная активность"
} }
}, },
"acceptInvitation": { "acceptInvitation": {
@@ -1849,7 +1951,13 @@
}, },
"storageDestination": "Место хранения", "storageDestination": "Место хранения",
"backupType": "Резервная копия {{type}}", "backupType": "Резервная копия {{type}}",
"noDestinationSet": "Место хранения не задано" "noDestinationSet": "Место хранения не задано",
"healthStatus": {
"warning": "Предупреждение",
"excellent": "Отлично",
"good": "Хорошо",
"critical": "Критически"
}
}, },
"configuration": { "configuration": {
"enableBackup": "Включить автоматическое резервное копирование", "enableBackup": "Включить автоматическое резервное копирование",
@@ -2314,7 +2422,14 @@
"hasComments": "Есть комментарии", "hasComments": "Есть комментарии",
"showingPhotos": "Всего фото", "showingPhotos": "Всего фото",
"withRatings": "С оценками", "withRatings": "С оценками",
"combineWith": "Объединить с" "combineWith": "Объединить с",
"allPhotos": "Все фото",
"anyRating": "Любая оценка",
"oneStarPlus": "1+ звезда",
"twoStarsPlus": "2+ звезды",
"threeStarsPlus": "3+ звезды",
"fourStarsPlus": "4+ звезды",
"fiveStarsOnly": "Только 5 звёзд"
}, },
"adminLogin": { "adminLogin": {
"title": "Вход для администратора", "title": "Вход для администратора",
+6 -7
View File
@@ -20,7 +20,7 @@ import { Button, Card, Loading } from '../../components/common';
import { UpdateNotification } from '../../components/admin/UpdateNotification'; import { UpdateNotification } from '../../components/admin/UpdateNotification';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { eventsService } from '../../services/events.service'; import { eventsService } from '../../services/events.service';
import { adminService } from '../../services/admin.service'; import { adminService, ActivityType } from '../../services/admin.service';
interface StatCard { interface StatCard {
title: string; title: string;
@@ -246,8 +246,8 @@ export const AdminDashboard: React.FC = () => {
) : ( ) : (
recentActivity.slice(0, 5).map((activity) => { recentActivity.slice(0, 5).map((activity) => {
// Get color based on activity type // Get color based on activity type
const getActivityColor = (type: string) => { const getActivityColor = (type: ActivityType) => {
const colors: Record<string, string> = { const colors: Partial<Record<ActivityType, string>> = {
'event_created': 'bg-green-500', 'event_created': 'bg-green-500',
'photos_uploaded': 'bg-blue-500', 'photos_uploaded': 'bg-blue-500',
'event_archived': 'bg-purple-500', 'event_archived': 'bg-purple-500',
@@ -264,21 +264,20 @@ export const AdminDashboard: React.FC = () => {
// Format activity message with translations // Format activity message with translations
const getActivityMessage = (): string => { const getActivityMessage = (): string => {
const translationKey = `admin.activities.${activity.type}`;
const params: Record<string, any> = { const params: Record<string, any> = {
eventName: activity.eventName || t('common.unknown'), eventName: activity.eventName || t('common.unknown'),
count: activity.metadata?.count || 0, count: activity.metadata?.count || 0,
template: activity.metadata?.template_key || '', template: activity.metadata?.template_key || '',
categoryName: activity.metadata?.category_name || '' categoryName: activity.metadata?.category_name || ''
}; };
const translated = t(`admin.activities.${activity.type}`, params);
// Translate; if key missing i18n returns the key string itself // Translate; if key missing i18n returns the key string itself
const translated = t(translationKey, params) as string; if (!translated || translated === `admin.activities.${activity.type}`) {
if (!translated || translated === translationKey) {
// Fallback: format a readable English message // Fallback: format a readable English message
return adminService.formatActivityMessage(activity); return adminService.formatActivityMessage(activity);
} }
return translated; return translated as string;
}; };
return ( return (
-3
View File
@@ -1,3 +0,0 @@
import type { ComponentType } from 'react';
export const BackupManagement: ComponentType<any>;
@@ -7,20 +7,9 @@ import {
RefreshCw, RefreshCw,
AlertCircle, AlertCircle,
CheckCircle, CheckCircle,
Calendar,
Database,
FileArchive,
Cloud,
Server,
Loader2,
Info,
Shield,
Clock, Clock,
Download, Loader2,
Upload, Shield,
Trash2,
Search,
Filter
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
@@ -34,41 +23,37 @@ import { BackupHistory } from '../../components/admin/BackupHistory';
import { RestoreWizard } from '../../components/admin/RestoreWizard'; import { RestoreWizard } from '../../components/admin/RestoreWizard';
import { api } from '../../config/api'; import { api } from '../../config/api';
// Tab components will be defined inside the component to use translations type TabId = 'dashboard' | 'configuration' | 'history' | 'restore';
export const BackupManagement = () => { export const BackupManagement: React.FC = () => {
const [activeTab, setActiveTab] = useState('dashboard'); const [activeTab, setActiveTab] = useState<TabId>('dashboard');
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { t } = useTranslation(); const { t } = useTranslation();
// Tab components with translations
const tabs = [ const tabs = [
{ id: 'dashboard', label: t('backup.tabs.dashboard'), icon: HardDrive }, { id: 'dashboard' as const, label: t('backup.tabs.dashboard'), icon: HardDrive },
{ id: 'configuration', label: t('backup.tabs.configuration'), icon: Settings }, { id: 'configuration' as const, label: t('backup.tabs.configuration'), icon: Settings },
{ id: 'history', label: t('backup.tabs.history'), icon: History }, { id: 'history' as const, label: t('backup.tabs.history'), icon: History },
{ id: 'restore', label: t('backup.tabs.restore'), icon: RefreshCw } { id: 'restore' as const, label: t('backup.tabs.restore'), icon: RefreshCw },
]; ];
// Fetch backup status
const { data: backupStatus, isLoading: statusLoading } = useQuery({ const { data: backupStatus, isLoading: statusLoading } = useQuery({
queryKey: ['backup-status'], queryKey: ['backup-status'],
queryFn: async () => { queryFn: async () => {
const response = await api.get('/admin/backup/status'); const response = await api.get('/admin/backup/status');
return response.data; return response.data;
}, },
refetchInterval: 10000 // Refresh every 10 seconds refetchInterval: 10000,
}); });
// Fetch backup configuration
const { data: backupConfig, isLoading: configLoading } = useQuery({ const { data: backupConfig, isLoading: configLoading } = useQuery({
queryKey: ['backup-config'], queryKey: ['backup-config'],
queryFn: async () => { queryFn: async () => {
const response = await api.get('/admin/backup/config'); const response = await api.get('/admin/backup/config');
return response.data; return response.data;
} },
}); });
// Trigger manual backup
const manualBackupMutation = useMutation({ const manualBackupMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
const response = await api.post('/admin/backup/run'); const response = await api.post('/admin/backup/run');
@@ -78,15 +63,14 @@ export const BackupManagement = () => {
toast.success(t('backup.messages.backupStarted')); toast.success(t('backup.messages.backupStarted'));
queryClient.invalidateQueries({ queryKey: ['backup-status'] }); queryClient.invalidateQueries({ queryKey: ['backup-status'] });
}, },
onError: (error) => { onError: (error: any) => {
const message = error.response?.data?.error || t('backup.messages.backupFailed'); const message = error.response?.data?.error || t('backup.messages.backupFailed');
toast.error(message); toast.error(message);
} },
}); });
// Update configuration
const updateConfigMutation = useMutation({ const updateConfigMutation = useMutation({
mutationFn: async (config) => { mutationFn: async (config: unknown) => {
const response = await api.put('/admin/backup/config', config); const response = await api.put('/admin/backup/config', config);
return response.data; return response.data;
}, },
@@ -94,10 +78,10 @@ export const BackupManagement = () => {
toast.success(t('backup.messages.configUpdated')); toast.success(t('backup.messages.configUpdated'));
queryClient.invalidateQueries({ queryKey: ['backup-config'] }); queryClient.invalidateQueries({ queryKey: ['backup-config'] });
}, },
onError: (error) => { onError: (error: any) => {
const message = error.response?.data?.error || t('backup.messages.configUpdateFailed'); const message = error.response?.data?.error || t('backup.messages.configUpdateFailed');
toast.error(message); toast.error(message);
} },
}); });
if (statusLoading || configLoading) { if (statusLoading || configLoading) {
@@ -156,11 +140,11 @@ export const BackupManagement = () => {
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<Button <Button
onClick={() => manualBackupMutation.mutate()} onClick={() => manualBackupMutation.mutate()}
disabled={backupStatus?.isRunning || manualBackupMutation.isLoading} disabled={backupStatus?.isRunning || manualBackupMutation.isPending}
variant="secondary" variant="secondary"
size="sm" size="sm"
> >
{manualBackupMutation.isLoading ? ( {manualBackupMutation.isPending ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('backup.actions.starting')} {t('backup.actions.starting')}
@@ -217,15 +201,15 @@ export const BackupManagement = () => {
status={backupStatus} status={backupStatus}
config={backupConfig} config={backupConfig}
onRunBackup={() => manualBackupMutation.mutate()} onRunBackup={() => manualBackupMutation.mutate()}
isBackupRunning={backupStatus?.isRunning || manualBackupMutation.isLoading} isBackupRunning={backupStatus?.isRunning || manualBackupMutation.isPending}
/> />
)} )}
{activeTab === 'configuration' && ( {activeTab === 'configuration' && (
<BackupConfiguration <BackupConfiguration
config={backupConfig} config={backupConfig}
onSave={(newConfig) => updateConfigMutation.mutate(newConfig)} onSave={(newConfig: unknown) => updateConfigMutation.mutate(newConfig)}
isSaving={updateConfigMutation.isLoading} isSaving={updateConfigMutation.isPending}
/> />
)} )}
+52 -1
View File
@@ -38,9 +38,60 @@ export interface SystemHealth {
}; };
} }
export type ActivityType =
| "event_created"
| "photos_uploaded"
| "event_archived"
| "archive_restored"
| "archive_deleted"
| "archive_downloaded"
| "email_config_updated"
| "email_template_updated"
| "branding_updated"
| "theme_updated"
| "bulk_download"
| "gallery_password_entry"
| "expiration_warning_viewed"
| "feedback_settings_updated"
| "feedback_moderated"
| "feedback_deleted"
| "photo_like"
| "photo_favorite"
| "photo_rating"
| "photo_comment"
| "guest_feedback_like"
| "guest_feedback_favorite"
| "guest_feedback_rating"
| "guest_feedback_comment"
| "word_filter_added"
| "external_import_completed"
| "bulk_archive_completed"
| "event_activated"
| "event_deactivated"
| "photo_deleted"
| "photos_bulk_deleted"
| "settings_updated"
| "event_updated"
| "event_renamed"
| "event_deleted"
| "password_changed"
| "email_resent"
| "category_created"
| "category_updated"
| "category_deleted"
| "general_settings_updated"
| "favicon_uploaded"
| "analytics_settings_updated"
| "cms_page_updated"
| "security_settings_updated"
| "password_reset"
| "admin_logout"
| "system_activity"
| "unknown"
export interface Activity { export interface Activity {
id: number; id: number;
type: string; type: ActivityType;
actorType: string; actorType: string;
actorName: string; actorName: string;
eventName?: string; eventName?: string;