@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.
Files changed (96) hide show
  1. package/dist/templates/AntdStaticMethods/index.tsx +20 -0
  2. package/dist/templates/AppTheme.tsx +136 -0
  3. package/dist/templates/DIRECTORY_STRUCTURE.md +141 -0
  4. package/dist/templates/GlobalProvider/AppTheme.tsx +136 -0
  5. package/dist/templates/GlobalProvider/Locale.tsx +84 -0
  6. package/dist/templates/GlobalProvider/Query.tsx +12 -0
  7. package/dist/templates/GlobalProvider/StyleRegistry.tsx +9 -0
  8. package/dist/templates/GlobalProvider/index.tsx +23 -0
  9. package/dist/templates/Locale.tsx +55 -56
  10. package/dist/templates/Query.tsx +12 -0
  11. package/dist/templates/StyleRegistry.tsx +9 -0
  12. package/dist/templates/analyzeUnusedKeys.ts +506 -0
  13. package/dist/templates/app/.i18nrc.js +57 -0
  14. package/dist/templates/app/config/jwt/index.ts +2 -1
  15. package/dist/templates/app/docs/DIRECTORY_STRUCTURE.md +141 -0
  16. package/dist/templates/app/docs/glossary.md +11 -0
  17. package/dist/templates/app/package.json.tpl +7 -15
  18. package/dist/templates/app/scripts/i18nWorkflow/analyzeUnusedKeys.ts +506 -0
  19. package/dist/templates/app/scripts/i18nWorkflow/cleanUnusedKeys.ts +344 -0
  20. package/dist/templates/app/scripts/i18nWorkflow/const.ts +18 -0
  21. package/dist/templates/app/scripts/i18nWorkflow/flattenLocaleKeys.ts +139 -0
  22. package/dist/templates/app/scripts/i18nWorkflow/genDefaultLocale.ts +19 -0
  23. package/dist/templates/app/scripts/i18nWorkflow/genDiff.ts +49 -0
  24. package/dist/templates/app/scripts/i18nWorkflow/i18nConfig.ts +7 -0
  25. package/dist/templates/app/scripts/i18nWorkflow/index.ts +11 -0
  26. package/dist/templates/app/scripts/i18nWorkflow/protectedPatterns.ts +91 -0
  27. package/dist/templates/app/scripts/i18nWorkflow/utils.ts +76 -0
  28. package/dist/templates/app/src/components/AntdStaticMethods/index.tsx +20 -0
  29. package/dist/templates/app/src/index.tsx +0 -4
  30. package/dist/templates/app/src/layout/GlobalProvider/AppTheme.tsx +136 -0
  31. package/dist/templates/app/src/layout/GlobalProvider/Locale.tsx +84 -0
  32. package/dist/templates/app/src/layout/GlobalProvider/Query.tsx +12 -0
  33. package/dist/templates/app/src/layout/GlobalProvider/StyleRegistry.tsx +9 -0
  34. package/dist/templates/app/src/layout/GlobalProvider/index.tsx +23 -0
  35. package/dist/templates/app/src/locales/utils.ts +23 -0
  36. package/dist/templates/app/src/pages/base/index.tsx +170 -79
  37. package/dist/templates/app/src/routes.tsx +2 -2
  38. package/dist/templates/app/tsconfig.json +19 -3
  39. package/dist/templates/base/index.tsx +170 -79
  40. package/dist/templates/cleanUnusedKeys.ts +344 -0
  41. package/dist/templates/components/AntdStaticMethods/index.tsx +20 -0
  42. package/dist/templates/config/jwt/index.ts +2 -1
  43. package/dist/templates/const.ts +18 -0
  44. package/dist/templates/docs/DIRECTORY_STRUCTURE.md +141 -0
  45. package/dist/templates/docs/glossary.md +11 -0
  46. package/dist/templates/flattenLocaleKeys.ts +139 -0
  47. package/dist/templates/genDefaultLocale.ts +19 -0
  48. package/dist/templates/genDiff.ts +49 -0
  49. package/dist/templates/glossary.md +11 -0
  50. package/dist/templates/i18nConfig.ts +7 -0
  51. package/dist/templates/i18nWorkflow/analyzeUnusedKeys.ts +506 -0
  52. package/dist/templates/i18nWorkflow/cleanUnusedKeys.ts +344 -0
  53. package/dist/templates/i18nWorkflow/const.ts +18 -0
  54. package/dist/templates/i18nWorkflow/flattenLocaleKeys.ts +139 -0
  55. package/dist/templates/i18nWorkflow/genDefaultLocale.ts +19 -0
  56. package/dist/templates/i18nWorkflow/genDiff.ts +49 -0
  57. package/dist/templates/i18nWorkflow/i18nConfig.ts +7 -0
  58. package/dist/templates/i18nWorkflow/index.ts +11 -0
  59. package/dist/templates/i18nWorkflow/protectedPatterns.ts +91 -0
  60. package/dist/templates/i18nWorkflow/utils.ts +76 -0
  61. package/dist/templates/index.tsx +170 -79
  62. package/dist/templates/jwt/index.ts +2 -1
  63. package/dist/templates/layout/GlobalProvider/AppTheme.tsx +136 -0
  64. package/dist/templates/layout/GlobalProvider/Locale.tsx +84 -0
  65. package/dist/templates/layout/GlobalProvider/Query.tsx +12 -0
  66. package/dist/templates/layout/GlobalProvider/StyleRegistry.tsx +9 -0
  67. package/dist/templates/layout/GlobalProvider/index.tsx +23 -0
  68. package/dist/templates/locales/utils.ts +23 -0
  69. package/dist/templates/package.json.tpl +7 -15
  70. package/dist/templates/pages/base/index.tsx +170 -79
  71. package/dist/templates/protectedPatterns.ts +91 -0
  72. package/dist/templates/routes.tsx +2 -2
  73. package/dist/templates/scripts/i18nWorkflow/analyzeUnusedKeys.ts +506 -0
  74. package/dist/templates/scripts/i18nWorkflow/cleanUnusedKeys.ts +344 -0
  75. package/dist/templates/scripts/i18nWorkflow/const.ts +18 -0
  76. package/dist/templates/scripts/i18nWorkflow/flattenLocaleKeys.ts +139 -0
  77. package/dist/templates/scripts/i18nWorkflow/genDefaultLocale.ts +19 -0
  78. package/dist/templates/scripts/i18nWorkflow/genDiff.ts +49 -0
  79. package/dist/templates/scripts/i18nWorkflow/i18nConfig.ts +7 -0
  80. package/dist/templates/scripts/i18nWorkflow/index.ts +11 -0
  81. package/dist/templates/scripts/i18nWorkflow/protectedPatterns.ts +91 -0
  82. package/dist/templates/scripts/i18nWorkflow/utils.ts +76 -0
  83. package/dist/templates/src/components/AntdStaticMethods/index.tsx +20 -0
  84. package/dist/templates/src/index.tsx +0 -4
  85. package/dist/templates/src/layout/GlobalProvider/AppTheme.tsx +136 -0
  86. package/dist/templates/src/layout/GlobalProvider/Locale.tsx +84 -0
  87. package/dist/templates/src/layout/GlobalProvider/Query.tsx +12 -0
  88. package/dist/templates/src/layout/GlobalProvider/StyleRegistry.tsx +9 -0
  89. package/dist/templates/src/layout/GlobalProvider/index.tsx +23 -0
  90. package/dist/templates/src/locales/utils.ts +23 -0
  91. package/dist/templates/src/pages/base/index.tsx +170 -79
  92. package/dist/templates/src/routes.tsx +2 -2
  93. package/dist/templates/tsconfig.json +19 -3
  94. package/dist/templates/type.ts +23 -24
  95. package/dist/templates/utils.ts +23 -0
  96. 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, Link } = Typography;
14
+ const { Title, Paragraph, Text } = Typography;
15
15
 
16
- const useStyles = createStyles(({ css, token }) => ({
17
- app: css`
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: 100%;
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="text-4xl text-blue-500" />,
134
+ icon: <ThunderboltOutlined className={styles.heroIcon} />,
32
135
  title: "Rspack ๆž„ๅปบ",
33
136
  description: "ๅŸบไบŽ Rspack ็š„ๆž้€Ÿๆž„ๅปบไฝ“้ชŒ๏ผŒๅผ€ๅ‘ๆ•ˆ็އ็ฟปๅ€",
34
137
  },
35
138
  {
36
- icon: <BulbOutlined className="text-4xl text-yellow-500" />,
139
+ icon: <BulbOutlined className={styles.heroIcon} />,
37
140
  title: "React 19",
38
141
  description: "ไฝฟ็”จๆœ€ๆ–ฐ็š„ React 19 ็‰นๆ€ง๏ผŒไบซๅ—ๅนถๅ‘ๆธฒๆŸ“",
39
142
  },
40
143
  {
41
- icon: <SettingOutlined className="text-4xl text-green-500" />,
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={cx(
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
- <div className="text-center mb-12 animate-fade-in">
65
- <div className="inline-block mb-4">
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} className="!mb-4">
165
+ <Title level={1} style={{ marginBottom: 16 }}>
69
166
  ๐ŸŽ‰ ๆญๅ–œ๏ผ้กน็›ฎๅˆ›ๅปบๆˆๅŠŸ
70
167
  </Title>
71
- <Paragraph className="text-lg text-gray-600 dark:text-gray-300">
168
+ <Paragraph>
72
169
  ไฝ ็š„้กน็›ฎๅทฒ็ปๅ‡†ๅค‡ๅฐฑ็ปช๏ผŒ็Žฐๅœจๅฏไปฅๅผ€ๅง‹ๅผ€ๅ‘ไบ†
73
170
  </Paragraph>
74
- <Space size="middle" className="mt-4">
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
- </div>
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="text-center transition-all duration-300 hover:shadow-xl dark:bg-gray-800"
185
+ className={cx(styles.featureCard)}
186
+ bodyStyle={{ padding: 24 }}
90
187
  >
91
- <div className="mb-4">{feature.icon}</div>
188
+ <div className={styles.featureIcon}>{feature.icon}</div>
92
189
  <Title level={4}>{feature.title}</Title>
93
- <Paragraph className="text-gray-600 dark:text-gray-400">
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="text-xl">
104
- <BookOutlined className="mr-2" />
197
+ <span className={styles.cardTitle}>
198
+ <BookOutlined />
105
199
  ๅฟซ้€Ÿๅผ€ๅง‹
106
200
  </span>
107
201
  }
108
- className="mb-8 dark:bg-gray-800"
202
+ className={styles.panelCard}
109
203
  >
110
- <Space direction="vertical" size="large" className="w-full">
204
+ <Space direction="vertical" size="large">
111
205
  <div>
112
- <Text strong className="text-base">
206
+ <Text strong className={styles.stepLabel}>
113
207
  1. ๅฎ‰่ฃ…ไพ่ต–
114
208
  </Text>
115
- <div className="mt-2 p-4 bg-gray-100 dark:bg-gray-900 rounded-lg font-mono text-sm">
209
+ <div className={styles.codeBlock}>
116
210
  <code>pnpm install</code>
117
211
  </div>
118
212
  </div>
119
213
  <div>
120
- <Text strong className="text-base">
214
+ <Text strong className={styles.stepLabel}>
121
215
  2. ๅฏๅŠจๅผ€ๅ‘ๆœๅŠกๅ™จ
122
216
  </Text>
123
- <div className="mt-2 p-4 bg-gray-100 dark:bg-gray-900 rounded-lg font-mono text-sm">
217
+ <div className={styles.codeBlock}>
124
218
  <code>pnpm dev</code>
125
219
  </div>
126
220
  </div>
127
221
  <div>
128
- <Text strong className="text-base">
222
+ <Text strong className={styles.stepLabel}>
129
223
  3. ๆž„ๅปบ็”Ÿไบง็‰ˆๆœฌ
130
224
  </Text>
131
- <div className="mt-2 p-4 bg-gray-100 dark:bg-gray-900 rounded-lg font-mono text-sm">
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="text-xl">
142
- <GithubOutlined className="mr-2" />
234
+ <span className={styles.cardTitle}>
235
+ <GithubOutlined />
143
236
  ๅฟซๆท้“พๆŽฅ
144
237
  </span>
145
238
  }
146
- className="mb-8 dark:bg-gray-800"
239
+ className={styles.panelCard}
147
240
  >
148
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
241
+ <div className={styles.quickLinkGrid}>
149
242
  {quickLinks.map((link, index) => (
150
- <div
151
- key={index}
152
- className="p-4 bg-gray-50 dark:bg-gray-900 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors"
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
- <Card className="text-center dark:bg-gray-800">
165
- <Space size="large" wrap>
166
- <Button
167
- type="primary"
168
- size="large"
169
- icon={<RocketOutlined />}
170
- onClick={() => window.open("https://ant.design", "_blank")}
171
- >
172
- ๆŸฅ็œ‹ Ant Design ๆ–‡ๆกฃ
173
- </Button>
174
- <Button
175
- size="large"
176
- icon={<BookOutlined />}
177
- onClick={() => window.open("https://react.dev", "_blank")}
178
- >
179
- React ๆ–‡ๆกฃ
180
- </Button>
181
- <Button
182
- size="large"
183
- onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
184
- >
185
- ๅˆ‡ๆขไธป้ข˜ ({theme === "dark" ? "๐ŸŒ™ ๆš—่‰ฒ" : "โ˜€๏ธ ไบฎ่‰ฒ"})
186
- </Button>
187
- </Space>
188
- <Paragraph className="mt-6 text-gray-500 dark:text-gray-400">
189
- ็ฅไฝ ๅผ€ๅ‘ๆ„‰ๅฟซ๏ผๅฆ‚ๆœ‰้—ฎ้ข˜๏ผŒ่ฏทๆŸฅ้˜…ๆ–‡ๆกฃๆˆ–่”็ณปๆŠ€ๆœฏๆ”ฏๆŒ ๐Ÿ’ช
190
- </Paragraph>
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();