create-forgeon 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/modules/executor.test.mjs +8 -1
- package/templates/base/apps/api/src/health/health.controller.ts +55 -29
- package/templates/base/resources/i18n/en/common.json +9 -5
- package/templates/base/resources/i18n/uk/common.json +9 -5
- package/templates/module-presets/i18n/apps/web/src/App.tsx +34 -3
- package/templates/module-presets/i18n/packages/i18n-web/src/index.ts +1 -1
package/package.json
CHANGED
|
@@ -104,7 +104,8 @@ describe('addModule', () => {
|
|
|
104
104
|
|
|
105
105
|
const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
|
|
106
106
|
assert.match(appTsx, /@forgeon\/i18n-web/);
|
|
107
|
-
assert.match(appTsx, /
|
|
107
|
+
assert.match(appTsx, /\/api\/health\/meta/);
|
|
108
|
+
assert.match(appTsx, /checkApiHealth/);
|
|
108
109
|
|
|
109
110
|
const i18nWebPackage = fs.readFileSync(
|
|
110
111
|
path.join(projectRoot, 'packages', 'i18n-web', 'package.json'),
|
|
@@ -118,6 +119,12 @@ describe('addModule', () => {
|
|
|
118
119
|
);
|
|
119
120
|
assert.match(i18nWebTsconfig, /"module": "ESNext"/);
|
|
120
121
|
|
|
122
|
+
const i18nWebSource = fs.readFileSync(
|
|
123
|
+
path.join(projectRoot, 'packages', 'i18n-web', 'src', 'index.ts'),
|
|
124
|
+
'utf8',
|
|
125
|
+
);
|
|
126
|
+
assert.match(i18nWebSource, /@forgeon\/i18n-contracts\/src\/index/);
|
|
127
|
+
|
|
121
128
|
const caddyDockerfile = fs.readFileSync(
|
|
122
129
|
path.join(projectRoot, 'infra', 'docker', 'caddy.Dockerfile'),
|
|
123
130
|
'utf8',
|
|
@@ -1,31 +1,57 @@
|
|
|
1
|
-
import { Controller, Get, Optional, Query } from '@nestjs/common';
|
|
2
|
-
import { I18nService } from 'nestjs-i18n';
|
|
3
|
-
import { EchoQueryDto } from '../common/dto/echo-query.dto';
|
|
4
|
-
|
|
5
|
-
@Controller('health')
|
|
6
|
-
export class HealthController {
|
|
7
|
-
constructor(@Optional() private readonly i18n?: I18nService) {}
|
|
8
|
-
|
|
9
|
-
@Get()
|
|
10
|
-
getHealth(@Query('lang') lang?: string) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
import { Controller, Get, Optional, Query } from '@nestjs/common';
|
|
2
|
+
import { I18nService } from 'nestjs-i18n';
|
|
3
|
+
import { EchoQueryDto } from '../common/dto/echo-query.dto';
|
|
4
|
+
|
|
5
|
+
@Controller('health')
|
|
6
|
+
export class HealthController {
|
|
7
|
+
constructor(@Optional() private readonly i18n?: I18nService) {}
|
|
8
|
+
|
|
9
|
+
@Get()
|
|
10
|
+
getHealth(@Query('lang') lang?: string) {
|
|
11
|
+
const locale = this.resolveLocale(lang);
|
|
12
|
+
return {
|
|
13
|
+
status: 'ok',
|
|
14
|
+
message: this.translate('common.ok', lang),
|
|
15
|
+
i18n: this.translate(this.localeNameKey(locale), lang),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Get('meta')
|
|
20
|
+
getMeta(@Query('lang') lang?: string) {
|
|
21
|
+
return {
|
|
22
|
+
checkApiHealth: this.translate('common.checkApiHealth', lang),
|
|
23
|
+
languageLabel: this.translate('common.language', lang),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Get('echo')
|
|
28
|
+
getEcho(@Query() query: EchoQueryDto) {
|
|
29
|
+
return { value: query.value };
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
private translate(key: string, lang?: string): string {
|
|
23
33
|
if (!this.i18n) {
|
|
24
|
-
if (key === 'common.ok') return 'OK';
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
if (key === 'common.ok') return 'OK';
|
|
35
|
+
if (key === 'common.checkApiHealth') return 'Check API health';
|
|
36
|
+
if (key === 'common.language') return 'Language';
|
|
37
|
+
if (key === 'languages.english') return 'English';
|
|
38
|
+
if (key === 'languages.ukrainian') return 'Ukrainian';
|
|
39
|
+
return key;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const value = this.i18n.t(key, { lang, defaultValue: key });
|
|
43
|
+
return typeof value === 'string' ? value : key;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private resolveLocale(lang?: string): 'en' | 'uk' {
|
|
47
|
+
const normalized = (lang ?? '').toLowerCase();
|
|
48
|
+
if (normalized.startsWith('uk')) {
|
|
49
|
+
return 'uk';
|
|
50
|
+
}
|
|
51
|
+
return 'en';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private localeNameKey(locale: 'en' | 'uk'): string {
|
|
55
|
+
return locale === 'uk' ? 'languages.ukrainian' : 'languages.english';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
{
|
|
2
|
-
"errors.accessDenied": "Access denied",
|
|
3
|
-
"errors.notFound": "Resource not found",
|
|
4
|
-
"common.ok": "OK"
|
|
5
|
-
|
|
1
|
+
{
|
|
2
|
+
"errors.accessDenied": "Access denied",
|
|
3
|
+
"errors.notFound": "Resource not found",
|
|
4
|
+
"common.ok": "OK",
|
|
5
|
+
"common.checkApiHealth": "Check API health",
|
|
6
|
+
"common.language": "Language",
|
|
7
|
+
"languages.english": "English",
|
|
8
|
+
"languages.ukrainian": "Ukrainian"
|
|
9
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
{
|
|
2
|
-
"errors.accessDenied": "Доступ заборонено",
|
|
3
|
-
"errors.notFound": "Ресурс не знайдено",
|
|
4
|
-
"common.ok": "OK"
|
|
5
|
-
|
|
1
|
+
{
|
|
2
|
+
"errors.accessDenied": "Доступ заборонено",
|
|
3
|
+
"errors.notFound": "Ресурс не знайдено",
|
|
4
|
+
"common.ok": "OK",
|
|
5
|
+
"common.checkApiHealth": "Перевірити API health",
|
|
6
|
+
"common.language": "Мова",
|
|
7
|
+
"languages.english": "Англійська",
|
|
8
|
+
"languages.ukrainian": "Українська"
|
|
9
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
2
|
import * as i18nWeb from '@forgeon/i18n-web';
|
|
3
3
|
import type { I18nLocale } from '@forgeon/i18n-web';
|
|
4
4
|
import './styles.css';
|
|
@@ -6,6 +6,12 @@ import './styles.css';
|
|
|
6
6
|
type HealthResponse = {
|
|
7
7
|
status: string;
|
|
8
8
|
message: string;
|
|
9
|
+
i18n: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type HealthMetaResponse = {
|
|
13
|
+
checkApiHealth: string;
|
|
14
|
+
languageLabel: string;
|
|
9
15
|
};
|
|
10
16
|
|
|
11
17
|
export default function App() {
|
|
@@ -13,12 +19,37 @@ export default function App() {
|
|
|
13
19
|
const [locale, setLocale] = useState<I18nLocale>(getInitialLocale);
|
|
14
20
|
const [data, setData] = useState<HealthResponse | null>(null);
|
|
15
21
|
const [error, setError] = useState<string | null>(null);
|
|
22
|
+
const [labels, setLabels] = useState<HealthMetaResponse>({
|
|
23
|
+
checkApiHealth: 'Check API health',
|
|
24
|
+
languageLabel: 'Language',
|
|
25
|
+
});
|
|
16
26
|
|
|
17
27
|
const changeLocale = (nextLocale: I18nLocale) => {
|
|
18
28
|
setLocale(nextLocale);
|
|
19
29
|
persistLocale(nextLocale);
|
|
20
30
|
};
|
|
21
31
|
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const loadLabels = async () => {
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(`/api/health/meta${toLangQuery(locale)}`, {
|
|
36
|
+
headers: {
|
|
37
|
+
'Accept-Language': locale,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const payload = (await response.json()) as HealthMetaResponse;
|
|
44
|
+
setLabels(payload);
|
|
45
|
+
} catch {
|
|
46
|
+
// Keep fallback labels if meta request fails.
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
void loadLabels();
|
|
51
|
+
}, [locale, toLangQuery]);
|
|
52
|
+
|
|
22
53
|
const checkApi = async () => {
|
|
23
54
|
setError(null);
|
|
24
55
|
try {
|
|
@@ -41,7 +72,7 @@ export default function App() {
|
|
|
41
72
|
<main className="page">
|
|
42
73
|
<h1>Forgeon Fullstack Scaffold</h1>
|
|
43
74
|
<p>Default frontend preset: React + Vite + TypeScript.</p>
|
|
44
|
-
<label htmlFor="language">
|
|
75
|
+
<label htmlFor="language">{labels.languageLabel}:</label>
|
|
45
76
|
<select
|
|
46
77
|
id="language"
|
|
47
78
|
value={locale}
|
|
@@ -53,7 +84,7 @@ export default function App() {
|
|
|
53
84
|
</option>
|
|
54
85
|
))}
|
|
55
86
|
</select>
|
|
56
|
-
<button onClick={checkApi}>
|
|
87
|
+
<button onClick={checkApi}>{labels.checkApiHealth}</button>
|
|
57
88
|
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : null}
|
|
58
89
|
{error ? <p className="error">{error}</p> : null}
|
|
59
90
|
</main>
|