@4399ywkf/cli 1.0.8 โ 1.0.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/dist/templates/AntdStaticMethods/index.tsx +20 -0
- package/dist/templates/AppTheme.tsx +136 -0
- package/dist/templates/DIRECTORY_STRUCTURE.md +141 -0
- package/dist/templates/GlobalProvider/AppTheme.tsx +136 -0
- package/dist/templates/GlobalProvider/Locale.tsx +84 -0
- package/dist/templates/GlobalProvider/Query.tsx +12 -0
- package/dist/templates/GlobalProvider/StyleRegistry.tsx +9 -0
- package/dist/templates/GlobalProvider/index.tsx +23 -0
- package/dist/templates/Locale.tsx +55 -56
- package/dist/templates/Query.tsx +12 -0
- package/dist/templates/StyleRegistry.tsx +9 -0
- package/dist/templates/analyzeUnusedKeys.ts +506 -0
- package/dist/templates/app/.i18nrc.js +57 -0
- package/dist/templates/app/config/jwt/index.ts +2 -1
- package/dist/templates/app/docs/DIRECTORY_STRUCTURE.md +141 -0
- package/dist/templates/app/docs/glossary.md +11 -0
- package/dist/templates/app/package.json.tpl +7 -15
- package/dist/templates/app/scripts/i18nWorkflow/analyzeUnusedKeys.ts +506 -0
- package/dist/templates/app/scripts/i18nWorkflow/cleanUnusedKeys.ts +344 -0
- package/dist/templates/app/scripts/i18nWorkflow/const.ts +18 -0
- package/dist/templates/app/scripts/i18nWorkflow/flattenLocaleKeys.ts +139 -0
- package/dist/templates/app/scripts/i18nWorkflow/genDefaultLocale.ts +19 -0
- package/dist/templates/app/scripts/i18nWorkflow/genDiff.ts +49 -0
- package/dist/templates/app/scripts/i18nWorkflow/i18nConfig.ts +7 -0
- package/dist/templates/app/scripts/i18nWorkflow/index.ts +11 -0
- package/dist/templates/app/scripts/i18nWorkflow/protectedPatterns.ts +91 -0
- package/dist/templates/app/scripts/i18nWorkflow/utils.ts +76 -0
- package/dist/templates/app/src/components/AntdStaticMethods/index.tsx +20 -0
- package/dist/templates/app/src/index.tsx +0 -4
- package/dist/templates/app/src/layout/GlobalProvider/AppTheme.tsx +136 -0
- package/dist/templates/app/src/layout/GlobalProvider/Locale.tsx +84 -0
- package/dist/templates/app/src/layout/GlobalProvider/Query.tsx +12 -0
- package/dist/templates/app/src/layout/GlobalProvider/StyleRegistry.tsx +9 -0
- package/dist/templates/app/src/layout/GlobalProvider/index.tsx +23 -0
- package/dist/templates/app/src/locales/utils.ts +23 -0
- package/dist/templates/app/src/pages/base/index.tsx +170 -79
- package/dist/templates/app/src/routes.tsx +2 -2
- package/dist/templates/app/tsconfig.json +19 -3
- package/dist/templates/base/index.tsx +170 -79
- package/dist/templates/cleanUnusedKeys.ts +344 -0
- package/dist/templates/components/AntdStaticMethods/index.tsx +20 -0
- package/dist/templates/config/jwt/index.ts +2 -1
- package/dist/templates/const.ts +18 -0
- package/dist/templates/docs/DIRECTORY_STRUCTURE.md +141 -0
- package/dist/templates/docs/glossary.md +11 -0
- package/dist/templates/flattenLocaleKeys.ts +139 -0
- package/dist/templates/genDefaultLocale.ts +19 -0
- package/dist/templates/genDiff.ts +49 -0
- package/dist/templates/glossary.md +11 -0
- package/dist/templates/i18nConfig.ts +7 -0
- package/dist/templates/i18nWorkflow/analyzeUnusedKeys.ts +506 -0
- package/dist/templates/i18nWorkflow/cleanUnusedKeys.ts +344 -0
- package/dist/templates/i18nWorkflow/const.ts +18 -0
- package/dist/templates/i18nWorkflow/flattenLocaleKeys.ts +139 -0
- package/dist/templates/i18nWorkflow/genDefaultLocale.ts +19 -0
- package/dist/templates/i18nWorkflow/genDiff.ts +49 -0
- package/dist/templates/i18nWorkflow/i18nConfig.ts +7 -0
- package/dist/templates/i18nWorkflow/index.ts +11 -0
- package/dist/templates/i18nWorkflow/protectedPatterns.ts +91 -0
- package/dist/templates/i18nWorkflow/utils.ts +76 -0
- package/dist/templates/index.tsx +170 -79
- package/dist/templates/jwt/index.ts +2 -1
- package/dist/templates/layout/GlobalProvider/AppTheme.tsx +136 -0
- package/dist/templates/layout/GlobalProvider/Locale.tsx +84 -0
- package/dist/templates/layout/GlobalProvider/Query.tsx +12 -0
- package/dist/templates/layout/GlobalProvider/StyleRegistry.tsx +9 -0
- package/dist/templates/layout/GlobalProvider/index.tsx +23 -0
- package/dist/templates/locales/utils.ts +23 -0
- package/dist/templates/package.json.tpl +7 -15
- package/dist/templates/pages/base/index.tsx +170 -79
- package/dist/templates/protectedPatterns.ts +91 -0
- package/dist/templates/routes.tsx +2 -2
- package/dist/templates/scripts/i18nWorkflow/analyzeUnusedKeys.ts +506 -0
- package/dist/templates/scripts/i18nWorkflow/cleanUnusedKeys.ts +344 -0
- package/dist/templates/scripts/i18nWorkflow/const.ts +18 -0
- package/dist/templates/scripts/i18nWorkflow/flattenLocaleKeys.ts +139 -0
- package/dist/templates/scripts/i18nWorkflow/genDefaultLocale.ts +19 -0
- package/dist/templates/scripts/i18nWorkflow/genDiff.ts +49 -0
- package/dist/templates/scripts/i18nWorkflow/i18nConfig.ts +7 -0
- package/dist/templates/scripts/i18nWorkflow/index.ts +11 -0
- package/dist/templates/scripts/i18nWorkflow/protectedPatterns.ts +91 -0
- package/dist/templates/scripts/i18nWorkflow/utils.ts +76 -0
- package/dist/templates/src/components/AntdStaticMethods/index.tsx +20 -0
- package/dist/templates/src/index.tsx +0 -4
- package/dist/templates/src/layout/GlobalProvider/AppTheme.tsx +136 -0
- package/dist/templates/src/layout/GlobalProvider/Locale.tsx +84 -0
- package/dist/templates/src/layout/GlobalProvider/Query.tsx +12 -0
- package/dist/templates/src/layout/GlobalProvider/StyleRegistry.tsx +9 -0
- package/dist/templates/src/layout/GlobalProvider/index.tsx +23 -0
- package/dist/templates/src/locales/utils.ts +23 -0
- package/dist/templates/src/pages/base/index.tsx +170 -79
- package/dist/templates/src/routes.tsx +2 -2
- package/dist/templates/tsconfig.json +19 -3
- package/dist/templates/type.ts +23 -24
- package/dist/templates/utils.ts +23 -0
- package/package.json +19 -21
|
@@ -11,14 +11,117 @@ import {
|
|
|
11
11
|
import React from "react";
|
|
12
12
|
import { createStyles } from "antd-style";
|
|
13
13
|
|
|
14
|
-
const { Title, Paragraph, Text
|
|
14
|
+
const { Title, Paragraph, Text } = Typography;
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
16
|
+
const px = (value: number | string) =>
|
|
17
|
+
typeof value === "number" ? `${value}px` : value;
|
|
18
|
+
|
|
19
|
+
const useStyles = createStyles(({ token, css }) => ({
|
|
20
|
+
root: css`
|
|
18
21
|
width: 100%;
|
|
19
|
-
height:
|
|
22
|
+
min-height: 100vh;
|
|
23
|
+
padding: ${px(token.paddingLG)};
|
|
24
|
+
background: linear-gradient(180deg, ${token.colorFillTertiary}, ${token.colorPrimaryHover});
|
|
20
25
|
overflow: auto;
|
|
21
26
|
`,
|
|
27
|
+
container: css`
|
|
28
|
+
margin: 0 auto;
|
|
29
|
+
max-width: 960px;
|
|
30
|
+
`,
|
|
31
|
+
hero: css`
|
|
32
|
+
text-align: center;
|
|
33
|
+
margin-bottom: ${px(token.marginLG)};
|
|
34
|
+
`,
|
|
35
|
+
heroIcon: css`
|
|
36
|
+
font-size: 3.5rem;
|
|
37
|
+
color: ${token.colorPrimary};
|
|
38
|
+
animation: bounce 1.2s infinite alternate;
|
|
39
|
+
|
|
40
|
+
@keyframes bounce {
|
|
41
|
+
from {
|
|
42
|
+
transform: translateY(0);
|
|
43
|
+
}
|
|
44
|
+
to {
|
|
45
|
+
transform: translateY(-6px);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`,
|
|
49
|
+
tagSpace: css`
|
|
50
|
+
margin-top: ${px(token.marginMD)};
|
|
51
|
+
`,
|
|
52
|
+
featuresGrid: css`
|
|
53
|
+
display: grid;
|
|
54
|
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
55
|
+
gap: ${px(token.marginSM)};
|
|
56
|
+
margin-bottom: ${px(token.marginLG)};
|
|
57
|
+
`,
|
|
58
|
+
featureCard: css`
|
|
59
|
+
text-align: center;
|
|
60
|
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
61
|
+
|
|
62
|
+
&:hover {
|
|
63
|
+
transform: translateY(-6px);
|
|
64
|
+
box-shadow: ${token.boxShadowSecondary};
|
|
65
|
+
}
|
|
66
|
+
`,
|
|
67
|
+
quickLinkGrid: css`
|
|
68
|
+
display: grid;
|
|
69
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
70
|
+
gap: ${px(token.marginSM)};
|
|
71
|
+
`,
|
|
72
|
+
quickLink: css`
|
|
73
|
+
padding: ${px(token.paddingMD)};
|
|
74
|
+
border-radius: ${px(token.borderRadiusLG)};
|
|
75
|
+
background: ${token.colorBgContainer};
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
transition: background 0.2s ease;
|
|
78
|
+
|
|
79
|
+
&:hover {
|
|
80
|
+
background: ${token.colorBgElevated};
|
|
81
|
+
}
|
|
82
|
+
`,
|
|
83
|
+
footerActions: css`
|
|
84
|
+
margin-top: ${px(token.marginMD)};
|
|
85
|
+
text-align: center;
|
|
86
|
+
`,
|
|
87
|
+
cardTitle: css`
|
|
88
|
+
display: inline-flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: ${px(token.marginXS)};
|
|
91
|
+
font-size: ${px(token.fontSizeHeading4)};
|
|
92
|
+
`,
|
|
93
|
+
codeBlock: css`
|
|
94
|
+
margin-top: ${px(token.marginXS)};
|
|
95
|
+
padding: ${px(token.paddingSM)};
|
|
96
|
+
background: ${token.colorFillAlter};
|
|
97
|
+
border-radius: ${px(token.borderRadiusLG)};
|
|
98
|
+
font-family: ${token.fontFamilyCode};
|
|
99
|
+
font-size: ${px(token.fontSizeSM)};
|
|
100
|
+
`,
|
|
101
|
+
stepLabel: css`
|
|
102
|
+
font-size: ${px(token.fontSizeBase)};
|
|
103
|
+
`,
|
|
104
|
+
quickLinkLabel: css`
|
|
105
|
+
font-size: ${px(token.fontSizeHeading2)};
|
|
106
|
+
margin-bottom: ${px(token.marginXS)};
|
|
107
|
+
`,
|
|
108
|
+
quickLinkDescription: css`
|
|
109
|
+
color: ${token.colorTextTertiary};
|
|
110
|
+
`,
|
|
111
|
+
panelCard: css`
|
|
112
|
+
margin-bottom: ${px(token.marginLG)};
|
|
113
|
+
background: ${token.colorBgElevated};
|
|
114
|
+
`,
|
|
115
|
+
featureIcon: css`
|
|
116
|
+
margin-bottom: ${px(token.marginMD)};
|
|
117
|
+
`,
|
|
118
|
+
footerParagraph: css`
|
|
119
|
+
margin-top: ${px(token.marginMD)};
|
|
120
|
+
color: ${token.colorTextTertiary};
|
|
121
|
+
`,
|
|
122
|
+
textCenterCard: css`
|
|
123
|
+
text-align: center;
|
|
124
|
+
`,
|
|
22
125
|
}));
|
|
23
126
|
|
|
24
127
|
export default function BasePage() {
|
|
@@ -28,17 +131,17 @@ export default function BasePage() {
|
|
|
28
131
|
|
|
29
132
|
const features = [
|
|
30
133
|
{
|
|
31
|
-
icon: <ThunderboltOutlined className=
|
|
134
|
+
icon: <ThunderboltOutlined className={styles.heroIcon} />,
|
|
32
135
|
title: "Rspack ๆๅปบ",
|
|
33
136
|
description: "ๅบไบ Rspack ็ๆ้ๆๅปบไฝ้ช๏ผๅผๅๆ็็ฟปๅ",
|
|
34
137
|
},
|
|
35
138
|
{
|
|
36
|
-
icon: <BulbOutlined className=
|
|
139
|
+
icon: <BulbOutlined className={styles.heroIcon} />,
|
|
37
140
|
title: "React 19",
|
|
38
141
|
description: "ไฝฟ็จๆๆฐ็ React 19 ็นๆง๏ผไบซๅๅนถๅๆธฒๆ",
|
|
39
142
|
},
|
|
40
143
|
{
|
|
41
|
-
icon: <SettingOutlined className=
|
|
144
|
+
icon: <SettingOutlined className={styles.heroIcon} />,
|
|
42
145
|
title: "TypeScript",
|
|
43
146
|
description: "ๅฎๆด็ TypeScript ๆฏๆ๏ผ็ฑปๅๅฎๅ
จๆไฟ้",
|
|
44
147
|
},
|
|
@@ -52,107 +155,94 @@ export default function BasePage() {
|
|
|
52
155
|
];
|
|
53
156
|
|
|
54
157
|
return (
|
|
55
|
-
<div
|
|
56
|
-
className={
|
|
57
|
-
styles.app,
|
|
58
|
-
"min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800 p-8"
|
|
59
|
-
)}
|
|
60
|
-
>
|
|
61
|
-
<div className="max-w-6xl mx-auto">
|
|
158
|
+
<div className={cx(styles.root)}>
|
|
159
|
+
<div className={styles.container}>
|
|
62
160
|
<Flex vertical gap={12}>
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<RocketOutlined className="text-6xl text-blue-500 animate-bounce" />
|
|
161
|
+
<section className={styles.hero}>
|
|
162
|
+
<div>
|
|
163
|
+
<RocketOutlined className={styles.heroIcon} />
|
|
67
164
|
</div>
|
|
68
|
-
<Title level={1}
|
|
165
|
+
<Title level={1} style={{ marginBottom: 16 }}>
|
|
69
166
|
๐ ๆญๅ๏ผ้กน็ฎๅๅปบๆๅ
|
|
70
167
|
</Title>
|
|
71
|
-
<Paragraph
|
|
168
|
+
<Paragraph>
|
|
72
169
|
ไฝ ็้กน็ฎๅทฒ็ปๅๅคๅฐฑ็ปช๏ผ็ฐๅจๅฏไปฅๅผๅงๅผๅไบ
|
|
73
170
|
</Paragraph>
|
|
74
|
-
<Space size="middle" className=
|
|
171
|
+
<Space size="middle" className={styles.tagSpace}>
|
|
75
172
|
<Tag color="blue">React 19</Tag>
|
|
76
173
|
<Tag color="green">TypeScript</Tag>
|
|
77
174
|
<Tag color="orange">Rspack</Tag>
|
|
78
175
|
<Tag color="purple">Ant Design</Tag>
|
|
79
176
|
<Tag color="cyan">Tailwind CSS</Tag>
|
|
80
177
|
</Space>
|
|
81
|
-
</
|
|
178
|
+
</section>
|
|
82
179
|
|
|
83
|
-
{
|
|
84
|
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
|
180
|
+
<div className={styles.featuresGrid}>
|
|
85
181
|
{features.map((feature, index) => (
|
|
86
182
|
<Card
|
|
87
183
|
key={index}
|
|
88
184
|
hoverable
|
|
89
|
-
className=
|
|
185
|
+
className={cx(styles.featureCard)}
|
|
186
|
+
bodyStyle={{ padding: 24 }}
|
|
90
187
|
>
|
|
91
|
-
<div className=
|
|
188
|
+
<div className={styles.featureIcon}>{feature.icon}</div>
|
|
92
189
|
<Title level={4}>{feature.title}</Title>
|
|
93
|
-
<Paragraph
|
|
94
|
-
{feature.description}
|
|
95
|
-
</Paragraph>
|
|
190
|
+
<Paragraph type="secondary">{feature.description}</Paragraph>
|
|
96
191
|
</Card>
|
|
97
192
|
))}
|
|
98
193
|
</div>
|
|
99
194
|
|
|
100
|
-
{/* ๅฟซ้ๅผๅง */}
|
|
101
195
|
<Card
|
|
102
196
|
title={
|
|
103
|
-
<span className=
|
|
104
|
-
<BookOutlined
|
|
197
|
+
<span className={styles.cardTitle}>
|
|
198
|
+
<BookOutlined />
|
|
105
199
|
ๅฟซ้ๅผๅง
|
|
106
200
|
</span>
|
|
107
201
|
}
|
|
108
|
-
className=
|
|
202
|
+
className={styles.panelCard}
|
|
109
203
|
>
|
|
110
|
-
<Space direction="vertical" size="large"
|
|
204
|
+
<Space direction="vertical" size="large">
|
|
111
205
|
<div>
|
|
112
|
-
<Text strong className=
|
|
206
|
+
<Text strong className={styles.stepLabel}>
|
|
113
207
|
1. ๅฎ่ฃ
ไพ่ต
|
|
114
208
|
</Text>
|
|
115
|
-
<div className=
|
|
209
|
+
<div className={styles.codeBlock}>
|
|
116
210
|
<code>pnpm install</code>
|
|
117
211
|
</div>
|
|
118
212
|
</div>
|
|
119
213
|
<div>
|
|
120
|
-
<Text strong className=
|
|
214
|
+
<Text strong className={styles.stepLabel}>
|
|
121
215
|
2. ๅฏๅจๅผๅๆๅกๅจ
|
|
122
216
|
</Text>
|
|
123
|
-
<div className=
|
|
217
|
+
<div className={styles.codeBlock}>
|
|
124
218
|
<code>pnpm dev</code>
|
|
125
219
|
</div>
|
|
126
220
|
</div>
|
|
127
221
|
<div>
|
|
128
|
-
<Text strong className=
|
|
222
|
+
<Text strong className={styles.stepLabel}>
|
|
129
223
|
3. ๆๅปบ็ไบง็ๆฌ
|
|
130
224
|
</Text>
|
|
131
|
-
<div className=
|
|
225
|
+
<div className={styles.codeBlock}>
|
|
132
226
|
<code>pnpm build</code>
|
|
133
227
|
</div>
|
|
134
228
|
</div>
|
|
135
229
|
</Space>
|
|
136
230
|
</Card>
|
|
137
231
|
|
|
138
|
-
{/* ๅฟซๆท้พๆฅ */}
|
|
139
232
|
<Card
|
|
140
233
|
title={
|
|
141
|
-
<span className=
|
|
142
|
-
<GithubOutlined
|
|
234
|
+
<span className={styles.cardTitle}>
|
|
235
|
+
<GithubOutlined />
|
|
143
236
|
ๅฟซๆท้พๆฅ
|
|
144
237
|
</span>
|
|
145
238
|
}
|
|
146
|
-
className=
|
|
239
|
+
className={styles.panelCard}
|
|
147
240
|
>
|
|
148
|
-
<div className=
|
|
241
|
+
<div className={styles.quickLinkGrid}>
|
|
149
242
|
{quickLinks.map((link, index) => (
|
|
150
|
-
<div
|
|
151
|
-
|
|
152
|
-
className=
|
|
153
|
-
>
|
|
154
|
-
<div className="text-2xl mb-2">{link.label}</div>
|
|
155
|
-
<Text className="text-sm text-gray-600 dark:text-gray-400">
|
|
243
|
+
<div key={index} className={styles.quickLink}>
|
|
244
|
+
<div className={styles.quickLinkLabel}>{link.label}</div>
|
|
245
|
+
<Text className={styles.quickLinkDescription}>
|
|
156
246
|
{link.description}
|
|
157
247
|
</Text>
|
|
158
248
|
</div>
|
|
@@ -160,34 +250,35 @@ export default function BasePage() {
|
|
|
160
250
|
</div>
|
|
161
251
|
</Card>
|
|
162
252
|
|
|
163
|
-
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
253
|
+
<Card className={cx(styles.panelCard, styles.textCenterCard)}>
|
|
254
|
+
<div className={styles.footerActions}>
|
|
255
|
+
<Space size="large" wrap>
|
|
256
|
+
<Button
|
|
257
|
+
type="primary"
|
|
258
|
+
size="large"
|
|
259
|
+
icon={<RocketOutlined />}
|
|
260
|
+
onClick={() => window.open("https://ant.design", "_blank")}
|
|
261
|
+
>
|
|
262
|
+
ๆฅ็ Ant Design ๆๆกฃ
|
|
263
|
+
</Button>
|
|
264
|
+
<Button
|
|
265
|
+
size="large"
|
|
266
|
+
icon={<BookOutlined />}
|
|
267
|
+
onClick={() => window.open("https://react.dev", "_blank")}
|
|
268
|
+
>
|
|
269
|
+
React ๆๆกฃ
|
|
270
|
+
</Button>
|
|
271
|
+
<Button
|
|
272
|
+
size="large"
|
|
273
|
+
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
274
|
+
>
|
|
275
|
+
ๅๆขไธป้ข ({theme === "dark" ? "๐ ๆ่ฒ" : "โ๏ธ ไบฎ่ฒ"})
|
|
276
|
+
</Button>
|
|
277
|
+
</Space>
|
|
278
|
+
<Paragraph className={styles.footerParagraph}>
|
|
279
|
+
็ฅไฝ ๅผๅๆๅฟซ๏ผๅฆๆ้ฎ้ข๏ผ่ฏทๆฅ้
ๆๆกฃๆ่็ณปๆๆฏๆฏๆ ๐ช
|
|
280
|
+
</Paragraph>
|
|
281
|
+
</div>
|
|
191
282
|
</Card>
|
|
192
283
|
</Flex>
|
|
193
284
|
</div>
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/* eslint-disable unicorn/prefer-top-level-await */
|
|
2
|
+
import { consola } from 'consola';
|
|
3
|
+
import { colors } from 'consola/utils';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { IGNORED_FILES } from './protectedPatterns';
|
|
8
|
+
|
|
9
|
+
interface UnusedKey {
|
|
10
|
+
filePath: string;
|
|
11
|
+
fullKey: string;
|
|
12
|
+
key: string;
|
|
13
|
+
namespace: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ReportData {
|
|
17
|
+
generatedAt: string;
|
|
18
|
+
statistics: {
|
|
19
|
+
totalKeys: number;
|
|
20
|
+
unusedKeys: number;
|
|
21
|
+
usageRate: string;
|
|
22
|
+
usedKeys: number;
|
|
23
|
+
};
|
|
24
|
+
unusedKeys: UnusedKey[];
|
|
25
|
+
unusedKeysByNamespace: Array<{
|
|
26
|
+
count: number;
|
|
27
|
+
keys: string[];
|
|
28
|
+
namespace: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Remove a key from a nested object
|
|
34
|
+
*/
|
|
35
|
+
function removeKeyFromObject(obj: any, keyPath: string): boolean {
|
|
36
|
+
const keys = keyPath.split('.');
|
|
37
|
+
const lastKey = keys.pop()!;
|
|
38
|
+
|
|
39
|
+
let current = obj;
|
|
40
|
+
const parents: Array<{ key: string; obj: any }> = [];
|
|
41
|
+
|
|
42
|
+
// Navigate to the parent of the target key
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
if (!current[key]) {
|
|
45
|
+
return false; // Key path doesn't exist
|
|
46
|
+
}
|
|
47
|
+
parents.push({ key, obj: current });
|
|
48
|
+
current = current[key];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Remove the key
|
|
52
|
+
if (lastKey in current) {
|
|
53
|
+
delete current[lastKey];
|
|
54
|
+
|
|
55
|
+
// Clean up empty parent objects
|
|
56
|
+
for (let i = parents.length - 1; i >= 0; i--) {
|
|
57
|
+
const { obj, key } = parents[i];
|
|
58
|
+
if (Object.keys(obj[key]).length === 0) {
|
|
59
|
+
delete obj[key];
|
|
60
|
+
} else {
|
|
61
|
+
break; // Stop if parent still has other keys
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Clean unused keys from TypeScript default locale files
|
|
73
|
+
*/
|
|
74
|
+
function cleanDefaultLocaleFiles(unusedKeys: UnusedKey[], dryRun: boolean = true) {
|
|
75
|
+
const defaultLocalesPath = path.join(process.cwd(), 'src/locales/default');
|
|
76
|
+
|
|
77
|
+
// Get ignored namespace names from IGNORED_FILES (remove .ts extension)
|
|
78
|
+
const ignoredNamespaces = new Set(IGNORED_FILES.map((f) => f.replace('.ts', '')));
|
|
79
|
+
|
|
80
|
+
// Group by namespace
|
|
81
|
+
const byNamespace = new Map<string, string[]>();
|
|
82
|
+
for (const key of unusedKeys) {
|
|
83
|
+
// Skip ignored namespaces (from IGNORED_FILES)
|
|
84
|
+
if (ignoredNamespaces.has(key.namespace)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!byNamespace.has(key.namespace)) {
|
|
89
|
+
byNamespace.set(key.namespace, []);
|
|
90
|
+
}
|
|
91
|
+
byNamespace.get(key.namespace)!.push(key.key);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
consola.info(`Processing ${byNamespace.size} namespace files...`);
|
|
95
|
+
consola.info('');
|
|
96
|
+
|
|
97
|
+
let totalRemoved = 0;
|
|
98
|
+
|
|
99
|
+
for (const [namespace, keys] of byNamespace.entries()) {
|
|
100
|
+
const filePath = path.join(defaultLocalesPath, `${namespace}.ts`);
|
|
101
|
+
|
|
102
|
+
if (!fs.existsSync(filePath)) {
|
|
103
|
+
consola.warn(`File not found: ${filePath}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
109
|
+
const loadedModule = require(filePath);
|
|
110
|
+
const translations = loadedModule.default || loadedModule;
|
|
111
|
+
|
|
112
|
+
// Create a deep copy to avoid modifying the original
|
|
113
|
+
const updatedTranslations = structuredClone(translations);
|
|
114
|
+
|
|
115
|
+
let removedCount = 0;
|
|
116
|
+
|
|
117
|
+
// Remove each unused key
|
|
118
|
+
for (const key of keys) {
|
|
119
|
+
if (removeKeyFromObject(updatedTranslations, key)) {
|
|
120
|
+
removedCount++;
|
|
121
|
+
totalRemoved++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (removedCount > 0) {
|
|
126
|
+
consola.info(
|
|
127
|
+
colors.cyan(namespace.padEnd(20)),
|
|
128
|
+
colors.gray('โ'),
|
|
129
|
+
colors.red(`${removedCount} keys to remove`),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (!dryRun) {
|
|
133
|
+
// Generate new content
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
135
|
+
const newContent = generateTypeScriptContent(updatedTranslations);
|
|
136
|
+
|
|
137
|
+
// Write back to file
|
|
138
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
139
|
+
consola.success(` โ Updated ${filePath}`);
|
|
140
|
+
} else {
|
|
141
|
+
consola.info(` ${colors.gray('(dry run - no changes made)')}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
consola.error(`Failed to process ${namespace}:`, error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return totalRemoved;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clean unused keys from JSON locale files
|
|
154
|
+
*/
|
|
155
|
+
function cleanLocaleJsonFiles(unusedKeys: UnusedKey[], dryRun: boolean = true) {
|
|
156
|
+
const localesPath = path.join(process.cwd(), 'locales');
|
|
157
|
+
const locales = fs
|
|
158
|
+
.readdirSync(localesPath)
|
|
159
|
+
.filter((f) => fs.statSync(path.join(localesPath, f)).isDirectory());
|
|
160
|
+
|
|
161
|
+
consola.info(`Processing ${locales.length} locale directories...`);
|
|
162
|
+
consola.info('');
|
|
163
|
+
|
|
164
|
+
// Get ignored namespace names from IGNORED_FILES (remove .ts extension)
|
|
165
|
+
const ignoredNamespaces = new Set(IGNORED_FILES.map((f) => f.replace('.ts', '')));
|
|
166
|
+
|
|
167
|
+
// Group by namespace
|
|
168
|
+
const byNamespace = new Map<string, string[]>();
|
|
169
|
+
for (const key of unusedKeys) {
|
|
170
|
+
// Skip ignored namespaces (from IGNORED_FILES)
|
|
171
|
+
if (ignoredNamespaces.has(key.namespace)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!byNamespace.has(key.namespace)) {
|
|
176
|
+
byNamespace.set(key.namespace, []);
|
|
177
|
+
}
|
|
178
|
+
byNamespace.get(key.namespace)!.push(key.key);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let totalRemoved = 0;
|
|
182
|
+
|
|
183
|
+
for (const locale of locales) {
|
|
184
|
+
consola.info(colors.cyan(`Locale: ${locale}`));
|
|
185
|
+
|
|
186
|
+
for (const [namespace, keys] of byNamespace.entries()) {
|
|
187
|
+
const filePath = path.join(localesPath, locale, `${namespace}.json`);
|
|
188
|
+
|
|
189
|
+
if (!fs.existsSync(filePath)) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
195
|
+
const translations = JSON.parse(content);
|
|
196
|
+
|
|
197
|
+
let removedCount = 0;
|
|
198
|
+
|
|
199
|
+
// Remove each unused key
|
|
200
|
+
for (const key of keys) {
|
|
201
|
+
if (removeKeyFromObject(translations, key)) {
|
|
202
|
+
removedCount++;
|
|
203
|
+
totalRemoved++;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (removedCount > 0) {
|
|
208
|
+
consola.info(
|
|
209
|
+
` ${colors.gray(namespace.padEnd(20))} โ ${colors.red(removedCount + ' keys removed')}`,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (!dryRun) {
|
|
213
|
+
// Write back to file with pretty formatting
|
|
214
|
+
fs.writeFileSync(filePath, JSON.stringify(translations, null, 2) + '\n', 'utf8');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
consola.error(`Failed to process ${locale}/${namespace}:`, error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
consola.info('');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return totalRemoved;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check if a key needs quotes in TypeScript object notation
|
|
230
|
+
*/
|
|
231
|
+
function needsQuotes(key: string): boolean {
|
|
232
|
+
// Keys that need quotes:
|
|
233
|
+
// - Contains special characters (-, ., spaces, etc.)
|
|
234
|
+
// - Starts with a number
|
|
235
|
+
// - Is a reserved keyword
|
|
236
|
+
return !/^[$A-Z_a-z][\w$]*$/.test(key);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Generate TypeScript file content from object
|
|
241
|
+
*/
|
|
242
|
+
function generateTypeScriptContent(obj: any): string {
|
|
243
|
+
const jsonString = JSON.stringify(obj, null, 2);
|
|
244
|
+
|
|
245
|
+
// Convert JSON to TypeScript object notation
|
|
246
|
+
// Handle keys that need quotes vs those that don't
|
|
247
|
+
let tsContent = jsonString.replaceAll(/"([^"]+)":/g, (match, key) => {
|
|
248
|
+
if (needsQuotes(key)) {
|
|
249
|
+
// Keep quotes for keys with special characters
|
|
250
|
+
return `'${key}':`;
|
|
251
|
+
}
|
|
252
|
+
// Remove quotes for valid identifiers
|
|
253
|
+
return `${key}:`;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Use single quotes for string values
|
|
257
|
+
tsContent = tsContent.replaceAll(/: "([^"]*)"/g, ": '$1'");
|
|
258
|
+
|
|
259
|
+
return `export default ${tsContent};\n`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Main function
|
|
264
|
+
*/
|
|
265
|
+
async function main() {
|
|
266
|
+
const reportPath = path.join(process.cwd(), 'i18n-unused-keys-report.json');
|
|
267
|
+
|
|
268
|
+
// Check if report exists
|
|
269
|
+
if (!fs.existsSync(reportPath)) {
|
|
270
|
+
consola.error(
|
|
271
|
+
`Report file not found: ${reportPath}\n` +
|
|
272
|
+
'Please run "bun run workflow:i18n-analyze" first to generate the report.',
|
|
273
|
+
);
|
|
274
|
+
throw new Error('Report file not found');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Load report
|
|
278
|
+
const reportContent = fs.readFileSync(reportPath, 'utf8');
|
|
279
|
+
const report: ReportData = JSON.parse(reportContent);
|
|
280
|
+
|
|
281
|
+
consola.box('๐งน Clean Unused i18n Keys');
|
|
282
|
+
consola.info('');
|
|
283
|
+
|
|
284
|
+
// Show statistics
|
|
285
|
+
consola.info(colors.cyan('Statistics from report:'));
|
|
286
|
+
consola.info(` Total keys: ${report.statistics.totalKeys}`);
|
|
287
|
+
consola.info(` Used keys: ${report.statistics.usedKeys}`);
|
|
288
|
+
consola.info(` Unused keys: ${colors.red(report.statistics.unusedKeys.toString())}`);
|
|
289
|
+
consola.info(` Usage rate: ${report.statistics.usageRate}`);
|
|
290
|
+
consola.info('');
|
|
291
|
+
|
|
292
|
+
if (report.unusedKeys.length === 0) {
|
|
293
|
+
consola.success('No unused keys to clean!');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Ask for confirmation
|
|
298
|
+
const args = process.argv.slice(2);
|
|
299
|
+
const dryRun = !args.includes('--no-dry-run');
|
|
300
|
+
|
|
301
|
+
if (dryRun) {
|
|
302
|
+
consola.warn('Running in DRY RUN mode - no files will be modified');
|
|
303
|
+
consola.info('To actually clean the files, run: bun run workflow:i18n-clean --no-dry-run');
|
|
304
|
+
consola.info('');
|
|
305
|
+
} else {
|
|
306
|
+
consola.warn('โ ๏ธ WARNING: This will modify your locale files!');
|
|
307
|
+
consola.info('Make sure you have committed your changes or have a backup.');
|
|
308
|
+
consola.info('');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Clean default locale files (TypeScript)
|
|
312
|
+
consola.box('Step 1: Cleaning default locale files (TypeScript)');
|
|
313
|
+
const removedFromDefault = cleanDefaultLocaleFiles(report.unusedKeys, dryRun);
|
|
314
|
+
consola.info('');
|
|
315
|
+
|
|
316
|
+
// Clean locale JSON files
|
|
317
|
+
consola.box('Step 2: Cleaning locale JSON files');
|
|
318
|
+
const removedFromJson = cleanLocaleJsonFiles(report.unusedKeys, dryRun);
|
|
319
|
+
consola.info('');
|
|
320
|
+
|
|
321
|
+
// Summary
|
|
322
|
+
consola.box('Summary');
|
|
323
|
+
consola.info(`Keys marked for removal: ${colors.red(report.unusedKeys.length.toString())}`);
|
|
324
|
+
consola.info(
|
|
325
|
+
`Total operations: ${colors.yellow((removedFromDefault + removedFromJson).toString())}`,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
if (dryRun) {
|
|
329
|
+
consola.info('');
|
|
330
|
+
consola.warn('This was a DRY RUN - no files were modified');
|
|
331
|
+
consola.info('To actually clean the files, run:');
|
|
332
|
+
consola.info(colors.cyan(' bun run workflow:i18n-clean --no-dry-run'));
|
|
333
|
+
} else {
|
|
334
|
+
consola.success('โ Cleanup completed!');
|
|
335
|
+
consola.info('');
|
|
336
|
+
consola.info('Next steps:');
|
|
337
|
+
consola.info(' 1. Review the changes with git diff');
|
|
338
|
+
consola.info(' 2. Run "bun run i18n" to regenerate all locale files');
|
|
339
|
+
consola.info(' 3. Test your application');
|
|
340
|
+
consola.info(' 4. Commit the changes');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
main();
|