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

This commit is contained in:
PiR1
2026-05-08 20:07:41 +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 { typescriptPlugin } from "./scripts/i18nextExtractionHelper";
export default defineConfig({
locales: ['en', 'de', 'nl', 'pt', 'ru', 'fr'],
@@ -18,6 +20,7 @@ export default defineConfig({
preserveContextVariants: true,
indentation: 2,
sort:false
sort: false,
},
plugins: [typescriptPlugin(["./src/App.tsx"]) ]
});
+540
View File
@@ -53,6 +53,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^25.6.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.5.3",
@@ -64,6 +65,7 @@
"globals": "^16.2.0",
"i18next-cli": "^1.56.11",
"jsdom": "^25.0.1",
"memfs": "^4.57.2",
"postcss": "^8.5.10",
"tailwindcss": "^3.3.0",
"typescript": "~5.8.3",
@@ -1618,6 +1620,436 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jsonjoy.com/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/buffers": {
"version": "17.67.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz",
"integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/codegen": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz",
"integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-core": {
"version": "4.57.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.2.tgz",
"integrity": "sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/fs-node-builtins": "4.57.2",
"@jsonjoy.com/fs-node-utils": "4.57.2",
"thingies": "^2.5.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-fsa": {
"version": "4.57.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.2.tgz",
"integrity": "sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/fs-core": "4.57.2",
"@jsonjoy.com/fs-node-builtins": "4.57.2",
"@jsonjoy.com/fs-node-utils": "4.57.2",
"thingies": "^2.5.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-node": {
"version": "4.57.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.2.tgz",
"integrity": "sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/fs-core": "4.57.2",
"@jsonjoy.com/fs-node-builtins": "4.57.2",
"@jsonjoy.com/fs-node-utils": "4.57.2",
"@jsonjoy.com/fs-print": "4.57.2",
"@jsonjoy.com/fs-snapshot": "4.57.2",
"glob-to-regex.js": "^1.0.0",
"thingies": "^2.5.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-node-builtins": {
"version": "4.57.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.2.tgz",
"integrity": "sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-node-to-fsa": {
"version": "4.57.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.2.tgz",
"integrity": "sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/fs-fsa": "4.57.2",
"@jsonjoy.com/fs-node-builtins": "4.57.2",
"@jsonjoy.com/fs-node-utils": "4.57.2"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-node-utils": {
"version": "4.57.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.2.tgz",
"integrity": "sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/fs-node-builtins": "4.57.2"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-print": {
"version": "4.57.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.2.tgz",
"integrity": "sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/fs-node-utils": "4.57.2",
"tree-dump": "^1.1.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-snapshot": {
"version": "4.57.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.2.tgz",
"integrity": "sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/buffers": "^17.65.0",
"@jsonjoy.com/fs-node-utils": "4.57.2",
"@jsonjoy.com/json-pack": "^17.65.0",
"@jsonjoy.com/util": "^17.65.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": {
"version": "17.67.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz",
"integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": {
"version": "17.67.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz",
"integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": {
"version": "17.67.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz",
"integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/base64": "17.67.0",
"@jsonjoy.com/buffers": "17.67.0",
"@jsonjoy.com/codegen": "17.67.0",
"@jsonjoy.com/json-pointer": "17.67.0",
"@jsonjoy.com/util": "17.67.0",
"hyperdyperid": "^1.2.0",
"thingies": "^2.5.0",
"tree-dump": "^1.1.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": {
"version": "17.67.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz",
"integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/util": "17.67.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": {
"version": "17.67.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz",
"integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/buffers": "17.67.0",
"@jsonjoy.com/codegen": "17.67.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/json-pack": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz",
"integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/base64": "^1.1.2",
"@jsonjoy.com/buffers": "^1.2.0",
"@jsonjoy.com/codegen": "^1.0.0",
"@jsonjoy.com/json-pointer": "^1.0.2",
"@jsonjoy.com/util": "^1.9.0",
"hyperdyperid": "^1.2.0",
"thingies": "^2.5.0",
"tree-dump": "^1.1.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz",
"integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/json-pointer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz",
"integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/codegen": "^1.0.0",
"@jsonjoy.com/util": "^1.9.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/util": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz",
"integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/buffers": "^1.0.0",
"@jsonjoy.com/codegen": "^1.0.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz",
"integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3038,6 +3470,16 @@
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz",
"integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -5100,6 +5542,23 @@
"node": ">=10.13.0"
}
},
"node_modules/glob-to-regex.js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz",
"integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/glob/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -5298,6 +5757,16 @@
"node": ">=18.18.0"
}
},
"node_modules/hyperdyperid": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
"integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.18"
}
},
"node_modules/i18next": {
"version": "25.7.3",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz",
@@ -6190,6 +6659,36 @@
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT"
},
"node_modules/memfs": {
"version": "4.57.2",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.2.tgz",
"integrity": "sha512-2nWzSsJzrukurSDna4Z0WywuScK4Id3tSKejgu74u8KCdW4uNrseKRSIDg75C6Yw5ZRqBe0F0EtMNlTbUq8bAQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jsonjoy.com/fs-core": "4.57.2",
"@jsonjoy.com/fs-fsa": "4.57.2",
"@jsonjoy.com/fs-node": "4.57.2",
"@jsonjoy.com/fs-node-builtins": "4.57.2",
"@jsonjoy.com/fs-node-to-fsa": "4.57.2",
"@jsonjoy.com/fs-node-utils": "4.57.2",
"@jsonjoy.com/fs-print": "4.57.2",
"@jsonjoy.com/fs-snapshot": "4.57.2",
"@jsonjoy.com/json-pack": "^1.11.0",
"@jsonjoy.com/util": "^1.9.0",
"glob-to-regex.js": "^1.0.1",
"thingies": "^2.5.0",
"tree-dump": "^1.0.3",
"tslib": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -8003,6 +8502,23 @@
"node": ">=0.8"
}
},
"node_modules/thingies": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz",
"integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "^2"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -8125,6 +8641,23 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tree-dump": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz",
"integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -8208,6 +8741,13 @@
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"dev": true,
"license": "MIT"
},
"node_modules/unicorn-magic": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+2
View File
@@ -60,6 +60,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^25.6.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.5.3",
@@ -71,6 +72,7 @@
"globals": "^16.2.0",
"i18next-cli": "^1.56.11",
"jsdom": "^25.0.1",
"memfs": "^4.57.2",
"postcss": "^8.5.10",
"tailwindcss": "^3.3.0",
"typescript": "~5.8.3",
+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;
}
interface NavItem {
nameKey: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
permission?: string;
}
const navigation: NavItem[] = [
{ nameKey: 'navigation.dashboard', href: '/admin/dashboard', icon: LayoutDashboard },
const navigation = [
{ nameKey: 'navigation.dashboard', href: '/admin/dashboard', icon: LayoutDashboard, permission: false },
{ nameKey: 'navigation.events', href: '/admin/events', icon: Calendar, permission: 'events.view' },
{ nameKey: 'navigation.archives', href: '/admin/archives', icon: Archive, permission: 'archives.view' },
{ nameKey: 'admin.analytics', href: '/admin/analytics', icon: BarChart3, permission: 'analytics.view' },
@@ -44,7 +37,7 @@ const navigation: NavItem[] = [
{ nameKey: 'navigation.backup', href: '/admin/backup', icon: HardDrive, permission: 'backup.view' },
{ nameKey: 'navigation.cmsPages', href: '/admin/cms', icon: FileText, permission: 'cms.view' },
{ nameKey: 'navigation.users', href: '/admin/users', icon: Users, permission: 'users.view' },
];
] as const;
export const AdminSidebar: React.FC<AdminSidebarProps> = ({ isOpen, onClose }) => {
const location = useLocation();
@@ -1,3 +0,0 @@
import type { ComponentType } from 'react';
export const BackupConfiguration: ComponentType<any>;
@@ -5,18 +5,11 @@ import {
Server,
Cloud,
HardDrive,
Clock,
Calendar,
Shield,
AlertCircle,
Info,
Eye,
EyeOff,
Wifi,
CheckCircle,
XCircle,
Loader2,
FolderOpen,
Database,
Image,
FileArchive
@@ -24,26 +17,58 @@ import {
import { toast } from 'react-toastify';
import { Button, Card, Input } from '../common';
export const BackupConfiguration = ({ config, onSave, isSaving }) => {
interface BackupFormData {
backup_enabled: boolean;
backup_destination_type: 'local' | 'rsync' | 's3';
backup_destination_path: string;
backup_rsync_host: string;
backup_rsync_user: string;
backup_rsync_path: string;
backup_rsync_ssh_key: string;
backup_s3_endpoint: string;
backup_s3_bucket: string;
backup_s3_access_key: string;
backup_s3_secret_key: string;
backup_s3_region: string;
backup_schedule: string;
backup_schedule_cron: string;
backup_retention_days: number;
backup_include_database: boolean;
backup_include_photos: boolean;
backup_include_archives: boolean;
backup_include_thumbnails: boolean;
backup_include_temp: boolean;
backup_compression: boolean;
backup_encryption: boolean;
backup_encryption_passphrase: string;
}
interface BackupConfigurationProps {
config?: Partial<BackupFormData>;
onSave: (data: BackupFormData) => void;
isSaving: boolean;
}
export const BackupConfiguration: React.FC<BackupConfigurationProps> = ({ config, onSave, isSaving }) => {
const { t } = useTranslation();
const destinationTypes = [
{
id: 'local',
id: 'local' as const,
name: t('backup.configuration.destinationTypes.local.name'),
icon: HardDrive,
description: t('backup.configuration.destinationTypes.local.description'),
fields: ['backup_destination_path']
},
{
id: 'rsync',
id: 'rsync' as const,
name: t('backup.configuration.destinationTypes.rsync.name'),
icon: Server,
description: t('backup.configuration.destinationTypes.rsync.description'),
fields: ['backup_rsync_host', 'backup_rsync_user', 'backup_rsync_path', 'backup_rsync_ssh_key']
},
{
id: 's3',
id: 's3' as const,
name: t('backup.configuration.destinationTypes.s3.name'),
icon: Cloud,
description: t('backup.configuration.destinationTypes.s3.description'),
@@ -57,8 +82,8 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{ value: 'weekly', label: t('backup.configuration.schedule.options.weekly') },
{ value: 'custom', label: t('backup.configuration.schedule.options.custom') }
];
const [formData, setFormData] = useState({
const [formData, setFormData] = useState<BackupFormData>({
backup_enabled: false,
backup_destination_type: 'local',
backup_destination_path: '',
@@ -101,33 +126,32 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
}
}, [config]);
const handleChange = (field, value) => {
const handleChange = <K extends keyof BackupFormData>(field: K, value: BackupFormData[K]) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
const handleSubmit = (e) => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Validate required fields
const destinationType = destinationTypes.find(t => t.id === formData.backup_destination_type);
const missingFields = [];
const destinationType = destinationTypes.find(dt => dt.id === formData.backup_destination_type);
const missingFields: string[] = [];
if (formData.backup_enabled && destinationType) {
destinationType.fields.forEach(field => {
if (!formData[field] && !field.includes('optional')) {
if (!formData[field as keyof BackupFormData] && !field.includes('optional')) {
missingFields.push(field);
}
});
}
if (missingFields.length > 0) {
toast.error(t('backup.configuration.messages.requiredFields'));
return;
}
onSave(formData);
};
@@ -138,14 +162,12 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
await new Promise(resolve => setTimeout(resolve, 2000));
toast.success(t('backup.configuration.messages.connectionSuccess'));
} catch (error) {
toast.error(t('backup.configuration.messages.connectionFailed') + ': ' + error.message);
toast.error(t('backup.configuration.messages.connectionFailed') + ': ' + (error as Error).message);
} finally {
setTestingConnection(false);
}
};
const selectedDestination = destinationTypes.find(t => t.id === formData.backup_destination_type);
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Enable/Disable Toggle */}
@@ -399,7 +421,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Schedule Configuration */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.configuration.schedule.title')}</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
@@ -456,7 +478,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Backup Content Selection */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.configuration.whatToBackup.title')}</h3>
<div className="space-y-3">
<label className="flex items-center">
<input
@@ -527,7 +549,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Advanced Options */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.configuration.advancedOptions.title')}</h3>
<div className="space-y-4">
<label className="flex items-center">
<input
@@ -608,4 +630,4 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
</div>
</form>
);
};
};
-3
View File
@@ -1,3 +0,0 @@
import type { ComponentType } from 'react';
export const BackupDashboard: ComponentType<any>;
@@ -8,11 +8,9 @@ import {
Clock,
CheckCircle,
AlertCircle,
TrendingUp,
Shield,
Server,
Cloud,
Calendar,
Play,
Loader2,
AlertTriangle,
@@ -21,7 +19,59 @@ import {
import { format, formatDistanceToNow } from 'date-fns';
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">
<div className="flex items-center justify-between">
<div className="flex-1">
@@ -38,7 +88,7 @@ const StatCard = ({ icon: Icon, label, value, color = 'blue', subtext }) => (
</Card>
);
const formatBytes = (bytes) => {
const formatBytes = (bytes: number): string => {
if (!bytes) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
@@ -46,28 +96,34 @@ const formatBytes = (bytes) => {
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 lastBackup = status?.lastBackup;
const statistics = lastBackup?.statistics || {};
const statistics = lastBackup?.statistics ?? {};
const isConfigured = config && config.backup_destination_type;
const isEnabled = config?.backup_enabled;
// Calculate backup health score
const getHealthScore = () => {
const getHealthScore = (): { score: number; status: HealthStatus; message: string } => {
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') {
return { score: 0, status: 'critical', message: t('backup.dashboard.healthMessages.lastBackupFailed') };
}
if (hoursSinceBackup < 24) {
return { score: 100, status: 'excellent', message: t('backup.dashboard.healthMessages.upToDate') };
} else if (hoursSinceBackup < 48) {
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') };
} else {
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 healthColors = {
excellent: 'green',
good: 'blue',
warning: 'amber',
critical: 'red'
};
return (
<div className="space-y-6">
@@ -109,7 +159,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
{t(`backup.dashboard.healthStatus.${health.status}`)}
</span>
</div>
<div className="flex items-center space-x-4">
<div className="relative w-24 h-24">
<svg className="w-24 h-24 transform -rotate-90">
@@ -137,7 +187,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
<span className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{health.score}%</span>
</div>
</div>
<div className="flex-1">
<p className="text-neutral-700 dark:text-neutral-300 font-medium">{health.message}</p>
{lastBackup && (
@@ -145,7 +195,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
Last successful backup: {formatDistanceToNow(new Date(lastBackup.created_at), { addSuffix: true })}
</p>
)}
<Button
onClick={onRunBackup}
disabled={!isConfigured || !isEnabled || isBackupRunning}
@@ -177,7 +227,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
color="blue"
subtext={lastBackup ? `${t('backup.dashboard.stats.last')}: ${format(new Date(lastBackup.created_at), 'PP')}` : t('backup.dashboard.stats.noBackupsYet')}
/>
<StatCard
icon={HardDrive}
label={t('backup.dashboard.stats.backupSize')}
@@ -185,7 +235,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
color="green"
subtext={`${statistics.files_processed || 0} ${t('backup.dashboard.stats.files')}`}
/>
<StatCard
icon={Clock}
label={t('backup.dashboard.stats.lastDuration')}
@@ -193,7 +243,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
color="purple"
subtext={lastBackup ? format(new Date(lastBackup.created_at), 'p') : ''}
/>
<StatCard
icon={Shield}
label={t('backup.dashboard.stats.backupStatus')}
@@ -324,4 +374,4 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
</div>
</div>
);
};
};
@@ -19,7 +19,7 @@ const RATING_OPTIONS = [
{ value: 3, label: 'filter.threeStarsPlus' },
{ value: 4, label: 'filter.fourStarsPlus' },
{ value: 5, label: 'filter.fiveStarsOnly' },
];
] as const;
export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
filters,
+472 -357
View File
@@ -158,7 +158,14 @@
"navigation": {
"dashboard": "Dashboard",
"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": {
"title": "Veranstaltungsarten",
@@ -275,6 +282,12 @@
"actions": {
"runBackupNow": "Backup jetzt ausführen",
"running": "Wird ausgeführt..."
},
"healthStatus": {
"warning": "Warnung",
"excellent": "Ausgezeichnet",
"good": "Gut",
"critical": "Kritisch"
}
},
"configuration": {
@@ -634,51 +647,51 @@
"anonymous": "Anonym"
},
"favorites": "Favoriten",
"expiresIn_one": "",
"expiresIn_other": "",
"downloading_one": "",
"downloading_other": "",
"photosSelected_one": "",
"photosSelected_other": "",
"downloadSelected_one": "",
"downloadSelected_other": "",
"expiresIn_one": "Galerie läuft in {{count}} Tag ab",
"expiresIn_other": "Galerie läuft in {{count}} Tagen ab",
"downloading_one": "1 Foto wird heruntergeladen…",
"downloading_other": "{{count}} Fotos werden heruntergeladen…",
"photosSelected_one": "{{count}} Foto ausgewählt",
"photosSelected_other": "{{count}} Fotos ausgewählt",
"downloadSelected_one": "{{count}} Foto herunterladen",
"downloadSelected_other": "{{count}} Fotos herunterladen",
"guestRecovery": {
"invalidEmail": "",
"codeSent": "",
"requestError": "",
"invalidCode": "",
"verifyError": "",
"back": "",
"title": "",
"emailStepDescription": "",
"codeStepDescription": "",
"emailLabel": "",
"sendCode": "",
"codeLabel": "",
"verifyCode": ""
"invalidEmail": "Gültige E-Mail-Adresse eingeben",
"codeSent": "Prüfen Sie Ihren Posteingang auf einen Bestätigungscode.",
"requestError": "Code konnte nicht gesendet werden. Bitte erneut versuchen.",
"invalidCode": "6-stelligen Code eingeben",
"verifyError": "Ungültiger oder abgelaufener Code.",
"back": "Zurück",
"title": "Ihre Auswahl wiederherstellen",
"emailStepDescription": "Geben Sie die E-Mail-Adresse ein, die Sie zuvor verwendet haben. Wir senden Ihnen einen 6-stelligen Bestätigungscode.",
"codeStepDescription": "Geben Sie den 6-stelligen Code ein, den wir an Ihre E-Mail-Adresse gesendet haben.",
"emailLabel": "E-Mail",
"sendCode": "Code senden",
"codeLabel": "Bestätigungscode",
"verifyCode": "Verifizieren und fortfahren"
},
"guestPrompt": {
"nameRequired": "",
"invalidEmail": "",
"emailRequired": "",
"error": "",
"title": "",
"description": "",
"nameLabel": "",
"namePlaceholder": "",
"emailLabelRequired": "",
"emailLabel": "",
"emailPlaceholder": "",
"submit": "",
"alreadyHere": ""
"nameRequired": "Name ist erforderlich",
"invalidEmail": "Ungültige E-Mail-Adresse",
"emailRequired": "E-Mail ist erforderlich",
"error": "Registrierung fehlgeschlagen",
"title": "Willkommen — wie heißen Sie?",
"description": "Ihre Auswahl wird unter diesem Namen gespeichert, damit der Fotograf weiß, welche Fotos Ihnen gefallen.",
"nameLabel": "Ihr Name",
"namePlaceholder": "Name eingeben",
"emailLabelRequired": "E-Mail",
"emailLabel": "E-Mail (optional)",
"emailPlaceholder": "Sie@beispiel.de",
"submit": "Weiter",
"alreadyHere": "Ich war schon einmal hier"
},
"footer": {
"forgetMeConfirm": "",
"forgetMe": ""
"forgetMeConfirm": "Ihr Name und Ihre Auswahl werden aus dieser Galerie entfernt.",
"forgetMe": "Vergiss mich ({{name}})"
},
"photosCount_one": "",
"photosCount_other": "",
"poweredBy": ""
"photosCount_one": "{{count}} Foto",
"photosCount_other": "{{count}} Fotos",
"poweredBy": "Bereitgestellt von PicPeak"
},
"categories": {
"title": "Fotokategorien",
@@ -907,14 +920,14 @@
"successAll": "{{count}} Veranstaltungen endgültig gelöscht",
"successPartial": "{{success}} Veranstaltungen gelöscht, {{failed}} fehlgeschlagen",
"errorGeneric": "Veranstaltungen konnten nicht gelöscht werden",
"successAll_one": "",
"successAll_other": "",
"title_one": "",
"title_other": "",
"processing_one": "",
"processing_other": "",
"submit_one": "",
"submit_other": ""
"successAll_one": "{{count}} Veranstaltung dauerhaft gelöscht",
"successAll_other": "{{count}} Veranstaltungen dauerhaft gelöscht",
"title_one": "{{count}} Veranstaltung dauerhaft löschen?",
"title_other": "{{count}} Veranstaltungen dauerhaft löschen?",
"processing_one": "{{count}} Veranstaltung wird gelöscht. Dies kann einige Minuten dauern — bitte schließen Sie dieses Fenster nicht.",
"processing_other": "{{count}} Veranstaltungen werden gelöscht. Dies kann einige Minuten dauern — bitte schließen Sie dieses Fenster nicht.",
"submit_one": "{{count}} Veranstaltung löschen",
"submit_other": "{{count}} Veranstaltungen löschen"
},
"searchEventsPlaceholder": "Veranstaltungen suchen...",
"all": "Alle",
@@ -941,21 +954,21 @@
"complete": "Fertig!",
"failed": "Umbenennung fehlgeschlagen",
"confirm": "Veranstaltung umbenennen",
"success": "",
"filesRenamed_one": "",
"filesRenamed_other": "",
"newLink": "",
"currentName": "",
"newName": "",
"enterNewName": "",
"newUrl": "",
"checkingAvailability": "",
"resendEmail": "",
"emailTo": "",
"warningTitle": "",
"warning1": "",
"warning2": "",
"warning3": ""
"success": "Veranstaltung erfolgreich umbenannt!",
"filesRenamed_one": "{{count}} Datei aktualisiert",
"filesRenamed_other": "{{count}} Dateien aktualisiert",
"newLink": "Neuer Galerie-Link",
"currentName": "Aktueller Name:",
"newName": "Neuer Veranstaltungsname",
"enterNewName": "Neuen Veranstaltungsnamen eingeben",
"newUrl": "Neue URL:",
"checkingAvailability": "Verfügbarkeit wird geprüft...",
"resendEmail": "Einladungs-E-Mail mit neuem Galerie-Link erneut senden",
"emailTo": "Aktualisierte Galerie-Zugangsemail senden an",
"warningTitle": "Bitte beachten:",
"warning1": "Die Galerie-URL wird sich ändern",
"warning2": "Alte URLs werden automatisch zur neuen URL weitergeleitet",
"warning3": "Fotodateien können umbenannt werden"
},
"stats": {
"totalEvents": "Gesamtveranstaltungen",
@@ -972,27 +985,27 @@
"downloadingArchive": "{{name}}-Archiv wird heruntergeladen...",
"downloadStarted": "Download gestartet",
"failedToDownloadArchive": "Archiv-Download fehlgeschlagen",
"bulkArchiveSuccess_one": "",
"bulkArchiveSuccess_other": "",
"daysLeft_one": "",
"daysLeft_other": "",
"eventsSelected_one": "",
"eventsSelected_other": "",
"paginationLabel": "",
"filtered": "",
"pageOf": "",
"notFound": "",
"customerPhone": "",
"customerPhonePlaceholder": "",
"allowPresignedDownload": "",
"neverExpires": "",
"rightClickBlocked": "",
"devtoolsDetection": "",
"watermarked": "",
"importExternal": "",
"externalImportInfo": "",
"selectExternalFolder": "",
"importFromSelectedFolder": ""
"bulkArchiveSuccess_one": "{{count}} Veranstaltung erfolgreich archiviert",
"bulkArchiveSuccess_other": "{{count}} Veranstaltungen erfolgreich archiviert",
"daysLeft_one": "({{count}} Tag verbleibend)",
"daysLeft_other": "({{count}} Tage verbleibend)",
"eventsSelected_one": "{{count}} Veranstaltung ausgewählt",
"eventsSelected_other": "{{count}} Veranstaltungen ausgewählt",
"paginationLabel": "{{from}}{{to}} von {{total}}",
"filtered": "gefiltert",
"pageOf": "Seite {{page}} von {{totalPages}}",
"notFound": "Veranstaltung nicht gefunden",
"customerPhone": "Kundentelefon",
"customerPhonePlaceholder": "+49 555 555 1234",
"allowPresignedDownload": "Direkten S3-Download erlauben (kein Wasserzeichen, nur S3-Modus)",
"neverExpires": "Nie",
"rightClickBlocked": "Rechtsklick gesperrt",
"devtoolsDetection": "DevTools-Erkennung",
"watermarked": "Mit Wasserzeichen",
"importExternal": "Aus externem Ordner importieren",
"externalImportInfo": "Alle Bilder aus dem ausgewählten Ordner werden importiert.",
"selectExternalFolder": "Externen Ordner unter /external-media auswählen",
"importFromSelectedFolder": "Aus ausgewähltem Ordner importieren"
},
"settings": {
"title": "Systemeinstellungen",
@@ -1139,12 +1152,12 @@
"saveSettings": "Veranstaltungseinstellungen speichern",
"noteTitle": "Hinweis",
"noteText": "Diese Einstellungen betreffen nur die Erstellung neuer Veranstaltungen. Bestehende Veranstaltungen sind nicht betroffen. Standardmäßig sind alle Felder erforderlich.",
"defaultRequirePassword": "",
"defaultRequirePasswordHelp": "",
"showGalleryFilterBar": "",
"showGalleryFilterBarHelp": "",
"enablePhoneField": "",
"enablePhoneFieldHelp": ""
"defaultRequirePassword": "Standardmäßig Passwort erforderlich",
"defaultRequirePasswordHelp": "\"Passwort erforderlich\" beim Erstellen neuer Veranstaltungen vorausgewählt. Deaktivieren für schnellere Erstellung öffentlicher Galerien.",
"showGalleryFilterBar": "Filterleiste in Galerien anzeigen",
"showGalleryFilterBarHelp": "Dateiname-Suche und Sortiersteuerungen über Raster-Galerien anzeigen. Deaktivieren für ein übersichtlicheres Layout.",
"enablePhoneField": "Telefonnummer-Feld aktivieren",
"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": {
"title": "Bildschutz",
@@ -1284,8 +1297,8 @@
"checkSuccess": "Benachrichtigung für neue Version gesendet",
"checkNoAction": "Keine Benachrichtigung erforderlich: {{reason}}",
"checkError": "Fehler beim Prüfen auf Updates",
"emailSent_one": "",
"emailSent_other": ""
"emailSent_one": "Benachrichtigungs-E-Mail an {{count}} Empfänger gesendet",
"emailSent_other": "Benachrichtigungs-E-Mail an {{count}} Empfänger gesendet"
},
"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."
},
"groups": {
"general": "",
"display": "",
"privacySecurity": "",
"integrations": "",
"system": ""
"general": "Allgemein",
"display": "Anzeige",
"privacySecurity": "Datenschutz & Sicherheit",
"integrations": "Integrationen",
"system": "System"
},
"apiTokens": {
"title": "",
"createError": "",
"revoked": "",
"subtitle": "",
"copyNow": "",
"copied": "",
"copyFailed": "",
"name": "",
"namePlaceholder": "",
"scopes": "",
"generate": "",
"scopeHint": "",
"existing": "",
"lastUsed": "",
"created": "",
"status": "",
"statusRevoked": "",
"statusExpired": "",
"statusActive": "",
"confirmRevoke": "",
"revoke": "",
"empty": ""
"title": "API-Tokens",
"createError": "Token konnte nicht erstellt werden",
"revoked": "Token widerrufen",
"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": "Kopieren Sie diesen Token jetzt — er wird nicht erneut angezeigt.",
"copied": "Kopiert",
"copyFailed": "Kopieren fehlgeschlagen",
"name": "Name",
"namePlaceholder": "z.B. n8n Produktion",
"scopes": "Berechtigungen",
"generate": "Token generieren",
"scopeHint": "admin > write > read. Ein reiner Lese-Token kann keine Änderungen vornehmen, auch wenn der Besitzer super_admin ist.",
"existing": "Vorhandene Tokens",
"lastUsed": "Zuletzt verwendet",
"created": "Erstellt",
"status": "Status",
"statusRevoked": "Widerrufen",
"statusExpired": "Abgelaufen",
"statusActive": "Aktiv",
"confirmRevoke": "Diesen Token wirklich widerrufen?",
"revoke": "Widerrufen",
"empty": "Noch keine Tokens. Generieren Sie oben einen, um zu beginnen."
},
"webhooks": {
"title": "",
"subtitle": "",
"piiNotice": "",
"copyNow": "",
"name": "",
"url": "",
"events": "",
"filter": "",
"template": "",
"create": "",
"existing": "",
"empty": ""
"title": "Webhooks",
"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": "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": "Kopieren Sie dieses Signaturgeheimnis jetzt — es wird nicht erneut angezeigt.",
"name": "Name",
"url": "Empfänger-URL",
"events": "Ereignisse abonnieren",
"filter": "Filter (JSON, optional)",
"template": "Vorlage (optional)",
"create": "Webhook erstellen",
"existing": "Vorhandene Webhooks",
"empty": "Noch keine Webhooks. Erstellen Sie oben einen, um Ereignisbenachrichtigungen zu erhalten."
},
"sectionLabel": "",
"navAriaLabel": ""
"sectionLabel": "Einstellungsbereich",
"navAriaLabel": "Einstellungsnavigation"
},
"branding": {
"title": "Branding & Themen",
@@ -1588,38 +1601,68 @@
"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.",
"betaThumbnailWarningLink": "Zu den Thumbnail-Einstellungen",
"logoSize": "",
"surfaceColor": "",
"elevatedColor": "",
"borderColor": "",
"mutedTextColor": "",
"accentDarkColor": "",
"syncFromBranding": "",
"forceColorMode": "",
"forceColorModeHelp": "",
"forceColorModeNone": "",
"forceColorModeDark": "",
"forceColorModeLight": "",
"colorGroupSurfaces": "",
"colorGroupSurfacesHelp": "",
"backgroundColorHelp": "",
"surfaceColorHelp": "",
"elevatedColorHelp": "",
"borderColorHelp": "",
"colorGroupText": "",
"colorGroupTextHelp": "",
"textColorHelp": "",
"mutedTextColorHelp": "",
"colorGroupAccent": "",
"colorGroupAccentHelp": "",
"accentColorHelp": "",
"accentDarkColorHelp": "",
"cssTemplate": "",
"cssTemplateDescription": "",
"noTemplate": "",
"noTemplateDescription": "",
"templateSlot": "",
"eventCustomCSS": ""
"logoSize": "Logo-Größe",
"surfaceColor": "Oberfläche",
"elevatedColor": "Erhöht",
"borderColor": "Rahmen",
"mutedTextColor": "Gedämpfter Text",
"accentDarkColor": "Akzent (gefüllt)",
"syncFromBranding": "Vom Branding synchronisieren",
"forceColorMode": "Farbmodus erzwingen",
"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": "Kein Zwang (Benutzerauswahl)",
"forceColorModeDark": "Dunkelmodus erzwingen",
"forceColorModeLight": "Hellmodus erzwingen",
"colorGroupSurfaces": "Oberflächen",
"colorGroupSurfacesHelp": "Die neutralen Ebenen hinter Ihrem Inhalt. Hintergrund liegt am weitesten zurück; Oberfläche und Erhöht stapeln sich darüber.",
"backgroundColorHelp": "Die Seite selbst — Hintergrundfarbe jeder Galerie, Admin-Seite und CMS-Seite.",
"surfaceColorHelp": "Karten, Seitenleiste, Kopfzeile und Navigation. Die erste Ebene über dem Hintergrund.",
"elevatedColorHelp": "Panels, die über Karten schweben: Bild-Platzhalter, Hover-/Aktiv-Zeilen, Modal-Kopfzeilen, Code-Blöcke.",
"borderColorHelp": "Trennlinien, Tabellengitterlinien, Kartenumrisse, Eingabe-Rahmen.",
"colorGroupText": "Text",
"colorGroupTextHelp": "Vordergrund-Textfarben. Primär für alles, worauf Leser sich konzentrieren; Sekundär für unterstützenden Text.",
"textColorHelp": "Überschriften, Fließtext, Tabellenzellen, Formulareingabewerte, Navigationsbezeichnungen — die Haupttextfarbe.",
"mutedTextColorHelp": "Bildunterschriften, Hilfetext unter Eingaben, Tabellen-Spaltenüberschriften, Fußzeilenlinks, Daten und Metadaten.",
"colorGroupAccent": "Akzent",
"colorGroupAccentHelp": "Markenfarben, die interaktive Elemente hervorheben. Verwenden Sie ein starkes Farbpaar — Akzent für Umrisse/Text, Akzent Dunkel für gefüllte Buttons.",
"accentColorHelp": "Links, Symbole, Fokusringe, Hover-Zustände bei Primär-Buttons, aktive Seitenleisten-Unterstreichung. Sollte auf Hintergrund und Oberfläche gut lesbar sein.",
"accentDarkColorHelp": "Gefüllte CTA-Buttons, aktiver Seitenleisten-Eintrag-Hintergrund, Abzeichen und Tags. Muss genug Kontrast für weißen Text bieten.",
"cssTemplate": "CSS-Vorlage",
"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": "Keine Vorlage",
"noTemplateDescription": "Nur Theme-Einstellungen ohne CSS-Vorlage verwenden",
"templateSlot": "Slot {{slot}}",
"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": {
"title": "Admin-Panel",
@@ -1635,7 +1678,10 @@
"archivedEvents": "Archivierte Veranstaltungen",
"systemHealth": "Systemstatus",
"health": {
"checking": "Prüfe..."
"checking": "Prüfe...",
"healthy": "Gesund",
"warning": "Warnung",
"error": "Fehler"
},
"updates": {
"available": "Update verfügbar",
@@ -1694,17 +1740,17 @@
"archiveRestored": "Archiv für \"{{eventName}}\" wiederhergestellt",
"systemActivity": "Systemaktivität: {{type}}",
"adminProfileUpdated": "Admin-Profil aktualisiert von {{actorName}}",
"photosUploaded_one": "",
"photosUploaded_other": "",
"photosBulkDeleted_one": "",
"photosBulkDeleted_other": "",
"bulkDownload_one": "",
"bulkDownload_other": ""
"photosUploaded_one": "{{count}} Foto in \"{{eventName}}\" hochgeladen",
"photosUploaded_other": "{{count}} Fotos in \"{{eventName}}\" hochgeladen",
"photosBulkDeleted_one": "{{count}} Foto aus \"{{eventName}}\" gelöscht",
"photosBulkDeleted_other": "{{count}} Fotos aus \"{{eventName}}\" gelöscht",
"bulkDownload_one": "{{count}} Foto aus \"{{eventName}}\" heruntergeladen",
"bulkDownload_other": "{{count}} Fotos aus \"{{eventName}}\" heruntergeladen"
},
"notificationToasts": {
"markedAllRead": "Alle Benachrichtigungen als gelesen markiert",
"clearedAll_one": "",
"clearedAll_other": ""
"clearedAll_one": "{{count}} Benachrichtigung gelöscht",
"clearedAll_other": "{{count}} Benachrichtigungen gelöscht"
},
"changePassword": "Passwort ändern",
"loadingDashboard": "Dashboard wird geladen...",
@@ -1724,73 +1770,135 @@
"showSelected": "Einblenden",
"hiddenSuccess": "Fotos für Gäste ausgeblendet",
"visibleSuccess": "Fotos jetzt für Gäste sichtbar",
"processingStatus": "",
"processingFailed": "",
"retryQueued": ""
"processingStatus": "Wird verarbeitet…",
"processingFailed": "Fehlgeschlagen",
"retryQueued": "Wiederholung in Warteschlange"
},
"events": {
"tabs": {
"guests": ""
"guests": "Gäste"
}
},
"daysLeft_one": "",
"daysLeft_other": "",
"viewAllExpiringEvents_one": "",
"viewAllExpiringEvents_other": "",
"daysLeft_one": "{{count}} Tag verbleibend",
"daysLeft_other": "{{count}} Tage verbleibend",
"viewAllExpiringEvents_one": "Alle {{count}} ablaufenden Veranstaltungen anzeigen",
"viewAllExpiringEvents_other": "Alle {{count}} ablaufenden Veranstaltungen anzeigen",
"guests": {
"loading": "",
"loading": "Wird geladen...",
"aggregate": {
"empty": "",
"description": ""
"empty": "Noch keine Gastauswahl.",
"description": "Fotos sortiert nach der Anzahl der Gäste, die sie geliket oder favorisiert haben."
},
"inviteCreated": "",
"inviteCreateError": "",
"inviteRevoked": "",
"inviteRevokeError": "",
"invitesTitle": "",
"createInvite": "",
"inviteName": "",
"inviteEmail": "",
"generateInvite": "",
"existingInvites": "",
"noInvites": "",
"copyLink": "",
"revokeInvite": "",
"deletedToast": "",
"deletedError": "",
"mergedToast": "",
"mergedError": "",
"forgetGuestConfirm": "",
"exportError": "",
"mergeSelectAtLeastTwo": "",
"mergeConfirm_one": "",
"mergeConfirm_other": "",
"backToList": "",
"title": "",
"mergeSelected_one": "",
"mergeSelected_other": "",
"mergeNow": "",
"aggregateView": "",
"mergeMode": "",
"exportAll": "",
"empty": "",
"inviteCreated": "Einladung erstellt",
"inviteCreateError": "Einladung konnte nicht erstellt werden",
"inviteRevoked": "Einladung widerrufen",
"inviteRevokeError": "Einladung konnte nicht widerrufen werden",
"invitesTitle": "Gasteinladungen",
"createInvite": "Einladung erstellen",
"inviteName": "Gastname",
"inviteEmail": "E-Mail (optional)",
"generateInvite": "Einladungslink generieren",
"existingInvites": "Vorhandene Einladungen",
"noInvites": "Noch keine Einladungen",
"copyLink": "Link kopieren",
"revokeInvite": "Widerrufen",
"deletedToast": "Gast entfernt",
"deletedError": "Gast konnte nicht entfernt werden",
"mergedToast": "Gäste zusammengeführt",
"mergedError": "Gäste konnten nicht zusammengeführt werden",
"forgetGuestConfirm": "Diesen Gast entfernen? Ihre Auswahl wird anonymisiert, aber in den Gesamtzahlen behalten.",
"exportError": "Export fehlgeschlagen",
"mergeSelectAtLeastTwo": "Mindestens 2 Gäste zum Zusammenführen auswählen",
"mergeConfirm_one": "{{count}} Gast in {{name}} zusammenführen? Dies kann nicht rückgängig gemacht werden.",
"mergeConfirm_other": "{{count}} Gäste in {{name}} zusammenführen? Dies kann nicht rückgängig gemacht werden.",
"backToList": "Zurück zur Liste",
"title": "Gäste",
"mergeSelected_one": "{{count}} ausgewählt",
"mergeSelected_other": "{{count}} ausgewählt",
"mergeNow": "Auswahl zusammenführen",
"aggregateView": "Nach Beliebtheit",
"mergeMode": "Zusammenführen",
"exportAll": "Alle exportieren",
"empty": "Noch keine Gäste registriert.",
"columns": {
"name": "",
"email": "",
"likes": "",
"favorites": "",
"comments": "",
"ratings": "",
"lastSeen": ""
"name": "Name",
"email": "E-Mail",
"likes": "Likes",
"favorites": "Favoriten",
"comments": "Kommentare",
"ratings": "Bewertungen",
"lastSeen": "Zuletzt gesehen"
},
"view": "",
"export": "",
"forgetGuest": "",
"loadingDetail": "",
"view": "Details anzeigen",
"export": "Exportieren",
"forgetGuest": "Gast entfernen",
"loadingDetail": "Auswahl wird geladen...",
"detail": {
"noComments": "",
"empty": ""
"noComments": "Keine Kommentare",
"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": {
@@ -1857,8 +1965,8 @@
"requiredFields": "Bitte füllen Sie alle Pflichtfelder aus",
"enterTestEmail": "Bitte geben Sie eine Test-E-Mail-Adresse ein",
"eventCreationFailed": "Veranstaltung konnte nicht erstellt werden",
"noShareLink": "",
"copyFailed": ""
"noShareLink": "Kein Freigabe-Link verfügbar",
"copyFailed": "Link konnte nicht kopiert werden"
},
"legal": {
"impressum": "Impressum",
@@ -1880,8 +1988,8 @@
"brandingUpdated": "Branding erfolgreich aktualisiert",
"categoryUpdated": "Kategorie erfolgreich aktualisiert",
"emailConfigSaved": "E-Mail-Konfiguration erfolgreich gespeichert",
"brandingThemeMissing": "",
"brandingPaletteSynced": ""
"brandingThemeMissing": "Es wurde noch kein Branding-Theme gespeichert.",
"brandingPaletteSynced": "Palette vom Branding synchronisiert."
},
"analytics": {
"title": "Analytics Dashboard",
@@ -1978,22 +2086,22 @@
"noTranslation": "Noch keine Übersetzung",
"noTranslationYet": "Für diese Sprache existiert noch keine Übersetzung. Kopieren Sie von einer vorhandenen Sprache:",
"copyFrom": "Kopieren von",
"syncedFromBranding": "",
"syncFromBranding": "",
"primaryColorHelp": "",
"secondaryColorHelp": "",
"bodyBgColor": "",
"bodyBgColorHelp": "",
"containerBgColor": "",
"containerBgColorHelp": "",
"listBgColor": "",
"listBgColorHelp": "",
"bodyTextColor": "",
"bodyTextColorHelp": "",
"mutedTextColor": "",
"mutedTextColorHelp": "",
"buttonTextColor": "",
"buttonTextColorHelp": ""
"syncedFromBranding": "E-Mail-Farben vom Branding synchronisiert. Klicken Sie auf Speichern, um sie anzuwenden.",
"syncFromBranding": "Vom Branding synchronisieren",
"primaryColorHelp": "Kopfzeile, H2-Überschriften, Button-Hintergrund, Linkfarbe. Entspricht Branding → Akzent (gefüllt).",
"secondaryColorHelp": "Fußzeilen-Hintergrund. Entspricht Branding → Oberfläche.",
"bodyBgColor": "Seitenhintergrund",
"bodyBgColorHelp": "Der Rahmen um die E-Mail-Karte — was der Empfänger hinter der E-Mail sieht. Entspricht Branding → Hintergrund.",
"containerBgColor": "E-Mail-Karte",
"containerBgColorHelp": "Die weiße Karte, die den E-Mail-Inhalt enthält. Entspricht Branding → Oberfläche.",
"listBgColor": "Infopanel",
"listBgColorHelp": "Hintergrund der Aufzählungs-Infopanels im E-Mail-Text. Entspricht Branding → Erhöht.",
"bodyTextColor": "Fließtext",
"bodyTextColorHelp": "Absatz- und Fetttext-Farbe. Entspricht Branding → Primärer Text.",
"mutedTextColor": "Fußzeilentext",
"mutedTextColorHelp": "Fußzeilentext und Copyright-Zeile. Entspricht Branding → Sekundärer Text.",
"buttonTextColor": "Button-Text",
"buttonTextColorHelp": "Textfarbe auf gefüllten Buttons. Sollte klar gegen die Primärfarbe kontrastieren. Kein Branding-Äquivalent — meistens weiß."
},
"cms": {
"title": "CMS-Seiten",
@@ -2015,14 +2123,14 @@
"externalUrlPlaceholder": "https://example.com/impressum",
"externalUrlInvalid": "Muss eine gültige https://-URL sein",
"externalUrlActive": "Externe URL ist aktiv — interne Inhalte bleiben gespeichert, werden Besuchern aber nicht angezeigt.",
"logoUploaded": "",
"logoCleared": "",
"pageLogo": "",
"pageLogoHelp": "",
"noLogo": "",
"replaceLogo": "",
"uploadLogo": "",
"clearLogo": ""
"logoUploaded": "Logo hochgeladen",
"logoCleared": "Logo entfernt",
"pageLogo": "Seitenlogo",
"pageLogoHelp": "Optional. Falls gesetzt, wird es anstelle des globalen Branding-Logos auf dieser Seite verwendet.",
"noLogo": "keine Überschreibung",
"replaceLogo": "Logo ersetzen",
"uploadLogo": "Logo hochladen",
"clearLogo": "Seitenstandard verwenden"
},
"validation": {
"eventNameRequired": "Veranstaltungsname ist erforderlich",
@@ -2034,10 +2142,10 @@
"passwordMinLength": "Passwort muss mindestens 6 Zeichen lang sein",
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
"expirationRange": "Ablauf muss zwischen 1 und 365 Tagen liegen",
"required": "",
"expirationRequired": "",
"eventDateRequired": "",
"passwordTooSimple": ""
"required": "Dieses Feld ist erforderlich",
"expirationRequired": "Ablaufdatum ist erforderlich.",
"eventDateRequired": "Veranstaltungsdatum ist erforderlich",
"passwordTooSimple": "Das Passwort darf nicht nur aus Zahlen bestehen. Verwenden Sie z.B. ein Datumsformat wie \"04.07.2025\""
},
"maintenance": {
"title": "Systemwartung",
@@ -2133,96 +2241,96 @@
"commentsDesc": "Textkommentare auf Fotos",
"favorites": "Favoriten",
"favoritesDesc": "Fotos als Favoriten markieren",
"identityMode": "",
"identityModeSimple": "",
"identityModeSimpleDesc": "",
"identityModeGuest": "",
"identityModeGuestDesc": "",
"privacyModeration": "",
"requireInfo": "",
"requireInfoDesc": "",
"moderateComments": "",
"moderateCommentsDesc": "",
"showToGuests": "",
"showToGuestsDesc": "",
"enableRateLimiting": "",
"rateLimitingDesc": "",
"timeWindow": "",
"maxRequests": ""
"identityMode": "Identitätsmodus",
"identityModeSimple": "Einfaches Feedback",
"identityModeSimpleDesc": "Anonym, gerätebasiert. Alle Besucher auf demselben Gerät teilen den Zustand.",
"identityModeGuest": "Gastbezogene Auswahl",
"identityModeGuestDesc": "Jeder Besucher gibt seinen Namen ein. Ermöglicht gastbezogenes Tracking und Admin-Einblicke.",
"privacyModeration": "Datenschutz & Moderation",
"requireInfo": "Name & E-Mail erforderlich",
"requireInfoDesc": "Gäste müssen Name und E-Mail angeben, um Feedback zu hinterlassen",
"moderateComments": "Kommentare moderieren",
"moderateCommentsDesc": "Kommentare benötigen eine Genehmigung, bevor sie sichtbar sind",
"showToGuests": "Feedback für Gäste anzeigen",
"showToGuestsDesc": "Andere Gäste können Bewertungen, Likes und genehmigte Kommentare sehen",
"enableRateLimiting": "Ratenbegrenzung aktivieren",
"rateLimitingDesc": "Spam durch Begrenzung der Feedback-Häufigkeit verhindern",
"timeWindow": "Zeitfenster (Minuten)",
"maxRequests": "Maximale Anfragen"
},
"settingsUpdated": "",
"settingsUpdateError": "",
"moderated": "",
"deleted": "",
"exported": "",
"exportError": "",
"title": "",
"exportCSV": "",
"exportJSON": "",
"settingsUpdated": "Feedback-Einstellungen aktualisiert",
"settingsUpdateError": "Einstellungen konnten nicht aktualisiert werden",
"moderated": "Feedback moderiert",
"deleted": "Feedback gelöscht",
"exported": "Feedback exportiert",
"exportError": "Feedback konnte nicht exportiert werden",
"title": "Feedback-Verwaltung",
"exportCSV": "CSV exportieren",
"exportJSON": "JSON exportieren",
"tabs": {
"settings": "",
"feedback": "",
"analytics": "",
"moderation": ""
"settings": "Einstellungen",
"feedback": "Feedback",
"analytics": "Analytik",
"moderation": "Moderation"
},
"allTypes": "",
"allTypes": "Alle Typen",
"types": {
"rating": "",
"like": "",
"comment": "",
"favorite": ""
"rating": "Bewertungen",
"like": "Likes",
"comment": "Kommentare",
"favorite": "Favoriten"
},
"allStatuses": "",
"allStatuses": "Alle Status",
"status": {
"pending": "",
"approved": "",
"hidden": ""
"pending": "Ausstehend",
"approved": "Genehmigt",
"hidden": "Versteckt"
},
"noFeedback": "",
"approve": "",
"hide": "",
"unhide": "",
"confirmDelete": "",
"avgRating": "",
"totalRatings_one": "",
"totalRatings_other": "",
"totalLikes": "",
"totalComments": "",
"pendingModeration_one": "",
"pendingModeration_other": "",
"totalInteractions": "",
"topRated": "",
"recentComments": "",
"wordFilters": "",
"wordFiltersDesc": "",
"manageFilters": "",
"manage": "",
"ratingSubmitted": "",
"ratingError": "",
"rateStar_one": "",
"rateStar_other": "",
"ratingsCount_one": "",
"ratingsCount_other": "",
"likeError": "",
"unlike": "",
"like": "",
"favoriteError": "",
"unfavorite": "",
"favorite": "",
"invalidEmail": "",
"identityRequired": "",
"identityReason": "",
"namePlaceholder": "",
"emailPlaceholder": "",
"submitFeedback": "",
"moderationSuccess": "",
"pendingModeration": "",
"pending": "",
"noPendingComments": "",
"onPhoto": "",
"showAll_one": "",
"showAll_other": "",
"viewAllFeedback": ""
"noFeedback": "Kein Feedback gefunden",
"approve": "Genehmigen",
"hide": "Ausblenden",
"unhide": "Einblenden",
"confirmDelete": "Dieses Feedback wirklich löschen?",
"avgRating": "Durchschnittsbewertung",
"totalRatings_one": "{{count}} Bewertungen",
"totalRatings_other": "{{count}} Bewertungen",
"totalLikes": "Gesamte Likes",
"totalComments": "Gesamte Kommentare",
"pendingModeration_one": "{{count}} ausstehend",
"pendingModeration_other": "{{count}} ausstehend",
"totalInteractions": "Gesamte Interaktionen",
"topRated": "Bestbewertete Fotos",
"recentComments": "Neueste Kommentare",
"wordFilters": "Wortfilter",
"wordFiltersDesc": "Gesperrte Wörter für die Kommentarmoderation verwalten",
"manageFilters": "Wortfilter verwalten",
"manage": "Feedback verwalten",
"ratingSubmitted": "Bewertung abgegeben",
"ratingError": "Bewertung konnte nicht abgegeben werden",
"rateStar_one": "{{count}} Stern bewerten",
"rateStar_other": "{{count}} Sterne bewerten",
"ratingsCount_one": "{{count}} Bewertungen",
"ratingsCount_other": "{{count}} Bewertungen",
"likeError": "Like konnte nicht aktualisiert werden",
"unlike": "Nicht mehr mögen",
"like": "Gefällt mir",
"favoriteError": "Favorit konnte nicht aktualisiert werden",
"unfavorite": "Aus Favoriten entfernen",
"favorite": "Zu Favoriten hinzufügen",
"invalidEmail": "Ungültige E-Mail-Adresse",
"identityRequired": "Ihre Informationen erforderlich",
"identityReason": "Bitte geben Sie Ihren Namen und Ihre E-Mail an, um {{type}} zu senden.",
"namePlaceholder": "Ihren Namen eingeben",
"emailPlaceholder": "Ihre E-Mail eingeben",
"submitFeedback": "Feedback senden",
"moderationSuccess": "Feedback moderiert",
"pendingModeration": "Ausstehende Moderation",
"pending": "ausstehend",
"noPendingComments": "Keine Kommentare warten auf Moderation",
"onPhoto": "Auf Foto",
"showAll_one": "Alle {{count}} ausstehenden Kommentare anzeigen",
"showAll_other": "Alle {{count}} ausstehenden Kommentare anzeigen",
"viewAllFeedback": "Gesamtes Feedback & Einstellungen anzeigen"
},
"filter": {
"feedbackFilters": "Feedback-Filter",
@@ -2233,7 +2341,14 @@
"hasComments": "Hat Kommentare",
"showingPhotos": "Fotos gesamt",
"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": {
"button": "Exportieren",
@@ -2241,8 +2356,8 @@
"error": "Export fehlgeschlagen: ",
"exportFiltered": "Gefilterte Fotos exportieren",
"hint": "Fotos auswählen oder Filter anwenden zum Exportieren",
"exportSelected_one": "",
"exportSelected_other": ""
"exportSelected_one": "{{count}} ausgewähltes exportieren",
"exportSelected_other": "{{count}} ausgewählte exportieren"
},
"clientAccess": {
"title": "Kundenzugang",
@@ -2324,15 +2439,15 @@
"dateTaken": "Aufnahmedatum"
},
"photos": {
"moveToCategory_one": "",
"moveToCategory_other": "",
"selectCategory": "",
"uncategorized": "",
"movePhotos": "",
"selectedCategory": "",
"movedToCategory_one": "",
"movedToCategory_other": "",
"moveToCategoryFailed": "",
"moveToCategory": ""
"moveToCategory_one": "{{count}} Foto in Kategorie verschieben",
"moveToCategory_other": "{{count}} Fotos in Kategorie verschieben",
"selectCategory": "Kategorie auswählen",
"uncategorized": "Ohne Kategorie",
"movePhotos": "Fotos verschieben",
"selectedCategory": "Ausgewählte Kategorie",
"movedToCategory_one": "{{count}} Foto nach {{category}} verschoben",
"movedToCategory_other": "{{count}} Fotos nach {{category}} verschoben",
"moveToCategoryFailed": "Fotos konnten nicht in die Kategorie verschoben werden",
"moveToCategory": "In Kategorie verschieben"
}
}
+120 -5
View File
@@ -158,7 +158,14 @@
"navigation": {
"dashboard": "Dashboard",
"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": {
"title": "Archives",
@@ -1295,7 +1302,37 @@
"noTemplate": "No Template",
"noTemplateDescription": "Use only theme settings without a CSS template",
"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": {
"title": "Admin Panel",
@@ -1309,6 +1346,9 @@
"archivedEvents": "Archived Events",
"systemHealth": "System Health",
"health": {
"healthy": "Healthy",
"warning": "Warning",
"error": "Error",
"checking": "Checking..."
},
"updates": {
@@ -1465,8 +1505,70 @@
"loadingDetail": "Loading selections...",
"detail": {
"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": {
@@ -1799,7 +1901,13 @@
},
"storageDestination": "Storage Destination",
"backupType": "{{type}} backup",
"noDestinationSet": "No destination set"
"noDestinationSet": "No destination set",
"healthStatus": {
"excellent": "Excellent",
"good": "Good",
"warning": "Warning",
"critical": "Critical"
}
},
"configuration": {
"enableBackup": "Enable Automated Backups",
@@ -2254,7 +2362,14 @@
"hasComments": "Has comments",
"showingPhotos": "Total photos",
"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": {
"title": "Admin Login",
+121 -6
View File
@@ -161,7 +161,14 @@
"navigation": {
"dashboard": "Tableau de bord",
"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": {
"title": "Archives",
@@ -1307,7 +1314,37 @@
"noTemplate": "Aucun modèle",
"noTemplateDescription": "Utiliser uniquement les paramètres de thème sans modèle CSS",
"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": {
"title": "Panneau d'administration",
@@ -1321,7 +1358,10 @@
"archivedEvents": "Événements archivés",
"systemHealth": "Santé système",
"health": {
"checking": "Vérification..."
"checking": "Vérification...",
"healthy": "Sain",
"warning": "Avertissement",
"error": "Erreur"
},
"updates": {
"available": "Mise à jour disponible",
@@ -1475,7 +1515,17 @@
"loadingDetail": "Chargement des sélections…",
"detail": {
"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": {
@@ -1487,6 +1537,58 @@
"processingStatus": "En cours…",
"processingFailed": "Échoué",
"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": {
@@ -1819,7 +1921,13 @@
},
"storageDestination": "Destination",
"backupType": "Sauvegarde {{type}}",
"noDestinationSet": "Pas de destination"
"noDestinationSet": "Pas de destination",
"healthStatus": {
"warning": "Avertissement",
"excellent": "Excellent",
"good": "Bon",
"critical": "Critique"
}
},
"configuration": {
"enableBackup": "Activer les sauvegardes auto",
@@ -2279,7 +2387,14 @@
"hasComments": "Avec Commentaires",
"combineWith": "Combiner avec",
"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": {
"title": "Connexion Admin",
+121 -6
View File
@@ -158,7 +158,14 @@
"navigation": {
"dashboard": "Dashboard",
"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": {
"title": "Archieven",
@@ -1295,7 +1302,37 @@
"noTemplate": "Geen sjabloon",
"noTemplateDescription": "Alleen thema-instellingen gebruiken zonder CSS-sjabloon",
"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": {
"title": "Beheerpaneel",
@@ -1309,7 +1346,10 @@
"archivedEvents": "Gearchiveerde evenementen",
"systemHealth": "Systeemgezondheid",
"health": {
"checking": "Controleren..."
"checking": "Controleren...",
"healthy": "Gezond",
"warning": "Waarschuwing",
"error": "Fout"
},
"updates": {
"available": "Update beschikbaar",
@@ -1465,8 +1505,70 @@
"loadingDetail": "Selecties laden…",
"detail": {
"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": {
@@ -1799,7 +1901,13 @@
},
"storageDestination": "Opslagbestemming",
"backupType": "{{type}} back-up",
"noDestinationSet": "Geen bestemming ingesteld"
"noDestinationSet": "Geen bestemming ingesteld",
"healthStatus": {
"warning": "Waarschuwing",
"excellent": "Uitstekend",
"good": "Goed",
"critical": "Kritiek"
}
},
"configuration": {
"enableBackup": "Geautomatiseerde back-ups inschakelen",
@@ -2254,7 +2362,14 @@
"hasComments": "Heeft opmerkingen",
"showingPhotos": "Totaal foto's",
"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": {
"button": "Exporteren",
+121 -6
View File
@@ -161,7 +161,14 @@
"navigation": {
"dashboard": "Painel",
"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": {
"title": "Arquivos",
@@ -1312,7 +1319,37 @@
"noTemplate": "Sem modelo",
"noTemplateDescription": "Usar apenas as definições de tema sem modelo CSS",
"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": {
"title": "Painel Admin",
@@ -1326,7 +1363,10 @@
"archivedEvents": "Eventos Arquivados",
"systemHealth": "Saúde do Sistema",
"health": {
"checking": "Verificando..."
"checking": "Verificando...",
"healthy": "Saudável",
"warning": "Aviso",
"error": "Erro"
},
"updates": {
"available": "Atualização Disponível",
@@ -1490,8 +1530,70 @@
"loadingDetail": "A carregar seleções…",
"detail": {
"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": {
@@ -1824,7 +1926,13 @@
},
"storageDestination": "Destino do Armazenamento",
"backupType": "Backup {{type}}",
"noDestinationSet": "Nenhum destino definido"
"noDestinationSet": "Nenhum destino definido",
"healthStatus": {
"warning": "Aviso",
"excellent": "Excelente",
"good": "Bom",
"critical": "Crítico"
}
},
"configuration": {
"enableBackup": "Ativar Backups Automáticos",
@@ -2284,7 +2392,14 @@
"hasComments": "Possui comentários",
"showingPhotos": "Total de fotos",
"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": {
"title": "Login Administrativo",
+121 -6
View File
@@ -164,7 +164,14 @@
"navigation": {
"dashboard": "Панель управления",
"events": "События",
"settings": "Настройки"
"settings": "Настройки",
"archives": "Архивы",
"emailSettings": "Настройки e-mail",
"branding": "Брендинг",
"eventTypes": "Типы событий",
"backup": "Резервное копирование",
"cmsPages": "CMS-страницы",
"users": "Пользователи"
},
"archives": {
"title": "Архивы",
@@ -1329,7 +1336,37 @@
"noTemplate": "Без шаблона",
"noTemplateDescription": "Использовать только настройки темы без CSS-шаблона",
"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": {
"title": "Панель администратора",
@@ -1343,7 +1380,10 @@
"archivedEvents": "Архивированных событий",
"systemHealth": "Состояние системы",
"health": {
"checking": "Проверка..."
"checking": "Проверка...",
"healthy": "Исправно",
"warning": "Предупреждение",
"error": "Ошибка"
},
"updates": {
"available": "Доступно обновление",
@@ -1515,8 +1555,70 @@
"loadingDetail": "Загрузка выборок…",
"detail": {
"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": {
@@ -1849,7 +1951,13 @@
},
"storageDestination": "Место хранения",
"backupType": "Резервная копия {{type}}",
"noDestinationSet": "Место хранения не задано"
"noDestinationSet": "Место хранения не задано",
"healthStatus": {
"warning": "Предупреждение",
"excellent": "Отлично",
"good": "Хорошо",
"critical": "Критически"
}
},
"configuration": {
"enableBackup": "Включить автоматическое резервное копирование",
@@ -2314,7 +2422,14 @@
"hasComments": "Есть комментарии",
"showingPhotos": "Всего фото",
"withRatings": "С оценками",
"combineWith": "Объединить с"
"combineWith": "Объединить с",
"allPhotos": "Все фото",
"anyRating": "Любая оценка",
"oneStarPlus": "1+ звезда",
"twoStarsPlus": "2+ звезды",
"threeStarsPlus": "3+ звезды",
"fourStarsPlus": "4+ звезды",
"fiveStarsOnly": "Только 5 звёзд"
},
"adminLogin": {
"title": "Вход для администратора",
+6 -7
View File
@@ -20,7 +20,7 @@ import { Button, Card, Loading } from '../../components/common';
import { UpdateNotification } from '../../components/admin/UpdateNotification';
import { useQuery } from '@tanstack/react-query';
import { eventsService } from '../../services/events.service';
import { adminService } from '../../services/admin.service';
import { adminService, ActivityType } from '../../services/admin.service';
interface StatCard {
title: string;
@@ -246,8 +246,8 @@ export const AdminDashboard: React.FC = () => {
) : (
recentActivity.slice(0, 5).map((activity) => {
// Get color based on activity type
const getActivityColor = (type: string) => {
const colors: Record<string, string> = {
const getActivityColor = (type: ActivityType) => {
const colors: Partial<Record<ActivityType, string>> = {
'event_created': 'bg-green-500',
'photos_uploaded': 'bg-blue-500',
'event_archived': 'bg-purple-500',
@@ -264,21 +264,20 @@ export const AdminDashboard: React.FC = () => {
// Format activity message with translations
const getActivityMessage = (): string => {
const translationKey = `admin.activities.${activity.type}`;
const params: Record<string, any> = {
eventName: activity.eventName || t('common.unknown'),
count: activity.metadata?.count || 0,
template: activity.metadata?.template_key || '',
categoryName: activity.metadata?.category_name || ''
};
const translated = t(`admin.activities.${activity.type}`, params);
// Translate; if key missing i18n returns the key string itself
const translated = t(translationKey, params) as string;
if (!translated || translated === translationKey) {
if (!translated || translated === `admin.activities.${activity.type}`) {
// Fallback: format a readable English message
return adminService.formatActivityMessage(activity);
}
return translated;
return translated as string;
};
return (
-3
View File
@@ -1,3 +0,0 @@
import type { ComponentType } from 'react';
export const BackupManagement: ComponentType<any>;
@@ -7,20 +7,9 @@ import {
RefreshCw,
AlertCircle,
CheckCircle,
Calendar,
Database,
FileArchive,
Cloud,
Server,
Loader2,
Info,
Shield,
Clock,
Download,
Upload,
Trash2,
Search,
Filter
Loader2,
Shield,
} from 'lucide-react';
import { toast } from 'react-toastify';
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 { api } from '../../config/api';
// Tab components will be defined inside the component to use translations
type TabId = 'dashboard' | 'configuration' | 'history' | 'restore';
export const BackupManagement = () => {
const [activeTab, setActiveTab] = useState('dashboard');
export const BackupManagement: React.FC = () => {
const [activeTab, setActiveTab] = useState<TabId>('dashboard');
const queryClient = useQueryClient();
const { t } = useTranslation();
// Tab components with translations
const tabs = [
{ id: 'dashboard', label: t('backup.tabs.dashboard'), icon: HardDrive },
{ id: 'configuration', label: t('backup.tabs.configuration'), icon: Settings },
{ id: 'history', label: t('backup.tabs.history'), icon: History },
{ id: 'restore', label: t('backup.tabs.restore'), icon: RefreshCw }
{ id: 'dashboard' as const, label: t('backup.tabs.dashboard'), icon: HardDrive },
{ id: 'configuration' as const, label: t('backup.tabs.configuration'), icon: Settings },
{ id: 'history' as const, label: t('backup.tabs.history'), icon: History },
{ id: 'restore' as const, label: t('backup.tabs.restore'), icon: RefreshCw },
];
// Fetch backup status
const { data: backupStatus, isLoading: statusLoading } = useQuery({
queryKey: ['backup-status'],
queryFn: async () => {
const response = await api.get('/admin/backup/status');
return response.data;
},
refetchInterval: 10000 // Refresh every 10 seconds
refetchInterval: 10000,
});
// Fetch backup configuration
const { data: backupConfig, isLoading: configLoading } = useQuery({
queryKey: ['backup-config'],
queryFn: async () => {
const response = await api.get('/admin/backup/config');
return response.data;
}
},
});
// Trigger manual backup
const manualBackupMutation = useMutation({
mutationFn: async () => {
const response = await api.post('/admin/backup/run');
@@ -78,15 +63,14 @@ export const BackupManagement = () => {
toast.success(t('backup.messages.backupStarted'));
queryClient.invalidateQueries({ queryKey: ['backup-status'] });
},
onError: (error) => {
onError: (error: any) => {
const message = error.response?.data?.error || t('backup.messages.backupFailed');
toast.error(message);
}
},
});
// Update configuration
const updateConfigMutation = useMutation({
mutationFn: async (config) => {
mutationFn: async (config: unknown) => {
const response = await api.put('/admin/backup/config', config);
return response.data;
},
@@ -94,10 +78,10 @@ export const BackupManagement = () => {
toast.success(t('backup.messages.configUpdated'));
queryClient.invalidateQueries({ queryKey: ['backup-config'] });
},
onError: (error) => {
onError: (error: any) => {
const message = error.response?.data?.error || t('backup.messages.configUpdateFailed');
toast.error(message);
}
},
});
if (statusLoading || configLoading) {
@@ -142,7 +126,7 @@ export const BackupManagement = () => {
</>
)}
</div>
{backupConfig?.backup_enabled && (
<div className="flex items-center space-x-2">
<Clock className="h-5 w-5 text-neutral-400" />
@@ -156,11 +140,11 @@ export const BackupManagement = () => {
<div className="flex items-center space-x-3">
<Button
onClick={() => manualBackupMutation.mutate()}
disabled={backupStatus?.isRunning || manualBackupMutation.isLoading}
disabled={backupStatus?.isRunning || manualBackupMutation.isPending}
variant="secondary"
size="sm"
>
{manualBackupMutation.isLoading ? (
{manualBackupMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('backup.actions.starting')}
@@ -172,7 +156,7 @@ export const BackupManagement = () => {
</>
)}
</Button>
<div className={`flex items-center space-x-1 px-3 py-1 rounded-full text-sm font-medium ${
backupConfig?.backup_enabled
? 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300'
@@ -213,30 +197,30 @@ export const BackupManagement = () => {
{/* Tab Content */}
<div className="mt-6">
{activeTab === 'dashboard' && (
<BackupDashboard
status={backupStatus}
<BackupDashboard
status={backupStatus}
config={backupConfig}
onRunBackup={() => manualBackupMutation.mutate()}
isBackupRunning={backupStatus?.isRunning || manualBackupMutation.isLoading}
isBackupRunning={backupStatus?.isRunning || manualBackupMutation.isPending}
/>
)}
{activeTab === 'configuration' && (
<BackupConfiguration
config={backupConfig}
onSave={(newConfig) => updateConfigMutation.mutate(newConfig)}
isSaving={updateConfigMutation.isLoading}
onSave={(newConfig: unknown) => updateConfigMutation.mutate(newConfig)}
isSaving={updateConfigMutation.isPending}
/>
)}
{activeTab === 'history' && (
<BackupHistory />
)}
{activeTab === 'restore' && (
<RestoreWizard />
)}
</div>
</div>
);
};
};
+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 {
id: number;
type: string;
type: ActivityType;
actorType: string;
actorName: string;
eventName?: string;