drzg15 commited on
Commit
7c893d7
·
1 Parent(s): b187967

with google translator

Browse files
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": "frontend",
3
  "version": "0.0.0",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
- "name": "frontend",
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
- "dev": true,
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 || `Stream ${si + 1} in ${proc.name || `Subprocess ${pi + 1}`}`;
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
- Current energy supply
87
- <ChartHelpButton inline title="Current Energy Supply" description="Define the baseline energy consumption (heating and cooling) for the selected streams. This represents the 'status quo' before heat integration." />
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>Supply {i + 1}</strong>
96
  {i > 0 && (
97
- <button className="pa-btn-icon" onClick={() => removeEnergyDemand(i)} title="Remove supply">
98
  🗑️
99
  </button>
100
  )}
@@ -102,7 +104,7 @@ export default function EnergyDemands() {
102
 
103
  {/* Heat supply */}
104
  <label className="pa-input-label">
105
- Heat (kW)
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>Hot streams selection</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">No hot streams</span>}
131
  </div>
132
  </details>
133
 
134
  {/* Cooling supply */}
135
  <label className="pa-input-label">
136
- Cooling (kW)
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>Cold streams selection</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">No cold streams</span>}
162
  </div>
163
  </details>
164
 
@@ -167,7 +169,7 @@ export default function EnergyDemands() {
167
  ))}
168
 
169
  <button className="pa-btn" onClick={handleAddSupply}>
170
- Add Supply
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">No processes found. Please add processes in the Data Collection page first.</div>;
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
- Streams Selection
55
- <ChartHelpButton inline title="Stream Selection" description="Select which streams (heat sources or sinks) to include in the pinch analysis. Deselect streams that are currently inactive or should be excluded." />
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 || `Subprocess ${pi + 1}`;
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 || `Stream ${si + 1}`;
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(' | ') : '(incomplete data)'}
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: '/', label: 'Home', icon: '💡' },
12
- { path: '/data-collection', label: 'Energy Data Collection', icon: '📊' },
13
- { path: '/potential-analysis', label: 'Potential Analysis', icon: '📈' },
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.label}
49
  >
50
  <span className="nav-icon">{item.icon}</span>
51
- <span className="nav-label">{item.label}</span>
52
  </NavLink>
53
  ))}
54
  </nav>
55
 
56
  <div className="nav-actions">
57
- <button className="theme-toggle" onClick={toggleTheme} title="Toggle dark mode">
 
 
 
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('Example loaded! Redirecting to Data Collection...');
30
  setTimeout(() => navigate('/data-collection'), 800);
31
  } catch (e: any) {
32
- setError(e.message || 'Failed to load example');
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>Home Page</h1>
44
 
45
- <h2>Information</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']}>Project website</span>
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']}>App</span>
91
  </div>
92
  </div>
93
  </div>
94
 
95
- <p className={styles['home-about-link']}>About HeatTransPlan</p>
96
 
97
- <h2>About this app</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>How to use</h3>
105
  <ul className={styles['home-instructions']}>
106
  <li>
107
- Open <strong>Energy Data Collection</strong> to locate the
108
- facility, describe the process and add energy data.
 
109
  </li>
110
- <li>
111
- Add and refine stream information, then review process data
112
- directly in the table and map views.
113
- </li>
114
- <li>Export recorded data and analysis results.</li>
115
  </ul>
116
 
117
- <h3>Examples</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
- 'Load Example 1: Heat integration example'
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' }}>Potential Analysis</h1>
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={defaultScenarioName}
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
- Save
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
- 💾 Save
495
  </button>
496
  <label className="btn btn-sm" style={{ cursor: 'pointer' }} title="Load project from JSON">
497
- 📂 Load
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 ? 'Generating…' : '📄 Report'}
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
- No processes found. Please add processes in the Data Collection page first.
571
  </div>
572
  ) : streamsData.length < 2 ? (
573
  <div className="pa-info-box">
574
- Select at least 2 streams with complete data (Tin, Tout, and either CP or ṁ+cp) to run pinch analysis.
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">Running pinch analysis</div>}
615
- {pinchError && <div className="pa-error">Error: {pinchError}</div>}
616
 
617
  {pinchResult && (
618
  <>
@@ -629,7 +631,7 @@ export default function PotentialAnalysisPage() {
629
  checked={showShifted}
630
  onChange={(e) => setShowShifted(e.target.checked)}
631
  />
632
- Show Shifted Composite Curves
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">