Spaces:
Running
Running
with google translator
Browse files- frontend/index.html +12 -0
- frontend/package-lock.json +96 -3
- frontend/package.json +3 -1
- frontend/src/components/analysis/EnergyDemands.tsx +14 -12
- frontend/src/components/analysis/StreamSelector.tsx +8 -6
- frontend/src/components/layout/AppShell.tsx +28 -6
- frontend/src/i18n.ts +26 -0
- frontend/src/index.css +19 -0
- frontend/src/locales/de.json +81 -0
- frontend/src/locales/en.json +81 -0
- frontend/src/main.tsx +1 -0
- frontend/src/pages/HomePage.tsx +19 -23
- frontend/src/pages/PotentialAnalysisPage.tsx +13 -11
frontend/index.html
CHANGED
|
@@ -13,6 +13,18 @@
|
|
| 13 |
</head>
|
| 14 |
|
| 15 |
<body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
<div id="root"></div>
|
| 17 |
<script type="module" src="/src/main.tsx"></script>
|
| 18 |
</body>
|
|
|
|
| 13 |
</head>
|
| 14 |
|
| 15 |
<body>
|
| 16 |
+
<div id="google_translate_element" style="display:none;"></div>
|
| 17 |
+
<script type="text/javascript">
|
| 18 |
+
function googleTranslateElementInit() {
|
| 19 |
+
new google.translate.TranslateElement({
|
| 20 |
+
pageLanguage: 'en',
|
| 21 |
+
includedLanguages: 'de,en',
|
| 22 |
+
autoDisplay: false
|
| 23 |
+
}, 'google_translate_element');
|
| 24 |
+
}
|
| 25 |
+
</script>
|
| 26 |
+
<script type="text/javascript" src="https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script>
|
| 27 |
+
|
| 28 |
<div id="root"></div>
|
| 29 |
<script type="module" src="/src/main.tsx"></script>
|
| 30 |
</body>
|
frontend/package-lock.json
CHANGED
|
@@ -1,19 +1,21 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "
|
| 3 |
"version": "0.0.0",
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
-
"name": "
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@types/leaflet": "^1.9.21",
|
| 12 |
"axios": "^1.13.5",
|
|
|
|
| 13 |
"leaflet": "^1.9.4",
|
| 14 |
"plotly.js": "^3.4.0",
|
| 15 |
"react": "^19.2.0",
|
| 16 |
"react-dom": "^19.2.0",
|
|
|
|
| 17 |
"react-leaflet": "^5.0.0",
|
| 18 |
"react-plotly.js": "^2.6.0",
|
| 19 |
"react-router-dom": "^7.13.1",
|
|
@@ -268,6 +270,15 @@
|
|
| 268 |
"@babel/core": "^7.0.0-0"
|
| 269 |
}
|
| 270 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
"node_modules/@babel/template": {
|
| 272 |
"version": "7.28.6",
|
| 273 |
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
|
@@ -4101,6 +4112,43 @@
|
|
| 4101 |
"hermes-estree": "0.25.1"
|
| 4102 |
}
|
| 4103 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4104 |
"node_modules/iconv-lite": {
|
| 4105 |
"version": "0.4.24",
|
| 4106 |
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
|
@@ -5190,6 +5238,33 @@
|
|
| 5190 |
"react": "^19.2.4"
|
| 5191 |
}
|
| 5192 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5193 |
"node_modules/react-is": {
|
| 5194 |
"version": "16.13.1",
|
| 5195 |
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
|
@@ -5906,7 +5981,7 @@
|
|
| 5906 |
"version": "5.9.3",
|
| 5907 |
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
| 5908 |
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
| 5909 |
-
"
|
| 5910 |
"license": "Apache-2.0",
|
| 5911 |
"bin": {
|
| 5912 |
"tsc": "bin/tsc",
|
|
@@ -6000,6 +6075,15 @@
|
|
| 6000 |
"punycode": "^2.1.0"
|
| 6001 |
}
|
| 6002 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6003 |
"node_modules/util-deprecate": {
|
| 6004 |
"version": "1.0.2",
|
| 6005 |
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
|
@@ -6081,6 +6165,15 @@
|
|
| 6081 |
}
|
| 6082 |
}
|
| 6083 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6084 |
"node_modules/vt-pbf": {
|
| 6085 |
"version": "3.1.3",
|
| 6086 |
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "heattransplan",
|
| 3 |
"version": "0.0.0",
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
+
"name": "heattransplan",
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@types/leaflet": "^1.9.21",
|
| 12 |
"axios": "^1.13.5",
|
| 13 |
+
"i18next": "^26.1.0",
|
| 14 |
"leaflet": "^1.9.4",
|
| 15 |
"plotly.js": "^3.4.0",
|
| 16 |
"react": "^19.2.0",
|
| 17 |
"react-dom": "^19.2.0",
|
| 18 |
+
"react-i18next": "^17.0.7",
|
| 19 |
"react-leaflet": "^5.0.0",
|
| 20 |
"react-plotly.js": "^2.6.0",
|
| 21 |
"react-router-dom": "^7.13.1",
|
|
|
|
| 270 |
"@babel/core": "^7.0.0-0"
|
| 271 |
}
|
| 272 |
},
|
| 273 |
+
"node_modules/@babel/runtime": {
|
| 274 |
+
"version": "7.29.2",
|
| 275 |
+
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
| 276 |
+
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
| 277 |
+
"license": "MIT",
|
| 278 |
+
"engines": {
|
| 279 |
+
"node": ">=6.9.0"
|
| 280 |
+
}
|
| 281 |
+
},
|
| 282 |
"node_modules/@babel/template": {
|
| 283 |
"version": "7.28.6",
|
| 284 |
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
|
|
|
| 4112 |
"hermes-estree": "0.25.1"
|
| 4113 |
}
|
| 4114 |
},
|
| 4115 |
+
"node_modules/html-parse-stringify": {
|
| 4116 |
+
"version": "3.0.1",
|
| 4117 |
+
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
| 4118 |
+
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
| 4119 |
+
"license": "MIT",
|
| 4120 |
+
"dependencies": {
|
| 4121 |
+
"void-elements": "3.1.0"
|
| 4122 |
+
}
|
| 4123 |
+
},
|
| 4124 |
+
"node_modules/i18next": {
|
| 4125 |
+
"version": "26.1.0",
|
| 4126 |
+
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.1.0.tgz",
|
| 4127 |
+
"integrity": "sha512-dIU6td04DvQuIqVst5S9g0GviTmhZ0DYD4b9ociVGJmuCa5vZ2de/t+Enf4olvj87mF8Y2lwjNQBwC9QZsvzKQ==",
|
| 4128 |
+
"funding": [
|
| 4129 |
+
{
|
| 4130 |
+
"type": "individual",
|
| 4131 |
+
"url": "https://www.locize.com/i18next"
|
| 4132 |
+
},
|
| 4133 |
+
{
|
| 4134 |
+
"type": "individual",
|
| 4135 |
+
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
| 4136 |
+
},
|
| 4137 |
+
{
|
| 4138 |
+
"type": "individual",
|
| 4139 |
+
"url": "https://www.locize.com"
|
| 4140 |
+
}
|
| 4141 |
+
],
|
| 4142 |
+
"license": "MIT",
|
| 4143 |
+
"peerDependencies": {
|
| 4144 |
+
"typescript": "^5 || ^6"
|
| 4145 |
+
},
|
| 4146 |
+
"peerDependenciesMeta": {
|
| 4147 |
+
"typescript": {
|
| 4148 |
+
"optional": true
|
| 4149 |
+
}
|
| 4150 |
+
}
|
| 4151 |
+
},
|
| 4152 |
"node_modules/iconv-lite": {
|
| 4153 |
"version": "0.4.24",
|
| 4154 |
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
|
|
|
| 5238 |
"react": "^19.2.4"
|
| 5239 |
}
|
| 5240 |
},
|
| 5241 |
+
"node_modules/react-i18next": {
|
| 5242 |
+
"version": "17.0.7",
|
| 5243 |
+
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.7.tgz",
|
| 5244 |
+
"integrity": "sha512-rwtPXsb/zwzDafN+gytcjF5YnqGQQIRmCQ6DctBC1VSipRB8GD/MWEVrFP42vjMyuYydxWxM8CZRt+yiNuuoHg==",
|
| 5245 |
+
"license": "MIT",
|
| 5246 |
+
"dependencies": {
|
| 5247 |
+
"@babel/runtime": "^7.29.2",
|
| 5248 |
+
"html-parse-stringify": "^3.0.1",
|
| 5249 |
+
"use-sync-external-store": "^1.6.0"
|
| 5250 |
+
},
|
| 5251 |
+
"peerDependencies": {
|
| 5252 |
+
"i18next": ">= 26.0.10",
|
| 5253 |
+
"react": ">= 16.8.0",
|
| 5254 |
+
"typescript": "^5 || ^6"
|
| 5255 |
+
},
|
| 5256 |
+
"peerDependenciesMeta": {
|
| 5257 |
+
"react-dom": {
|
| 5258 |
+
"optional": true
|
| 5259 |
+
},
|
| 5260 |
+
"react-native": {
|
| 5261 |
+
"optional": true
|
| 5262 |
+
},
|
| 5263 |
+
"typescript": {
|
| 5264 |
+
"optional": true
|
| 5265 |
+
}
|
| 5266 |
+
}
|
| 5267 |
+
},
|
| 5268 |
"node_modules/react-is": {
|
| 5269 |
"version": "16.13.1",
|
| 5270 |
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
|
|
|
| 5981 |
"version": "5.9.3",
|
| 5982 |
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
| 5983 |
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
| 5984 |
+
"devOptional": true,
|
| 5985 |
"license": "Apache-2.0",
|
| 5986 |
"bin": {
|
| 5987 |
"tsc": "bin/tsc",
|
|
|
|
| 6075 |
"punycode": "^2.1.0"
|
| 6076 |
}
|
| 6077 |
},
|
| 6078 |
+
"node_modules/use-sync-external-store": {
|
| 6079 |
+
"version": "1.6.0",
|
| 6080 |
+
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
| 6081 |
+
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
| 6082 |
+
"license": "MIT",
|
| 6083 |
+
"peerDependencies": {
|
| 6084 |
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 6085 |
+
}
|
| 6086 |
+
},
|
| 6087 |
"node_modules/util-deprecate": {
|
| 6088 |
"version": "1.0.2",
|
| 6089 |
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
|
|
|
| 6165 |
}
|
| 6166 |
}
|
| 6167 |
},
|
| 6168 |
+
"node_modules/void-elements": {
|
| 6169 |
+
"version": "3.1.0",
|
| 6170 |
+
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
| 6171 |
+
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
| 6172 |
+
"license": "MIT",
|
| 6173 |
+
"engines": {
|
| 6174 |
+
"node": ">=0.10.0"
|
| 6175 |
+
}
|
| 6176 |
+
},
|
| 6177 |
"node_modules/vt-pbf": {
|
| 6178 |
"version": "3.1.3",
|
| 6179 |
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
|
frontend/package.json
CHANGED
|
@@ -12,10 +12,12 @@
|
|
| 12 |
"dependencies": {
|
| 13 |
"@types/leaflet": "^1.9.21",
|
| 14 |
"axios": "^1.13.5",
|
|
|
|
| 15 |
"leaflet": "^1.9.4",
|
| 16 |
"plotly.js": "^3.4.0",
|
| 17 |
"react": "^19.2.0",
|
| 18 |
"react-dom": "^19.2.0",
|
|
|
|
| 19 |
"react-leaflet": "^5.0.0",
|
| 20 |
"react-plotly.js": "^2.6.0",
|
| 21 |
"react-router-dom": "^7.13.1",
|
|
@@ -35,4 +37,4 @@
|
|
| 35 |
"typescript-eslint": "^8.48.0",
|
| 36 |
"vite": "^7.3.1"
|
| 37 |
}
|
| 38 |
-
}
|
|
|
|
| 12 |
"dependencies": {
|
| 13 |
"@types/leaflet": "^1.9.21",
|
| 14 |
"axios": "^1.13.5",
|
| 15 |
+
"i18next": "^26.1.0",
|
| 16 |
"leaflet": "^1.9.4",
|
| 17 |
"plotly.js": "^3.4.0",
|
| 18 |
"react": "^19.2.0",
|
| 19 |
"react-dom": "^19.2.0",
|
| 20 |
+
"react-i18next": "^17.0.7",
|
| 21 |
"react-leaflet": "^5.0.0",
|
| 22 |
"react-plotly.js": "^2.6.0",
|
| 23 |
"react-router-dom": "^7.13.1",
|
|
|
|
| 37 |
"typescript-eslint": "^8.48.0",
|
| 38 |
"vite": "^7.3.1"
|
| 39 |
}
|
| 40 |
+
}
|
frontend/src/components/analysis/EnergyDemands.tsx
CHANGED
|
@@ -5,8 +5,10 @@ import { useProjectStore } from '../../store/projectStore';
|
|
| 5 |
import { useAnalysisStore } from '../../store/analysisStore';
|
| 6 |
import { getStreamInfo } from '../../utils/streamUtils';
|
| 7 |
import ChartHelpButton from '../ui/ChartHelpButton';
|
|
|
|
| 8 |
|
| 9 |
export default function EnergyDemands() {
|
|
|
|
| 10 |
const processes = useProjectStore((s) => s.state.processes);
|
| 11 |
const energyDemands = useAnalysisStore((s) => s.energyDemands);
|
| 12 |
const updateEnergyDemand = useAnalysisStore((s) => s.updateEnergyDemand);
|
|
@@ -20,7 +22,7 @@ export default function EnergyDemands() {
|
|
| 20 |
processes.forEach((proc, pi) => {
|
| 21 |
(proc.streams ?? []).forEach((stream, si) => {
|
| 22 |
const info = getStreamInfo(stream);
|
| 23 |
-
const name = stream.name || `
|
| 24 |
if (info.type?.includes('Hot')) hot.push(name);
|
| 25 |
else if (info.type?.includes('Cold')) cold.push(name);
|
| 26 |
});
|
|
@@ -83,8 +85,8 @@ export default function EnergyDemands() {
|
|
| 83 |
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
|
| 84 |
>
|
| 85 |
<span style={{ display: 'flex', alignItems: 'center' }}>
|
| 86 |
-
|
| 87 |
-
<ChartHelpButton inline title=
|
| 88 |
</span>
|
| 89 |
</h4>
|
| 90 |
|
|
@@ -92,9 +94,9 @@ export default function EnergyDemands() {
|
|
| 92 |
{energyDemands.map((demand, i) => (
|
| 93 |
<div key={i} className="pa-demand-card">
|
| 94 |
<div className="pa-demand-header">
|
| 95 |
-
<strong>
|
| 96 |
{i > 0 && (
|
| 97 |
-
<button className="pa-btn-icon" onClick={() => removeEnergyDemand(i)} title=
|
| 98 |
🗑️
|
| 99 |
</button>
|
| 100 |
)}
|
|
@@ -102,7 +104,7 @@ export default function EnergyDemands() {
|
|
| 102 |
|
| 103 |
{/* Heat supply */}
|
| 104 |
<label className="pa-input-label">
|
| 105 |
-
|
| 106 |
<input
|
| 107 |
type="number"
|
| 108 |
min={0}
|
|
@@ -115,7 +117,7 @@ export default function EnergyDemands() {
|
|
| 115 |
/>
|
| 116 |
</label>
|
| 117 |
<details className="pa-details" open>
|
| 118 |
-
<summary>
|
| 119 |
<div className="pa-multi-select">
|
| 120 |
{hotNames.map((name) => (
|
| 121 |
<label key={name} className="pa-multi-option" title={name}>
|
|
@@ -127,13 +129,13 @@ export default function EnergyDemands() {
|
|
| 127 |
{name}
|
| 128 |
</label>
|
| 129 |
))}
|
| 130 |
-
{hotNames.length === 0 && <span className="pa-muted">
|
| 131 |
</div>
|
| 132 |
</details>
|
| 133 |
|
| 134 |
{/* Cooling supply */}
|
| 135 |
<label className="pa-input-label">
|
| 136 |
-
|
| 137 |
<input
|
| 138 |
type="number"
|
| 139 |
min={0}
|
|
@@ -146,7 +148,7 @@ export default function EnergyDemands() {
|
|
| 146 |
/>
|
| 147 |
</label>
|
| 148 |
<details className="pa-details" open>
|
| 149 |
-
<summary>
|
| 150 |
<div className="pa-multi-select">
|
| 151 |
{coldNames.map((name) => (
|
| 152 |
<label key={name} className="pa-multi-option" title={name}>
|
|
@@ -158,7 +160,7 @@ export default function EnergyDemands() {
|
|
| 158 |
{name}
|
| 159 |
</label>
|
| 160 |
))}
|
| 161 |
-
{coldNames.length === 0 && <span className="pa-muted">
|
| 162 |
</div>
|
| 163 |
</details>
|
| 164 |
|
|
@@ -167,7 +169,7 @@ export default function EnergyDemands() {
|
|
| 167 |
))}
|
| 168 |
|
| 169 |
<button className="pa-btn" onClick={handleAddSupply}>
|
| 170 |
-
➕
|
| 171 |
</button>
|
| 172 |
</>
|
| 173 |
</div>
|
|
|
|
| 5 |
import { useAnalysisStore } from '../../store/analysisStore';
|
| 6 |
import { getStreamInfo } from '../../utils/streamUtils';
|
| 7 |
import ChartHelpButton from '../ui/ChartHelpButton';
|
| 8 |
+
import { useTranslation } from 'react-i18next';
|
| 9 |
|
| 10 |
export default function EnergyDemands() {
|
| 11 |
+
const { t } = useTranslation();
|
| 12 |
const processes = useProjectStore((s) => s.state.processes);
|
| 13 |
const energyDemands = useAnalysisStore((s) => s.energyDemands);
|
| 14 |
const updateEnergyDemand = useAnalysisStore((s) => s.updateEnergyDemand);
|
|
|
|
| 22 |
processes.forEach((proc, pi) => {
|
| 23 |
(proc.streams ?? []).forEach((stream, si) => {
|
| 24 |
const info = getStreamInfo(stream);
|
| 25 |
+
const name = stream.name || `${t('stream_selector.stream')} ${si + 1} in ${proc.name || `${t('stream_selector.subprocess')} ${pi + 1}`}`;
|
| 26 |
if (info.type?.includes('Hot')) hot.push(name);
|
| 27 |
else if (info.type?.includes('Cold')) cold.push(name);
|
| 28 |
});
|
|
|
|
| 85 |
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
|
| 86 |
>
|
| 87 |
<span style={{ display: 'flex', alignItems: 'center' }}>
|
| 88 |
+
{t('energy_demands.title')}
|
| 89 |
+
<ChartHelpButton inline title={t('energy_demands.title')} description={t('energy_demands.help_desc')} />
|
| 90 |
</span>
|
| 91 |
</h4>
|
| 92 |
|
|
|
|
| 94 |
{energyDemands.map((demand, i) => (
|
| 95 |
<div key={i} className="pa-demand-card">
|
| 96 |
<div className="pa-demand-header">
|
| 97 |
+
<strong>{t('energy_demands.supply')} {i + 1}</strong>
|
| 98 |
{i > 0 && (
|
| 99 |
+
<button className="pa-btn-icon" onClick={() => removeEnergyDemand(i)} title={t('energy_demands.remove')}>
|
| 100 |
🗑️
|
| 101 |
</button>
|
| 102 |
)}
|
|
|
|
| 104 |
|
| 105 |
{/* Heat supply */}
|
| 106 |
<label className="pa-input-label">
|
| 107 |
+
{t('energy_demands.heat_kw')}
|
| 108 |
<input
|
| 109 |
type="number"
|
| 110 |
min={0}
|
|
|
|
| 117 |
/>
|
| 118 |
</label>
|
| 119 |
<details className="pa-details" open>
|
| 120 |
+
<summary>{t('energy_demands.hot_selection')}</summary>
|
| 121 |
<div className="pa-multi-select">
|
| 122 |
{hotNames.map((name) => (
|
| 123 |
<label key={name} className="pa-multi-option" title={name}>
|
|
|
|
| 129 |
{name}
|
| 130 |
</label>
|
| 131 |
))}
|
| 132 |
+
{hotNames.length === 0 && <span className="pa-muted">{t('energy_demands.no_hot')}</span>}
|
| 133 |
</div>
|
| 134 |
</details>
|
| 135 |
|
| 136 |
{/* Cooling supply */}
|
| 137 |
<label className="pa-input-label">
|
| 138 |
+
{t('energy_demands.cooling_kw')}
|
| 139 |
<input
|
| 140 |
type="number"
|
| 141 |
min={0}
|
|
|
|
| 148 |
/>
|
| 149 |
</label>
|
| 150 |
<details className="pa-details" open>
|
| 151 |
+
<summary>{t('energy_demands.cold_selection')}</summary>
|
| 152 |
<div className="pa-multi-select">
|
| 153 |
{coldNames.map((name) => (
|
| 154 |
<label key={name} className="pa-multi-option" title={name}>
|
|
|
|
| 160 |
{name}
|
| 161 |
</label>
|
| 162 |
))}
|
| 163 |
+
{coldNames.length === 0 && <span className="pa-muted">{t('energy_demands.no_cold')}</span>}
|
| 164 |
</div>
|
| 165 |
</details>
|
| 166 |
|
|
|
|
| 169 |
))}
|
| 170 |
|
| 171 |
<button className="pa-btn" onClick={handleAddSupply}>
|
| 172 |
+
➕ {t('energy_demands.add_supply')}
|
| 173 |
</button>
|
| 174 |
</>
|
| 175 |
</div>
|
frontend/src/components/analysis/StreamSelector.tsx
CHANGED
|
@@ -6,8 +6,10 @@ import { useAnalysisStore } from '../../store/analysisStore';
|
|
| 6 |
import { getStreamInfo } from '../../utils/streamUtils';
|
| 7 |
import type { Stream } from '../../types/stream';
|
| 8 |
import ChartHelpButton from '../ui/ChartHelpButton';
|
|
|
|
| 9 |
|
| 10 |
export default function StreamSelector() {
|
|
|
|
| 11 |
const processes = useProjectStore((s) => s.state.processes);
|
| 12 |
const selectedStreams = useAnalysisStore((s) => s.selectedStreams);
|
| 13 |
const setSelectedStream = useAnalysisStore((s) => s.setSelectedStream);
|
|
@@ -41,7 +43,7 @@ export default function StreamSelector() {
|
|
| 41 |
);
|
| 42 |
|
| 43 |
if (!processes.length) {
|
| 44 |
-
return <div className="pa-info-box">
|
| 45 |
}
|
| 46 |
|
| 47 |
return (
|
|
@@ -51,8 +53,8 @@ export default function StreamSelector() {
|
|
| 51 |
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
|
| 52 |
>
|
| 53 |
<span style={{ display: 'flex', alignItems: 'center' }}>
|
| 54 |
-
|
| 55 |
-
<ChartHelpButton inline title=
|
| 56 |
</span>
|
| 57 |
</h4>
|
| 58 |
|
|
@@ -60,7 +62,7 @@ export default function StreamSelector() {
|
|
| 60 |
{processes.map((proc, pi) => {
|
| 61 |
const streams = proc.streams ?? [];
|
| 62 |
if (streams.length === 0) return null;
|
| 63 |
-
const procName = proc.name || `
|
| 64 |
return (
|
| 65 |
<div key={pi} className="pa-stream-group">
|
| 66 |
<div className="pa-stream-group-title">{procName}</div>
|
|
@@ -68,7 +70,7 @@ export default function StreamSelector() {
|
|
| 68 |
const key = `stream_${pi}_${si}`;
|
| 69 |
const checked = selectedStreams[key] ?? true;
|
| 70 |
const info = getStreamInfo(stream);
|
| 71 |
-
const streamName = stream.name || `
|
| 72 |
|
| 73 |
const parts: string[] = [];
|
| 74 |
if (info.tin !== null) parts.push(`Tin:${info.tin}°C`);
|
|
@@ -86,7 +88,7 @@ export default function StreamSelector() {
|
|
| 86 |
/>
|
| 87 |
<span className="pa-stream-name">{streamName}</span>
|
| 88 |
<span className="pa-stream-info">
|
| 89 |
-
{parts.length > 0 ? parts.join(' | ') :
|
| 90 |
</span>
|
| 91 |
</label>
|
| 92 |
);
|
|
|
|
| 6 |
import { getStreamInfo } from '../../utils/streamUtils';
|
| 7 |
import type { Stream } from '../../types/stream';
|
| 8 |
import ChartHelpButton from '../ui/ChartHelpButton';
|
| 9 |
+
import { useTranslation } from 'react-i18next';
|
| 10 |
|
| 11 |
export default function StreamSelector() {
|
| 12 |
+
const { t } = useTranslation();
|
| 13 |
const processes = useProjectStore((s) => s.state.processes);
|
| 14 |
const selectedStreams = useAnalysisStore((s) => s.selectedStreams);
|
| 15 |
const setSelectedStream = useAnalysisStore((s) => s.setSelectedStream);
|
|
|
|
| 43 |
);
|
| 44 |
|
| 45 |
if (!processes.length) {
|
| 46 |
+
return <div className="pa-info-box">{t('analysis.messages.no_processes')}</div>;
|
| 47 |
}
|
| 48 |
|
| 49 |
return (
|
|
|
|
| 53 |
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
|
| 54 |
>
|
| 55 |
<span style={{ display: 'flex', alignItems: 'center' }}>
|
| 56 |
+
{t('stream_selector.title')}
|
| 57 |
+
<ChartHelpButton inline title={t('stream_selector.title')} description={t('stream_selector.help_desc')} />
|
| 58 |
</span>
|
| 59 |
</h4>
|
| 60 |
|
|
|
|
| 62 |
{processes.map((proc, pi) => {
|
| 63 |
const streams = proc.streams ?? [];
|
| 64 |
if (streams.length === 0) return null;
|
| 65 |
+
const procName = proc.name || `${t('stream_selector.subprocess')} ${pi + 1}`;
|
| 66 |
return (
|
| 67 |
<div key={pi} className="pa-stream-group">
|
| 68 |
<div className="pa-stream-group-title">{procName}</div>
|
|
|
|
| 70 |
const key = `stream_${pi}_${si}`;
|
| 71 |
const checked = selectedStreams[key] ?? true;
|
| 72 |
const info = getStreamInfo(stream);
|
| 73 |
+
const streamName = stream.name || `${t('stream_selector.stream')} ${si + 1}`;
|
| 74 |
|
| 75 |
const parts: string[] = [];
|
| 76 |
if (info.tin !== null) parts.push(`Tin:${info.tin}°C`);
|
|
|
|
| 88 |
/>
|
| 89 |
<span className="pa-stream-name">{streamName}</span>
|
| 90 |
<span className="pa-stream-info">
|
| 91 |
+
{parts.length > 0 ? parts.join(' | ') : t('stream_selector.incomplete')}
|
| 92 |
</span>
|
| 93 |
</label>
|
| 94 |
);
|
frontend/src/components/layout/AppShell.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import { type ReactNode, useEffect } from 'react';
|
| 2 |
import { NavLink } from 'react-router-dom';
|
|
|
|
| 3 |
import { useUIStore } from '../../store/uiStore';
|
| 4 |
import './AppShell.css';
|
| 5 |
|
|
@@ -8,15 +9,33 @@ interface Props {
|
|
| 8 |
}
|
| 9 |
|
| 10 |
const NAV_ITEMS = [
|
| 11 |
-
{ path: '/',
|
| 12 |
-
{ path: '/data-collection',
|
| 13 |
-
{ path: '/potential-analysis',
|
| 14 |
] as const;
|
| 15 |
|
| 16 |
export default function AppShell({ children }: Props) {
|
|
|
|
| 17 |
const theme = useUIStore((s) => s.theme);
|
| 18 |
const toggleTheme = useUIStore((s) => s.toggleTheme);
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
// Sync dark mode class to HTML element
|
| 21 |
useEffect(() => {
|
| 22 |
console.log('[HeatTransPlan] Current Theme:', theme);
|
|
@@ -45,16 +64,19 @@ export default function AppShell({ children }: Props) {
|
|
| 45 |
`nav-item ${isActive ? 'active' : ''}`
|
| 46 |
}
|
| 47 |
end={item.path === '/'}
|
| 48 |
-
title={item.
|
| 49 |
>
|
| 50 |
<span className="nav-icon">{item.icon}</span>
|
| 51 |
-
<span className="nav-label">{item.
|
| 52 |
</NavLink>
|
| 53 |
))}
|
| 54 |
</nav>
|
| 55 |
|
| 56 |
<div className="nav-actions">
|
| 57 |
-
<button className="theme-toggle" onClick={
|
|
|
|
|
|
|
|
|
|
| 58 |
{theme === 'dark' ? '☀️' : '🌙'}
|
| 59 |
</button>
|
| 60 |
</div>
|
|
|
|
| 1 |
import { type ReactNode, useEffect } from 'react';
|
| 2 |
import { NavLink } from 'react-router-dom';
|
| 3 |
+
import { useTranslation } from 'react-i18next';
|
| 4 |
import { useUIStore } from '../../store/uiStore';
|
| 5 |
import './AppShell.css';
|
| 6 |
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
const NAV_ITEMS = [
|
| 12 |
+
{ path: '/', translationKey: 'appshell.nav.home', icon: '💡' },
|
| 13 |
+
{ path: '/data-collection', translationKey: 'appshell.nav.data_collection', icon: '📊' },
|
| 14 |
+
{ path: '/potential-analysis', translationKey: 'appshell.nav.potential_analysis', icon: '📈' },
|
| 15 |
] as const;
|
| 16 |
|
| 17 |
export default function AppShell({ children }: Props) {
|
| 18 |
+
const { t, i18n } = useTranslation();
|
| 19 |
const theme = useUIStore((s) => s.theme);
|
| 20 |
const toggleTheme = useUIStore((s) => s.toggleTheme);
|
| 21 |
|
| 22 |
+
const toggleLanguage = () => {
|
| 23 |
+
const nextLang = i18n.language === 'en' ? 'de' : 'en';
|
| 24 |
+
i18n.changeLanguage(nextLang);
|
| 25 |
+
localStorage.setItem('i18nextLng', nextLang);
|
| 26 |
+
|
| 27 |
+
// Trigger Google Translate widget
|
| 28 |
+
try {
|
| 29 |
+
const selectField = document.querySelector('.goog-te-combo') as HTMLSelectElement;
|
| 30 |
+
if (selectField) {
|
| 31 |
+
selectField.value = nextLang;
|
| 32 |
+
selectField.dispatchEvent(new Event('change'));
|
| 33 |
+
}
|
| 34 |
+
} catch (e) {
|
| 35 |
+
console.error('Google Translate trigger failed:', e);
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
// Sync dark mode class to HTML element
|
| 40 |
useEffect(() => {
|
| 41 |
console.log('[HeatTransPlan] Current Theme:', theme);
|
|
|
|
| 64 |
`nav-item ${isActive ? 'active' : ''}`
|
| 65 |
}
|
| 66 |
end={item.path === '/'}
|
| 67 |
+
title={t(item.translationKey)}
|
| 68 |
>
|
| 69 |
<span className="nav-icon">{item.icon}</span>
|
| 70 |
+
<span className="nav-label">{t(item.translationKey)}</span>
|
| 71 |
</NavLink>
|
| 72 |
))}
|
| 73 |
</nav>
|
| 74 |
|
| 75 |
<div className="nav-actions">
|
| 76 |
+
<button className="theme-toggle" onClick={toggleLanguage} title={i18n.language === 'en' ? 'Switch to German' : 'Switch to English'}>
|
| 77 |
+
{i18n.language === 'en' ? '🇩🇪' : '🇬🇧'}
|
| 78 |
+
</button>
|
| 79 |
+
<button className="theme-toggle" onClick={toggleTheme} title={theme === 'dark' ? t('appshell.theme.light') : t('appshell.theme.dark')}>
|
| 80 |
{theme === 'dark' ? '☀️' : '🌙'}
|
| 81 |
</button>
|
| 82 |
</div>
|
frontend/src/i18n.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import i18n from 'i18next';
|
| 2 |
+
import { initReactI18next } from 'react-i18next';
|
| 3 |
+
|
| 4 |
+
import en from './locales/en.json';
|
| 5 |
+
import de from './locales/de.json';
|
| 6 |
+
|
| 7 |
+
const resources = {
|
| 8 |
+
en: { translation: en },
|
| 9 |
+
de: { translation: de }
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
// Check if user previously saved a language choice
|
| 13 |
+
const savedLang = localStorage.getItem('i18nextLng') || 'en';
|
| 14 |
+
|
| 15 |
+
i18n
|
| 16 |
+
.use(initReactI18next)
|
| 17 |
+
.init({
|
| 18 |
+
resources,
|
| 19 |
+
lng: savedLang,
|
| 20 |
+
fallbackLng: 'en',
|
| 21 |
+
interpolation: {
|
| 22 |
+
escapeValue: false // react already safes from xss
|
| 23 |
+
}
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
export default i18n;
|
frontend/src/index.css
CHANGED
|
@@ -131,6 +131,25 @@ button {
|
|
| 131 |
}
|
| 132 |
|
| 133 |
/* ── Buttons ──────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
.btn {
|
| 135 |
display: inline-flex;
|
| 136 |
align-items: center;
|
|
|
|
| 131 |
}
|
| 132 |
|
| 133 |
/* ── Buttons ──────────────────────────────────────────── */
|
| 134 |
+
.body {
|
| 135 |
+
margin: 0;
|
| 136 |
+
display: flex;
|
| 137 |
+
place-items: center;
|
| 138 |
+
min-width: 320px;
|
| 139 |
+
min-height: 100vh;
|
| 140 |
+
top: 0 !important; /* Prevent Google Translate from pushing body down */
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/* Hide Google Translate UI elements */
|
| 144 |
+
.skiptranslate iframe {
|
| 145 |
+
display: none !important;
|
| 146 |
+
}
|
| 147 |
+
body {
|
| 148 |
+
top: 0 !important;
|
| 149 |
+
}
|
| 150 |
+
.goog-te-combo {
|
| 151 |
+
display: none !important;
|
| 152 |
+
}
|
| 153 |
.btn {
|
| 154 |
display: inline-flex;
|
| 155 |
align-items: center;
|
frontend/src/locales/de.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"appshell": {
|
| 3 |
+
"title": "HeatTransPlan",
|
| 4 |
+
"nav": {
|
| 5 |
+
"home": "Startseite",
|
| 6 |
+
"data_collection": "Datenerfassung",
|
| 7 |
+
"potential_analysis": "Potenzialanalyse"
|
| 8 |
+
},
|
| 9 |
+
"theme": {
|
| 10 |
+
"light": "Hellmodus",
|
| 11 |
+
"dark": "Dunkelmodus"
|
| 12 |
+
},
|
| 13 |
+
"lang": {
|
| 14 |
+
"en": "English",
|
| 15 |
+
"de": "Deutsch"
|
| 16 |
+
}
|
| 17 |
+
},
|
| 18 |
+
"home": {
|
| 19 |
+
"title": "Startseite",
|
| 20 |
+
"information": "Informationen",
|
| 21 |
+
"project_website": "Projekt-Website",
|
| 22 |
+
"app": "App",
|
| 23 |
+
"about_link": "Über HeatTransPlan",
|
| 24 |
+
"about_title": "Über diese App",
|
| 25 |
+
"about_text": "HeatTransPlan hilft bei der Erfassung von Energiedaten industrieller Prozesse und der Analyse des Wärmerückgewinnungspotenzials sowie der Integrationsmöglichkeiten von Wärmepumpen mittels Pinch-Analyse.",
|
| 26 |
+
"how_to_use": "So verwenden Sie die App",
|
| 27 |
+
"instruction_1": "Öffnen Sie <strong>Datenerfassung</strong>, um die Anlage zu lokalisieren, den Prozess zu beschreiben und Energiedaten hinzuzufügen.",
|
| 28 |
+
"instruction_2": "Fügen Sie Strominformationen hinzu und verfeinern Sie diese. Überprüfen Sie dann die Prozessdaten direkt in den Tabellen- und Kartenansichten.",
|
| 29 |
+
"instruction_3": "Exportieren Sie aufgezeichnete Daten und Analyseergebnisse.",
|
| 30 |
+
"examples": "Beispiele",
|
| 31 |
+
"load_example_1": "Beispiel 1 laden: Wärmeintegrationsbeispiel",
|
| 32 |
+
"loading": "Wird geladen...",
|
| 33 |
+
"example_success": "Beispiel geladen! Weiterleitung zur Datenerfassung...",
|
| 34 |
+
"example_error": "Fehler beim Laden des Beispiels"
|
| 35 |
+
},
|
| 36 |
+
"analysis": {
|
| 37 |
+
"title": "Potenzialanalyse",
|
| 38 |
+
"buttons": {
|
| 39 |
+
"save": "Speichern",
|
| 40 |
+
"load": "Laden",
|
| 41 |
+
"report": "Bericht",
|
| 42 |
+
"generating": "Wird erstellt..."
|
| 43 |
+
},
|
| 44 |
+
"messages": {
|
| 45 |
+
"no_processes": "Keine Prozesse gefunden. Bitte fügen Sie zuerst Prozesse auf der Seite Datenerfassung hinzu.",
|
| 46 |
+
"need_more_streams": "Wählen Sie mindestens 2 Ströme mit vollständigen Daten (Tin, Tout und entweder CP oder ṁ+cp) aus, um die Pinch-Analyse durchzuführen.",
|
| 47 |
+
"running": "Pinch-Analyse wird ausgeführt…",
|
| 48 |
+
"error": "Fehler:"
|
| 49 |
+
},
|
| 50 |
+
"controls": {
|
| 51 |
+
"show_shifted": "Verschobene Composite Curves anzeigen"
|
| 52 |
+
},
|
| 53 |
+
"sections": {
|
| 54 |
+
"notes": "Notizen"
|
| 55 |
+
},
|
| 56 |
+
"placeholders": {
|
| 57 |
+
"scenario_name": "Szenario",
|
| 58 |
+
"notes": "Hier tippen..."
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"stream_selector": {
|
| 62 |
+
"title": "Stromauswahl",
|
| 63 |
+
"help_desc": "Wählen Sie aus, welche Ströme (Wärmequellen oder -senken) in die Pinch-Analyse einbezogen werden sollen. Deaktivieren Sie Ströme, die derzeit inaktiv sind oder ausgeschlossen werden sollen.",
|
| 64 |
+
"subprocess": "Teilprozess",
|
| 65 |
+
"stream": "Strom",
|
| 66 |
+
"incomplete": "(unvollständige Daten)"
|
| 67 |
+
},
|
| 68 |
+
"energy_demands": {
|
| 69 |
+
"title": "Aktuelle Energieversorgung",
|
| 70 |
+
"help_desc": "Definieren Sie den Basisenergieverbrauch (Heizen und Kühlen) für die ausgewählten Ströme. Dies stellt den 'Status quo' vor der Wärmeintegration dar.",
|
| 71 |
+
"supply": "Versorgung",
|
| 72 |
+
"remove": "Versorgung entfernen",
|
| 73 |
+
"heat_kw": "Heizung (kW)",
|
| 74 |
+
"hot_selection": "Auswahl heißer Ströme",
|
| 75 |
+
"no_hot": "Keine heißen Ströme",
|
| 76 |
+
"cooling_kw": "Kühlung (kW)",
|
| 77 |
+
"cold_selection": "Auswahl kalter Ströme",
|
| 78 |
+
"no_cold": "Keine kalten Ströme",
|
| 79 |
+
"add_supply": "Versorgung hinzufügen"
|
| 80 |
+
}
|
| 81 |
+
}
|
frontend/src/locales/en.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"appshell": {
|
| 3 |
+
"title": "HeatTransPlan",
|
| 4 |
+
"nav": {
|
| 5 |
+
"home": "Home",
|
| 6 |
+
"data_collection": "Data Collection",
|
| 7 |
+
"potential_analysis": "Potential Analysis"
|
| 8 |
+
},
|
| 9 |
+
"theme": {
|
| 10 |
+
"light": "Light Mode",
|
| 11 |
+
"dark": "Dark Mode"
|
| 12 |
+
},
|
| 13 |
+
"lang": {
|
| 14 |
+
"en": "English",
|
| 15 |
+
"de": "Deutsch"
|
| 16 |
+
}
|
| 17 |
+
},
|
| 18 |
+
"home": {
|
| 19 |
+
"title": "Home Page",
|
| 20 |
+
"information": "Information",
|
| 21 |
+
"project_website": "Project website",
|
| 22 |
+
"app": "App",
|
| 23 |
+
"about_link": "About HeatTransPlan",
|
| 24 |
+
"about_title": "About this app",
|
| 25 |
+
"about_text": "HeatTransPlan helps collect industrial process energy data and analyze heat recovery potential and heat pump integration opportunities using pinch analysis.",
|
| 26 |
+
"how_to_use": "How to use",
|
| 27 |
+
"instruction_1": "Open <strong>Energy Data Collection</strong> to locate the facility, describe the process and add energy data.",
|
| 28 |
+
"instruction_2": "Add and refine stream information, then review process data directly in the table and map views.",
|
| 29 |
+
"instruction_3": "Export recorded data and analysis results.",
|
| 30 |
+
"examples": "Examples",
|
| 31 |
+
"load_example_1": "Load Example 1: Heat integration example",
|
| 32 |
+
"loading": "Loading...",
|
| 33 |
+
"example_success": "Example loaded! Redirecting to Data Collection...",
|
| 34 |
+
"example_error": "Failed to load example"
|
| 35 |
+
},
|
| 36 |
+
"analysis": {
|
| 37 |
+
"title": "Potential Analysis",
|
| 38 |
+
"buttons": {
|
| 39 |
+
"save": "Save",
|
| 40 |
+
"load": "Load",
|
| 41 |
+
"report": "Report",
|
| 42 |
+
"generating": "Generating..."
|
| 43 |
+
},
|
| 44 |
+
"messages": {
|
| 45 |
+
"no_processes": "No processes found. Please add processes in the Data Collection page first.",
|
| 46 |
+
"need_more_streams": "Select at least 2 streams with complete data (Tin, Tout, and either CP or ṁ+cp) to run pinch analysis.",
|
| 47 |
+
"running": "Running pinch analysis…",
|
| 48 |
+
"error": "Error:"
|
| 49 |
+
},
|
| 50 |
+
"controls": {
|
| 51 |
+
"show_shifted": "Show Shifted Composite Curves"
|
| 52 |
+
},
|
| 53 |
+
"sections": {
|
| 54 |
+
"notes": "Notes"
|
| 55 |
+
},
|
| 56 |
+
"placeholders": {
|
| 57 |
+
"scenario_name": "Scenario",
|
| 58 |
+
"notes": "Type here..."
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"stream_selector": {
|
| 62 |
+
"title": "Streams Selection",
|
| 63 |
+
"help_desc": "Select which streams (heat sources or sinks) to include in the pinch analysis. Deselect streams that are currently inactive or should be excluded.",
|
| 64 |
+
"subprocess": "Subprocess",
|
| 65 |
+
"stream": "Stream",
|
| 66 |
+
"incomplete": "(incomplete data)"
|
| 67 |
+
},
|
| 68 |
+
"energy_demands": {
|
| 69 |
+
"title": "Current energy supply",
|
| 70 |
+
"help_desc": "Define the baseline energy consumption (heating and cooling) for the selected streams. This represents the 'status quo' before heat integration.",
|
| 71 |
+
"supply": "Supply",
|
| 72 |
+
"remove": "Remove supply",
|
| 73 |
+
"heat_kw": "Heat (kW)",
|
| 74 |
+
"hot_selection": "Hot streams selection",
|
| 75 |
+
"no_hot": "No hot streams",
|
| 76 |
+
"cooling_kw": "Cooling (kW)",
|
| 77 |
+
"cold_selection": "Cold streams selection",
|
| 78 |
+
"no_cold": "No cold streams",
|
| 79 |
+
"add_supply": "Add Supply"
|
| 80 |
+
}
|
| 81 |
+
}
|
frontend/src/main.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { StrictMode } from 'react'
|
| 2 |
import { createRoot } from 'react-dom/client'
|
| 3 |
import { BrowserRouter } from 'react-router-dom'
|
|
|
|
| 4 |
import './index.css'
|
| 5 |
import App from './App'
|
| 6 |
|
|
|
|
| 1 |
import { StrictMode } from 'react'
|
| 2 |
import { createRoot } from 'react-dom/client'
|
| 3 |
import { BrowserRouter } from 'react-router-dom'
|
| 4 |
+
import './i18n'
|
| 5 |
import './index.css'
|
| 6 |
import App from './App'
|
| 7 |
|
frontend/src/pages/HomePage.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
|
| 2 |
import { useNavigate } from 'react-router-dom';
|
| 3 |
import { useProjectStore } from '../store/projectStore';
|
| 4 |
import { getExample } from '../api/io';
|
|
|
|
| 5 |
import styles from './HomePage.module.css';
|
| 6 |
|
| 7 |
const PROJECT_IMG_URL =
|
|
@@ -13,6 +14,7 @@ function qrImgUrl(url: string, size = 110) {
|
|
| 13 |
}
|
| 14 |
|
| 15 |
export default function HomePage() {
|
|
|
|
| 16 |
const navigate = useNavigate();
|
| 17 |
const setState = useProjectStore((s) => s.setState);
|
| 18 |
const [loadingExample, setLoadingExample] = useState(false);
|
|
@@ -26,10 +28,10 @@ export default function HomePage() {
|
|
| 26 |
const data = await getExample(filename);
|
| 27 |
// data is the full project state JSON
|
| 28 |
setState(data);
|
| 29 |
-
setSuccessMsg('
|
| 30 |
setTimeout(() => navigate('/data-collection'), 800);
|
| 31 |
} catch (e: any) {
|
| 32 |
-
setError(e.message || '
|
| 33 |
} finally {
|
| 34 |
setLoadingExample(false);
|
| 35 |
}
|
|
@@ -40,9 +42,9 @@ export default function HomePage() {
|
|
| 40 |
<div className={styles['home-grid']}>
|
| 41 |
{/* Left column */}
|
| 42 |
<div className={styles['home-left']}>
|
| 43 |
-
<h1>
|
| 44 |
|
| 45 |
-
<h2>
|
| 46 |
|
| 47 |
<div className={styles['home-info-row']}>
|
| 48 |
<a
|
|
@@ -71,7 +73,7 @@ export default function HomePage() {
|
|
| 71 |
width={110}
|
| 72 |
/>
|
| 73 |
</a>
|
| 74 |
-
<span className={styles['qr-label']}>
|
| 75 |
</div>
|
| 76 |
<div className={styles['qr-item']}>
|
| 77 |
<a
|
|
@@ -87,34 +89,28 @@ export default function HomePage() {
|
|
| 87 |
width={110}
|
| 88 |
/>
|
| 89 |
</a>
|
| 90 |
-
<span className={styles['qr-label']}>
|
| 91 |
</div>
|
| 92 |
</div>
|
| 93 |
</div>
|
| 94 |
|
| 95 |
-
<p className={styles['home-about-link']}>
|
| 96 |
|
| 97 |
-
<h2>
|
| 98 |
-
<p>
|
| 99 |
-
HeatTransPlan helps collect industrial process energy data and
|
| 100 |
-
analyze heat recovery potential and heat pump integration
|
| 101 |
-
opportunities using pinch analysis.
|
| 102 |
-
</p>
|
| 103 |
|
| 104 |
-
<h3>
|
| 105 |
<ul className={styles['home-instructions']}>
|
| 106 |
<li>
|
| 107 |
-
|
| 108 |
-
|
|
|
|
| 109 |
</li>
|
| 110 |
-
<li>
|
| 111 |
-
|
| 112 |
-
directly in the table and map views.
|
| 113 |
-
</li>
|
| 114 |
-
<li>Export recorded data and analysis results.</li>
|
| 115 |
</ul>
|
| 116 |
|
| 117 |
-
<h3>
|
| 118 |
{error && <div className={styles['home-error']}>{error}</div>}
|
| 119 |
{successMsg && <div className={styles['home-success']}>{successMsg}</div>}
|
| 120 |
|
|
@@ -126,7 +122,7 @@ export default function HomePage() {
|
|
| 126 |
{loadingExample ? (
|
| 127 |
<span className="spinner" />
|
| 128 |
) : (
|
| 129 |
-
'
|
| 130 |
)}
|
| 131 |
</button>
|
| 132 |
</div>
|
|
|
|
| 2 |
import { useNavigate } from 'react-router-dom';
|
| 3 |
import { useProjectStore } from '../store/projectStore';
|
| 4 |
import { getExample } from '../api/io';
|
| 5 |
+
import { useTranslation, Trans } from 'react-i18next';
|
| 6 |
import styles from './HomePage.module.css';
|
| 7 |
|
| 8 |
const PROJECT_IMG_URL =
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
export default function HomePage() {
|
| 17 |
+
const { t } = useTranslation();
|
| 18 |
const navigate = useNavigate();
|
| 19 |
const setState = useProjectStore((s) => s.setState);
|
| 20 |
const [loadingExample, setLoadingExample] = useState(false);
|
|
|
|
| 28 |
const data = await getExample(filename);
|
| 29 |
// data is the full project state JSON
|
| 30 |
setState(data);
|
| 31 |
+
setSuccessMsg(t('home.example_success'));
|
| 32 |
setTimeout(() => navigate('/data-collection'), 800);
|
| 33 |
} catch (e: any) {
|
| 34 |
+
setError(e.message || t('home.example_error'));
|
| 35 |
} finally {
|
| 36 |
setLoadingExample(false);
|
| 37 |
}
|
|
|
|
| 42 |
<div className={styles['home-grid']}>
|
| 43 |
{/* Left column */}
|
| 44 |
<div className={styles['home-left']}>
|
| 45 |
+
<h1>{t('home.title')}</h1>
|
| 46 |
|
| 47 |
+
<h2>{t('home.information')}</h2>
|
| 48 |
|
| 49 |
<div className={styles['home-info-row']}>
|
| 50 |
<a
|
|
|
|
| 73 |
width={110}
|
| 74 |
/>
|
| 75 |
</a>
|
| 76 |
+
<span className={styles['qr-label']}>{t('home.project_website')}</span>
|
| 77 |
</div>
|
| 78 |
<div className={styles['qr-item']}>
|
| 79 |
<a
|
|
|
|
| 89 |
width={110}
|
| 90 |
/>
|
| 91 |
</a>
|
| 92 |
+
<span className={styles['qr-label']}>{t('home.app')}</span>
|
| 93 |
</div>
|
| 94 |
</div>
|
| 95 |
</div>
|
| 96 |
|
| 97 |
+
<p className={styles['home-about-link']}>{t('home.about_link')}</p>
|
| 98 |
|
| 99 |
+
<h2>{t('home.about_title')}</h2>
|
| 100 |
+
<p>{t('home.about_text')}</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
<h3>{t('home.how_to_use')}</h3>
|
| 103 |
<ul className={styles['home-instructions']}>
|
| 104 |
<li>
|
| 105 |
+
<Trans i18nKey="home.instruction_1">
|
| 106 |
+
Open <strong>Energy Data Collection</strong> to locate the facility, describe the process and add energy data.
|
| 107 |
+
</Trans>
|
| 108 |
</li>
|
| 109 |
+
<li>{t('home.instruction_2')}</li>
|
| 110 |
+
<li>{t('home.instruction_3')}</li>
|
|
|
|
|
|
|
|
|
|
| 111 |
</ul>
|
| 112 |
|
| 113 |
+
<h3>{t('home.examples')}</h3>
|
| 114 |
{error && <div className={styles['home-error']}>{error}</div>}
|
| 115 |
{successMsg && <div className={styles['home-success']}>{successMsg}</div>}
|
| 116 |
|
|
|
|
| 122 |
{loadingExample ? (
|
| 123 |
<span className="spinner" />
|
| 124 |
) : (
|
| 125 |
+
t('home.load_example_1')
|
| 126 |
)}
|
| 127 |
</button>
|
| 128 |
</div>
|
frontend/src/pages/PotentialAnalysisPage.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import HPIChart from '../components/analysis/HPIChart';
|
|
| 20 |
import StatusQuoComparison from '../components/analysis/StatusQuoComparison';
|
| 21 |
import TemperatureIntervalDiagram from '../components/analysis/TemperatureIntervalDiagram';
|
| 22 |
import ScenarioComparison from '../components/analysis/ScenarioComparison';
|
|
|
|
| 23 |
|
| 24 |
import './PotentialAnalysisPage.css';
|
| 25 |
|
|
@@ -33,6 +34,7 @@ import AnalysisHelp from '../components/ui/AnalysisHelp';
|
|
| 33 |
import ChartHelpButton from '../components/ui/ChartHelpButton';
|
| 34 |
|
| 35 |
export default function PotentialAnalysisPage() {
|
|
|
|
| 36 |
const processes = useProjectStore((s) => s.state.processes);
|
| 37 |
const pinchNotes = useProjectStore((s) => s.state.pinch_notes);
|
| 38 |
const setPinchNotes = useProjectStore((s) => s.setPinchNotes);
|
|
@@ -413,7 +415,7 @@ export default function PotentialAnalysisPage() {
|
|
| 413 |
border: '1px solid var(--border)',
|
| 414 |
boxShadow: 'var(--shadow-sm)'
|
| 415 |
}}>
|
| 416 |
-
<h1 className="pa-title" style={{ fontSize: '1.25rem', whiteSpace: 'nowrap' }}>
|
| 417 |
|
| 418 |
{/* Scenarios - Inline */}
|
| 419 |
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1, overflow: 'hidden' }}>
|
|
@@ -474,7 +476,7 @@ export default function PotentialAnalysisPage() {
|
|
| 474 |
<div style={{ display: 'flex', gap: 4, alignItems: 'center', background: 'var(--bg)', padding: '4px', borderRadius: '8px', marginLeft: 8 }}>
|
| 475 |
<input
|
| 476 |
type="text"
|
| 477 |
-
placeholder={
|
| 478 |
value={scenarioName}
|
| 479 |
onChange={(e) => setScenarioName(e.target.value)}
|
| 480 |
onKeyDown={(e) => e.key === 'Enter' && handleSaveScenario()}
|
|
@@ -484,17 +486,17 @@ export default function PotentialAnalysisPage() {
|
|
| 484 |
onClick={handleSaveScenario}
|
| 485 |
style={{ background: 'var(--success)', color: 'var(--text-on-primary)', padding: '4px 10px', borderRadius: '6px', fontSize: '0.75rem', fontWeight: 600, border: 'none', cursor: 'pointer' }}
|
| 486 |
>
|
| 487 |
-
|
| 488 |
</button>
|
| 489 |
</div>
|
| 490 |
</div>
|
| 491 |
|
| 492 |
<div className="pa-header-actions" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
| 493 |
<button className="btn btn-sm" onClick={handleSaveProject} title="Save project to JSON">
|
| 494 |
-
💾
|
| 495 |
</button>
|
| 496 |
<label className="btn btn-sm" style={{ cursor: 'pointer' }} title="Load project from JSON">
|
| 497 |
-
📂
|
| 498 |
<input
|
| 499 |
ref={fileRef}
|
| 500 |
type="file"
|
|
@@ -510,7 +512,7 @@ export default function PotentialAnalysisPage() {
|
|
| 510 |
onClick={handleGenerateReport}
|
| 511 |
style={{ padding: '6px 14px', fontSize: '0.8rem' }}
|
| 512 |
>
|
| 513 |
-
{reportLoading ?
|
| 514 |
</button>
|
| 515 |
<div style={{ width: 1, height: 20, background: 'var(--border)', margin: '0 4px' }} />
|
| 516 |
<button
|
|
@@ -567,11 +569,11 @@ export default function PotentialAnalysisPage() {
|
|
| 567 |
|
| 568 |
{processes.length === 0 ? (
|
| 569 |
<div className="pa-info-box">
|
| 570 |
-
|
| 571 |
</div>
|
| 572 |
) : streamsData.length < 2 ? (
|
| 573 |
<div className="pa-info-box">
|
| 574 |
-
|
| 575 |
{selectedCount > 0 && (
|
| 576 |
<div className="pa-data-status">
|
| 577 |
<strong>Data status for selected items:</strong>
|
|
@@ -611,8 +613,8 @@ export default function PotentialAnalysisPage() {
|
|
| 611 |
</div>
|
| 612 |
) : (
|
| 613 |
<>
|
| 614 |
-
{pinchLoading && <div className="pa-loading">
|
| 615 |
-
{pinchError && <div className="pa-error">
|
| 616 |
|
| 617 |
{pinchResult && (
|
| 618 |
<>
|
|
@@ -629,7 +631,7 @@ export default function PotentialAnalysisPage() {
|
|
| 629 |
checked={showShifted}
|
| 630 |
onChange={(e) => setShowShifted(e.target.checked)}
|
| 631 |
/>
|
| 632 |
-
|
| 633 |
</label>
|
| 634 |
|
| 635 |
<label className="pa-input-label pa-tmin-label">
|
|
|
|
| 20 |
import StatusQuoComparison from '../components/analysis/StatusQuoComparison';
|
| 21 |
import TemperatureIntervalDiagram from '../components/analysis/TemperatureIntervalDiagram';
|
| 22 |
import ScenarioComparison from '../components/analysis/ScenarioComparison';
|
| 23 |
+
import { useTranslation } from 'react-i18next';
|
| 24 |
|
| 25 |
import './PotentialAnalysisPage.css';
|
| 26 |
|
|
|
|
| 34 |
import ChartHelpButton from '../components/ui/ChartHelpButton';
|
| 35 |
|
| 36 |
export default function PotentialAnalysisPage() {
|
| 37 |
+
const { t } = useTranslation();
|
| 38 |
const processes = useProjectStore((s) => s.state.processes);
|
| 39 |
const pinchNotes = useProjectStore((s) => s.state.pinch_notes);
|
| 40 |
const setPinchNotes = useProjectStore((s) => s.setPinchNotes);
|
|
|
|
| 415 |
border: '1px solid var(--border)',
|
| 416 |
boxShadow: 'var(--shadow-sm)'
|
| 417 |
}}>
|
| 418 |
+
<h1 className="pa-title" style={{ fontSize: '1.25rem', whiteSpace: 'nowrap' }}>{t('analysis.title')}</h1>
|
| 419 |
|
| 420 |
{/* Scenarios - Inline */}
|
| 421 |
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1, overflow: 'hidden' }}>
|
|
|
|
| 476 |
<div style={{ display: 'flex', gap: 4, alignItems: 'center', background: 'var(--bg)', padding: '4px', borderRadius: '8px', marginLeft: 8 }}>
|
| 477 |
<input
|
| 478 |
type="text"
|
| 479 |
+
placeholder={t('analysis.placeholders.scenario_name')}
|
| 480 |
value={scenarioName}
|
| 481 |
onChange={(e) => setScenarioName(e.target.value)}
|
| 482 |
onKeyDown={(e) => e.key === 'Enter' && handleSaveScenario()}
|
|
|
|
| 486 |
onClick={handleSaveScenario}
|
| 487 |
style={{ background: 'var(--success)', color: 'var(--text-on-primary)', padding: '4px 10px', borderRadius: '6px', fontSize: '0.75rem', fontWeight: 600, border: 'none', cursor: 'pointer' }}
|
| 488 |
>
|
| 489 |
+
{t('analysis.buttons.save')}
|
| 490 |
</button>
|
| 491 |
</div>
|
| 492 |
</div>
|
| 493 |
|
| 494 |
<div className="pa-header-actions" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
| 495 |
<button className="btn btn-sm" onClick={handleSaveProject} title="Save project to JSON">
|
| 496 |
+
💾 {t('analysis.buttons.save')}
|
| 497 |
</button>
|
| 498 |
<label className="btn btn-sm" style={{ cursor: 'pointer' }} title="Load project from JSON">
|
| 499 |
+
📂 {t('analysis.buttons.load')}
|
| 500 |
<input
|
| 501 |
ref={fileRef}
|
| 502 |
type="file"
|
|
|
|
| 512 |
onClick={handleGenerateReport}
|
| 513 |
style={{ padding: '6px 14px', fontSize: '0.8rem' }}
|
| 514 |
>
|
| 515 |
+
{reportLoading ? `⏳ ${t('analysis.buttons.generating')}` : `📄 ${t('analysis.buttons.report')}`}
|
| 516 |
</button>
|
| 517 |
<div style={{ width: 1, height: 20, background: 'var(--border)', margin: '0 4px' }} />
|
| 518 |
<button
|
|
|
|
| 569 |
|
| 570 |
{processes.length === 0 ? (
|
| 571 |
<div className="pa-info-box">
|
| 572 |
+
{t('analysis.messages.no_processes')}
|
| 573 |
</div>
|
| 574 |
) : streamsData.length < 2 ? (
|
| 575 |
<div className="pa-info-box">
|
| 576 |
+
{t('analysis.messages.need_more_streams')}
|
| 577 |
{selectedCount > 0 && (
|
| 578 |
<div className="pa-data-status">
|
| 579 |
<strong>Data status for selected items:</strong>
|
|
|
|
| 613 |
</div>
|
| 614 |
) : (
|
| 615 |
<>
|
| 616 |
+
{pinchLoading && <div className="pa-loading">{t('analysis.messages.running')}</div>}
|
| 617 |
+
{pinchError && <div className="pa-error">{t('analysis.messages.error')} {pinchError}</div>}
|
| 618 |
|
| 619 |
{pinchResult && (
|
| 620 |
<>
|
|
|
|
| 631 |
checked={showShifted}
|
| 632 |
onChange={(e) => setShowShifted(e.target.checked)}
|
| 633 |
/>
|
| 634 |
+
{t('analysis.controls.show_shifted')}
|
| 635 |
</label>
|
| 636 |
|
| 637 |
<label className="pa-input-label pa-tmin-label">
|