@neyugn/agent-kits 0.1.0
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/LICENSE +21 -0
- package/README.md +514 -0
- package/README.vi.md +410 -0
- package/README.zh.md +410 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +422 -0
- package/kits/coder/ARCHITECTURE.md +289 -0
- package/kits/coder/agents/ai-engineer.md +344 -0
- package/kits/coder/agents/backend-specialist.md +270 -0
- package/kits/coder/agents/cloud-architect.md +363 -0
- package/kits/coder/agents/code-reviewer.md +284 -0
- package/kits/coder/agents/data-engineer.md +401 -0
- package/kits/coder/agents/database-specialist.md +251 -0
- package/kits/coder/agents/debugger.md +209 -0
- package/kits/coder/agents/devops-engineer.md +281 -0
- package/kits/coder/agents/documentation-writer.md +296 -0
- package/kits/coder/agents/frontend-specialist.md +298 -0
- package/kits/coder/agents/i18n-specialist.md +348 -0
- package/kits/coder/agents/integration-specialist.md +314 -0
- package/kits/coder/agents/mobile-developer.md +271 -0
- package/kits/coder/agents/multi-tenant-architect.md +281 -0
- package/kits/coder/agents/orchestrator.md +263 -0
- package/kits/coder/agents/performance-analyst.md +327 -0
- package/kits/coder/agents/project-planner.md +277 -0
- package/kits/coder/agents/queue-specialist.md +282 -0
- package/kits/coder/agents/realtime-specialist.md +267 -0
- package/kits/coder/agents/security-auditor.md +253 -0
- package/kits/coder/agents/test-engineer.md +315 -0
- package/kits/coder/agents/ux-researcher.md +388 -0
- package/kits/coder/rules/.cursorrules +287 -0
- package/kits/coder/rules/CLAUDE.md +287 -0
- package/kits/coder/rules/CODEX.md +287 -0
- package/kits/coder/rules/GEMINI.md +287 -0
- package/kits/coder/scripts/checklist.py +318 -0
- package/kits/coder/scripts/kit_status.py +292 -0
- package/kits/coder/scripts/skills_manager.py +243 -0
- package/kits/coder/scripts/verify_all.py +391 -0
- package/kits/coder/skills/accessibility-patterns/SKILL.md +372 -0
- package/kits/coder/skills/accessibility-patterns/scripts/a11y_checker.py +211 -0
- package/kits/coder/skills/ai-rag-patterns/SKILL.md +444 -0
- package/kits/coder/skills/api-patterns/SKILL.md +316 -0
- package/kits/coder/skills/api-patterns/assets/.gitkeep +1 -0
- package/kits/coder/skills/api-patterns/references/deep-dive.md +21 -0
- package/kits/coder/skills/api-patterns/scripts/api_validator.py +253 -0
- package/kits/coder/skills/api-patterns/scripts/validate.py +56 -0
- package/kits/coder/skills/auth-patterns/SKILL.md +267 -0
- package/kits/coder/skills/aws-patterns/SKILL.md +576 -0
- package/kits/coder/skills/brainstorming/SKILL.md +370 -0
- package/kits/coder/skills/brainstorming/assets/.gitkeep +1 -0
- package/kits/coder/skills/brainstorming/references/deep-dive.md +21 -0
- package/kits/coder/skills/brainstorming/scripts/validate.py +56 -0
- package/kits/coder/skills/clean-code/SKILL.md +240 -0
- package/kits/coder/skills/clean-code/assets/.gitkeep +1 -0
- package/kits/coder/skills/clean-code/references/deep-dive.md +21 -0
- package/kits/coder/skills/clean-code/scripts/lint_runner.py +186 -0
- package/kits/coder/skills/clean-code/scripts/validate.py +56 -0
- package/kits/coder/skills/database-design/SKILL.md +255 -0
- package/kits/coder/skills/database-design/assets/.gitkeep +1 -0
- package/kits/coder/skills/database-design/references/deep-dive.md +21 -0
- package/kits/coder/skills/database-design/scripts/schema_validator.py +272 -0
- package/kits/coder/skills/database-design/scripts/validate.py +56 -0
- package/kits/coder/skills/docker-patterns/SKILL.md +240 -0
- package/kits/coder/skills/documentation-templates/SKILL.md +441 -0
- package/kits/coder/skills/e2e-testing/SKILL.md +457 -0
- package/kits/coder/skills/flutter-patterns/SKILL.md +330 -0
- package/kits/coder/skills/frontend-design/SKILL.md +127 -0
- package/kits/coder/skills/github-actions/SKILL.md +349 -0
- package/kits/coder/skills/gitlab-ci-patterns/SKILL.md +466 -0
- package/kits/coder/skills/graphql-patterns/SKILL.md +558 -0
- package/kits/coder/skills/i18n-localization/SKILL.md +345 -0
- package/kits/coder/skills/i18n-localization/scripts/i18n_checker.py +267 -0
- package/kits/coder/skills/kubernetes-patterns/SKILL.md +357 -0
- package/kits/coder/skills/mermaid-diagrams/SKILL.md +351 -0
- package/kits/coder/skills/mobile-design/SKILL.md +305 -0
- package/kits/coder/skills/monitoring-observability/SKILL.md +458 -0
- package/kits/coder/skills/multi-tenancy/SKILL.md +317 -0
- package/kits/coder/skills/multi-tenancy/assets/.gitkeep +1 -0
- package/kits/coder/skills/multi-tenancy/references/deep-dive.md +21 -0
- package/kits/coder/skills/multi-tenancy/scripts/validate.py +56 -0
- package/kits/coder/skills/nodejs-best-practices/SKILL.md +220 -0
- package/kits/coder/skills/performance-profiling/SKILL.md +333 -0
- package/kits/coder/skills/performance-profiling/assets/.gitkeep +1 -0
- package/kits/coder/skills/performance-profiling/references/deep-dive.md +21 -0
- package/kits/coder/skills/performance-profiling/scripts/validate.py +56 -0
- package/kits/coder/skills/plan-writing/SKILL.md +360 -0
- package/kits/coder/skills/plan-writing/assets/.gitkeep +1 -0
- package/kits/coder/skills/plan-writing/references/deep-dive.md +21 -0
- package/kits/coder/skills/plan-writing/scripts/validate.py +56 -0
- package/kits/coder/skills/postgres-patterns/SKILL.md +361 -0
- package/kits/coder/skills/prompt-engineering/SKILL.md +277 -0
- package/kits/coder/skills/queue-patterns/SKILL.md +359 -0
- package/kits/coder/skills/queue-patterns/assets/.gitkeep +1 -0
- package/kits/coder/skills/queue-patterns/references/deep-dive.md +21 -0
- package/kits/coder/skills/queue-patterns/scripts/validate.py +56 -0
- package/kits/coder/skills/react-native-patterns/SKILL.md +393 -0
- package/kits/coder/skills/react-patterns/SKILL.md +319 -0
- package/kits/coder/skills/realtime-patterns/SKILL.md +506 -0
- package/kits/coder/skills/realtime-patterns/assets/.gitkeep +1 -0
- package/kits/coder/skills/realtime-patterns/references/deep-dive.md +21 -0
- package/kits/coder/skills/realtime-patterns/scripts/validate.py +56 -0
- package/kits/coder/skills/redis-patterns/SKILL.md +484 -0
- package/kits/coder/skills/security-fundamentals/SKILL.md +363 -0
- package/kits/coder/skills/security-fundamentals/assets/.gitkeep +1 -0
- package/kits/coder/skills/security-fundamentals/references/deep-dive.md +21 -0
- package/kits/coder/skills/security-fundamentals/scripts/security_scan.py +326 -0
- package/kits/coder/skills/security-fundamentals/scripts/validate.py +56 -0
- package/kits/coder/skills/seo-patterns/SKILL.md +262 -0
- package/kits/coder/skills/seo-patterns/scripts/seo_checker.py +211 -0
- package/kits/coder/skills/systematic-debugging/SKILL.md +478 -0
- package/kits/coder/skills/systematic-debugging/assets/.gitkeep +1 -0
- package/kits/coder/skills/systematic-debugging/references/deep-dive.md +21 -0
- package/kits/coder/skills/systematic-debugging/scripts/validate.py +56 -0
- package/kits/coder/skills/tailwind-patterns/SKILL.md +395 -0
- package/kits/coder/skills/terraform-patterns/SKILL.md +470 -0
- package/kits/coder/skills/testing-patterns/SKILL.md +285 -0
- package/kits/coder/skills/testing-patterns/assets/.gitkeep +1 -0
- package/kits/coder/skills/testing-patterns/references/deep-dive.md +21 -0
- package/kits/coder/skills/testing-patterns/scripts/test_runner.py +219 -0
- package/kits/coder/skills/testing-patterns/scripts/validate.py +56 -0
- package/kits/coder/skills/typescript-patterns/SKILL.md +417 -0
- package/kits/coder/skills/ui-ux-pro-max/SKILL.md +364 -0
- package/kits/coder/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/kits/coder/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/kits/coder/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/kits/coder/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/kits/coder/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/kits/coder/skills/ui-ux-pro-max/data/prompts.csv +24 -0
- package/kits/coder/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/kits/coder/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/kits/coder/skills/ui-ux-pro-max/data/styles.csv +59 -0
- package/kits/coder/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/kits/coder/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/kits/coder/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/kits/coder/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/kits/coder/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc +0 -0
- package/kits/coder/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-314.pyc +0 -0
- package/kits/coder/skills/ui-ux-pro-max/scripts/core.py +257 -0
- package/kits/coder/skills/ui-ux-pro-max/scripts/design_system.py +488 -0
- package/kits/coder/skills/ui-ux-pro-max/scripts/search.py +76 -0
- package/kits/coder/workflows/.gitkeep +20 -0
- package/kits/coder/workflows/create.md +152 -0
- package/kits/coder/workflows/debug.md +223 -0
- package/kits/coder/workflows/deploy.md +283 -0
- package/kits/coder/workflows/orchestrate.md +243 -0
- package/kits/coder/workflows/plan.md +134 -0
- package/kits/coder/workflows/test.md +237 -0
- package/kits/coder/workflows/ui-ux-pro-max.md +109 -0
- package/package.json +49 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: i18n-localization
|
|
3
|
+
description: Internationalization and localization patterns. Use when implementing multi-language support, translation workflows, locale handling, RTL support, or currency/date formatting.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# i18n & Localization Patterns
|
|
8
|
+
|
|
9
|
+
> Make software work beautifully in every language and culture.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Core Principles
|
|
14
|
+
|
|
15
|
+
1. **Externalize all strings** - No hardcoded text in components
|
|
16
|
+
2. **Use ICU Message Format** - Handle plurals, gender, and complex formatting
|
|
17
|
+
3. **Design for translation** - Leave room for text expansion (30-50%)
|
|
18
|
+
4. **Test with pseudo-localization** - Catch issues early
|
|
19
|
+
5. **Locale is more than language** - Includes date, number, currency formats
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## š§ Framework Selection
|
|
24
|
+
|
|
25
|
+
| Framework | Platform | Best For |
|
|
26
|
+
| --------------------- | ------------ | --------------------------- |
|
|
27
|
+
| **next-intl** | Next.js | App Router, RSC support |
|
|
28
|
+
| **react-intl** | React | Established, ICU format |
|
|
29
|
+
| **i18next** | Universal | Flexible, plugin ecosystem |
|
|
30
|
+
| **vue-i18n** | Vue | Vue-native, composition API |
|
|
31
|
+
| **expo-localization** | React Native | Mobile apps with Expo |
|
|
32
|
+
|
|
33
|
+
### Decision Tree
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Next.js App Router?
|
|
37
|
+
āāā Yes ā next-intl (best RSC support)
|
|
38
|
+
āāā No ā React SPA?
|
|
39
|
+
āāā Yes ā react-intl or i18next
|
|
40
|
+
āāā No ā Vue?
|
|
41
|
+
āāā Yes ā vue-i18n
|
|
42
|
+
āāā No ā i18next (universal)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## š Project Structure
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
locales/
|
|
51
|
+
āāā en/
|
|
52
|
+
ā āāā common.json # Shared strings
|
|
53
|
+
ā āāā auth.json # Auth-related
|
|
54
|
+
ā āāā dashboard.json # Feature-specific
|
|
55
|
+
ā āāā errors.json # Error messages
|
|
56
|
+
āāā vi/
|
|
57
|
+
ā āāā common.json
|
|
58
|
+
ā āāā auth.json
|
|
59
|
+
ā āāā ...
|
|
60
|
+
āāā ja/
|
|
61
|
+
āāā ...
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Namespace Strategy
|
|
65
|
+
|
|
66
|
+
| Namespace | Contents |
|
|
67
|
+
| ----------- | --------------------------- |
|
|
68
|
+
| `common` | Shared: buttons, labels |
|
|
69
|
+
| `auth` | Login, register, password |
|
|
70
|
+
| `errors` | Error messages, validations |
|
|
71
|
+
| `[feature]` | Feature-specific strings |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## š Key Naming Convention
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
// ā
GOOD: Descriptive, hierarchical keys
|
|
79
|
+
{
|
|
80
|
+
"auth": {
|
|
81
|
+
"login": {
|
|
82
|
+
"title": "Sign In",
|
|
83
|
+
"button": "Sign In",
|
|
84
|
+
"forgotPassword": "Forgot password?"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ā BAD: Flat, unclear keys
|
|
90
|
+
{
|
|
91
|
+
"login_title": "Sign In",
|
|
92
|
+
"btn1": "Sign In"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Naming Rules
|
|
97
|
+
|
|
98
|
+
| Rule | Example |
|
|
99
|
+
| --------------------- | ---------------------------- |
|
|
100
|
+
| **camelCase keys** | `forgotPassword` |
|
|
101
|
+
| **Nested by feature** | `auth.login.title` |
|
|
102
|
+
| **Semantic naming** | `submitButton` not `button1` |
|
|
103
|
+
| **No hardcoded text** | Even for "OK" or "Cancel" |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## š ICU Message Format
|
|
108
|
+
|
|
109
|
+
### Plurals
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"itemCount": "{count, plural, =0 {No items} one {# item} other {# items}}"
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Gender/Select
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"greeting": "{gender, select, male {Mr.} female {Ms.} other {}} {name}"
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Date & Number
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"lastUpdated": "Updated {date, date, medium}",
|
|
130
|
+
"price": "Price: {amount, number, ::currency/USD}"
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Rich Text (react-intl)
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
<FormattedMessage
|
|
138
|
+
id="terms"
|
|
139
|
+
defaultMessage="By signing up, you agree to our <link>Terms of Service</link>"
|
|
140
|
+
values={{
|
|
141
|
+
link: (chunks) => <Link href="/terms">{chunks}</Link>,
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## š Translation Workflow
|
|
149
|
+
|
|
150
|
+
### Development Flow
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
1. Add key to en/[namespace].json
|
|
154
|
+
2. Use key in component
|
|
155
|
+
3. Export for translators
|
|
156
|
+
4. Import translations
|
|
157
|
+
5. Review in context
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Translation Management
|
|
161
|
+
|
|
162
|
+
| Tool | Pricing | Best For |
|
|
163
|
+
| ------------ | ----------- | ---------------------- |
|
|
164
|
+
| **Crowdin** | Free tier | Open source, community |
|
|
165
|
+
| **Lokalise** | Paid | Professional teams |
|
|
166
|
+
| **Phrase** | Paid | Enterprise |
|
|
167
|
+
| **Tolgee** | Open source | Self-hosted |
|
|
168
|
+
|
|
169
|
+
### Pseudo-localization
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// Test without real translations
|
|
173
|
+
// "Hello" ā "[Ħëľľö]" (longer + special chars)
|
|
174
|
+
|
|
175
|
+
const pseudoLocalize = (str: string) => {
|
|
176
|
+
const map: Record<string, string> = {
|
|
177
|
+
a: "Ƥ",
|
|
178
|
+
e: "Ć«",
|
|
179
|
+
i: "ĆÆ",
|
|
180
|
+
o: "ƶ",
|
|
181
|
+
u: "ü",
|
|
182
|
+
A: "Ć",
|
|
183
|
+
E: "Ć",
|
|
184
|
+
I: "Ć",
|
|
185
|
+
O: "Ć",
|
|
186
|
+
U: "Ć",
|
|
187
|
+
};
|
|
188
|
+
const converted = str.replace(/[aeiouAEIOU]/g, (c) => map[c] || c);
|
|
189
|
+
return `[${converted}]`;
|
|
190
|
+
};
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## š± RTL Support
|
|
196
|
+
|
|
197
|
+
### CSS Approach
|
|
198
|
+
|
|
199
|
+
```css
|
|
200
|
+
/* Use logical properties */
|
|
201
|
+
.container {
|
|
202
|
+
/* ā Physical */
|
|
203
|
+
margin-left: 16px;
|
|
204
|
+
padding-right: 8px;
|
|
205
|
+
|
|
206
|
+
/* ā
Logical (RTL-aware) */
|
|
207
|
+
margin-inline-start: 16px;
|
|
208
|
+
padding-inline-end: 8px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Direction-aware flex */
|
|
212
|
+
.row {
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-direction: row; /* Auto-reverses in RTL */
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Logical Properties Mapping
|
|
219
|
+
|
|
220
|
+
| Physical | Logical |
|
|
221
|
+
| ------------------ | --------------------- |
|
|
222
|
+
| `left` | `inline-start` |
|
|
223
|
+
| `right` | `inline-end` |
|
|
224
|
+
| `top` | `block-start` |
|
|
225
|
+
| `bottom` | `block-end` |
|
|
226
|
+
| `margin-left` | `margin-inline-start` |
|
|
227
|
+
| `padding-right` | `padding-inline-end` |
|
|
228
|
+
| `text-align: left` | `text-align: start` |
|
|
229
|
+
|
|
230
|
+
### HTML Direction
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
<html dir={isRTL ? 'rtl' : 'ltr'} lang={locale}>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## š¢ Formatting
|
|
239
|
+
|
|
240
|
+
### Dates
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// Use Intl.DateTimeFormat
|
|
244
|
+
const formatDate = (date: Date, locale: string) => {
|
|
245
|
+
return new Intl.DateTimeFormat(locale, {
|
|
246
|
+
year: "numeric",
|
|
247
|
+
month: "long",
|
|
248
|
+
day: "numeric",
|
|
249
|
+
}).format(date);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Results:
|
|
253
|
+
// en-US: "January 15, 2025"
|
|
254
|
+
// vi-VN: "15 thƔng 1, 2025"
|
|
255
|
+
// ja-JP: "2025幓1ę15ę„"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Numbers & Currency
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const formatCurrency = (amount: number, locale: string, currency: string) => {
|
|
262
|
+
return new Intl.NumberFormat(locale, {
|
|
263
|
+
style: "currency",
|
|
264
|
+
currency,
|
|
265
|
+
}).format(amount);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Results for 1234.56:
|
|
269
|
+
// en-US, USD: "$1,234.56"
|
|
270
|
+
// vi-VN, VND: "1.234,56 ā«"
|
|
271
|
+
// ja-JP, JPY: "„1,235" (no decimals)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Relative Time
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
const formatRelative = (date: Date, locale: string) => {
|
|
278
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
279
|
+
const diff = (date.getTime() - Date.now()) / 1000;
|
|
280
|
+
|
|
281
|
+
if (Math.abs(diff) < 60) return rtf.format(Math.round(diff), "second");
|
|
282
|
+
if (Math.abs(diff) < 3600) return rtf.format(Math.round(diff / 60), "minute");
|
|
283
|
+
// ... etc
|
|
284
|
+
};
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## ā
Implementation Checklist
|
|
290
|
+
|
|
291
|
+
### Setup
|
|
292
|
+
|
|
293
|
+
- [ ] i18n library installed and configured
|
|
294
|
+
- [ ] Default locale defined
|
|
295
|
+
- [ ] Locale detection (browser/user preference)
|
|
296
|
+
- [ ] Fallback locale strategy
|
|
297
|
+
|
|
298
|
+
### Content
|
|
299
|
+
|
|
300
|
+
- [ ] All user-facing strings externalized
|
|
301
|
+
- [ ] ICU format for plurals/formatting
|
|
302
|
+
- [ ] No string concatenation for sentences
|
|
303
|
+
- [ ] Translator context/comments added
|
|
304
|
+
|
|
305
|
+
### Styling
|
|
306
|
+
|
|
307
|
+
- [ ] CSS uses logical properties
|
|
308
|
+
- [ ] RTL tested with real RTL content
|
|
309
|
+
- [ ] Text expansion tested (30-50% longer)
|
|
310
|
+
- [ ] Fonts support all target languages
|
|
311
|
+
|
|
312
|
+
### Testing
|
|
313
|
+
|
|
314
|
+
- [ ] Pseudo-localization tested
|
|
315
|
+
- [ ] All locales render correctly
|
|
316
|
+
- [ ] Date/number formats verified
|
|
317
|
+
- [ ] RTL layout tested
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## ā Anti-Patterns
|
|
322
|
+
|
|
323
|
+
| ā Don't | ā
Do |
|
|
324
|
+
| --------------------------------- | ------------------------------------------ |
|
|
325
|
+
| Concatenate strings for sentences | Use ICU message format |
|
|
326
|
+
| Hardcode "OK", "Cancel", etc. | Externalize ALL strings |
|
|
327
|
+
| Use physical CSS properties | Use logical properties for RTL |
|
|
328
|
+
| Assume text length stays same | Design for 30-50% expansion |
|
|
329
|
+
| Store locale in localStorage only | Support URL-based locale switching |
|
|
330
|
+
| Use images with embedded text | Separate text layer or generate per-locale |
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## š Related Skills
|
|
335
|
+
|
|
336
|
+
| Need | Skill |
|
|
337
|
+
| -------------- | ------------------------ |
|
|
338
|
+
| React patterns | `react-patterns` |
|
|
339
|
+
| API design | `api-patterns` |
|
|
340
|
+
| Accessibility | `accessibility-patterns` |
|
|
341
|
+
| Testing | `testing-patterns` |
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
> **Remember:** Localization is not just translation. It's making users feel the product was made for them, in their language, with their cultural expectations.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
i18n Checker - Internationalization audit for AGT-Kit
|
|
4
|
+
======================================================
|
|
5
|
+
|
|
6
|
+
Detects hardcoded strings and checks translation completeness.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 .agent/skills/i18n-localization/scripts/i18n_checker.py <project_path>
|
|
10
|
+
|
|
11
|
+
Checks:
|
|
12
|
+
- Hardcoded strings in JSX/Vue/Python
|
|
13
|
+
- Locale file completeness (missing keys)
|
|
14
|
+
- i18n library usage patterns
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
import re
|
|
19
|
+
import json
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from typing import Dict, List, Set, Any
|
|
23
|
+
|
|
24
|
+
# Fix console encoding
|
|
25
|
+
try:
|
|
26
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
27
|
+
except:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
SKIP_DIRS = {'node_modules', '.git', 'dist', 'build', '__pycache__', '.next', 'venv'}
|
|
31
|
+
|
|
32
|
+
# Patterns indicating hardcoded strings
|
|
33
|
+
HARDCODED_PATTERNS = {
|
|
34
|
+
'jsx': [
|
|
35
|
+
r'>\s*[A-Z][a-zA-Z\s]{3,30}\s*</', # Text in JSX
|
|
36
|
+
r'(title|placeholder|label|alt)="[A-Z][a-zA-Z\s]{2,}"', # Attributes
|
|
37
|
+
],
|
|
38
|
+
'vue': [
|
|
39
|
+
r'>\s*[A-Z][a-zA-Z\s]{3,30}\s*</',
|
|
40
|
+
r'(placeholder|label|title)="[A-Z][a-zA-Z\s]{2,}"',
|
|
41
|
+
],
|
|
42
|
+
'python': [
|
|
43
|
+
r'(print|raise\s+\w+)\s*\(\s*["\'][A-Z][^"\']{5,}["\']',
|
|
44
|
+
r'flash\s*\(\s*["\'][A-Z][^"\']{5,}["\']',
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Patterns indicating proper i18n usage
|
|
49
|
+
I18N_PATTERNS = [
|
|
50
|
+
r't\(["\']', # react-i18next
|
|
51
|
+
r'useTranslation', # React hook
|
|
52
|
+
r'\$t\(', # Vue i18n
|
|
53
|
+
r'_\(["\']', # Python gettext
|
|
54
|
+
r'gettext\(', # Python
|
|
55
|
+
r'useTranslations', # next-intl
|
|
56
|
+
r'FormattedMessage', # react-intl
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def find_locale_files(project_path: Path) -> List[Path]:
|
|
61
|
+
"""Find translation/locale files."""
|
|
62
|
+
patterns = [
|
|
63
|
+
'**/locales/**/*.json',
|
|
64
|
+
'**/translations/**/*.json',
|
|
65
|
+
'**/lang/**/*.json',
|
|
66
|
+
'**/i18n/**/*.json',
|
|
67
|
+
'**/messages/*.json',
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
files = []
|
|
71
|
+
for pattern in patterns:
|
|
72
|
+
for f in project_path.glob(pattern):
|
|
73
|
+
if not any(skip in f.parts for skip in SKIP_DIRS):
|
|
74
|
+
files.append(f)
|
|
75
|
+
|
|
76
|
+
return files
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def flatten_keys(d: dict, prefix: str = '') -> Set[str]:
|
|
80
|
+
"""Flatten nested dict keys."""
|
|
81
|
+
keys = set()
|
|
82
|
+
for k, v in d.items():
|
|
83
|
+
new_key = f"{prefix}.{k}" if prefix else k
|
|
84
|
+
if isinstance(v, dict):
|
|
85
|
+
keys.update(flatten_keys(v, new_key))
|
|
86
|
+
else:
|
|
87
|
+
keys.add(new_key)
|
|
88
|
+
return keys
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def check_locale_completeness(locale_files: List[Path]) -> Dict[str, Any]:
|
|
92
|
+
"""Check if all locales have the same keys."""
|
|
93
|
+
issues = []
|
|
94
|
+
passed = []
|
|
95
|
+
|
|
96
|
+
if not locale_files:
|
|
97
|
+
return {'passed': ['No locale files found (not required)'], 'issues': []}
|
|
98
|
+
|
|
99
|
+
# Group by language folder
|
|
100
|
+
locales: Dict[str, Dict[str, Set[str]]] = {}
|
|
101
|
+
|
|
102
|
+
for f in locale_files:
|
|
103
|
+
if f.suffix == '.json':
|
|
104
|
+
try:
|
|
105
|
+
lang = f.parent.name
|
|
106
|
+
content = json.loads(f.read_text(encoding='utf-8'))
|
|
107
|
+
if lang not in locales:
|
|
108
|
+
locales[lang] = {}
|
|
109
|
+
locales[lang][f.stem] = flatten_keys(content)
|
|
110
|
+
except:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
if len(locales) < 2:
|
|
114
|
+
passed.append(f"Found {len(locale_files)} locale file(s)")
|
|
115
|
+
return {'passed': passed, 'issues': issues}
|
|
116
|
+
|
|
117
|
+
passed.append(f"Found {len(locales)} languages: {', '.join(locales.keys())}")
|
|
118
|
+
|
|
119
|
+
# Compare keys across locales
|
|
120
|
+
all_langs = list(locales.keys())
|
|
121
|
+
base_lang = all_langs[0]
|
|
122
|
+
|
|
123
|
+
for namespace in locales.get(base_lang, {}):
|
|
124
|
+
base_keys = locales[base_lang].get(namespace, set())
|
|
125
|
+
|
|
126
|
+
for lang in all_langs[1:]:
|
|
127
|
+
other_keys = locales.get(lang, {}).get(namespace, set())
|
|
128
|
+
|
|
129
|
+
missing = base_keys - other_keys
|
|
130
|
+
if missing:
|
|
131
|
+
issues.append(f"{lang}/{namespace}: Missing {len(missing)} keys")
|
|
132
|
+
|
|
133
|
+
extra = other_keys - base_keys
|
|
134
|
+
if extra and len(extra) > 3:
|
|
135
|
+
issues.append(f"{lang}/{namespace}: {len(extra)} extra keys")
|
|
136
|
+
|
|
137
|
+
if not issues:
|
|
138
|
+
passed.append("All locales have matching keys")
|
|
139
|
+
|
|
140
|
+
return {'passed': passed, 'issues': issues}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def check_hardcoded_strings(project_path: Path) -> Dict[str, Any]:
|
|
144
|
+
"""Check for hardcoded strings in code files."""
|
|
145
|
+
issues = []
|
|
146
|
+
passed = []
|
|
147
|
+
|
|
148
|
+
extensions = {
|
|
149
|
+
'.tsx': 'jsx', '.jsx': 'jsx', '.ts': 'jsx', '.js': 'jsx',
|
|
150
|
+
'.vue': 'vue',
|
|
151
|
+
'.py': 'python'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
code_files = []
|
|
155
|
+
for ext in extensions:
|
|
156
|
+
for f in project_path.rglob(f"*{ext}"):
|
|
157
|
+
if not any(skip in f.parts for skip in SKIP_DIRS):
|
|
158
|
+
if not any(x in f.name for x in ['test', 'spec', 'config']):
|
|
159
|
+
code_files.append(f)
|
|
160
|
+
|
|
161
|
+
if not code_files:
|
|
162
|
+
return {'passed': ['No code files found'], 'issues': []}
|
|
163
|
+
|
|
164
|
+
files_with_i18n = 0
|
|
165
|
+
files_with_hardcoded = 0
|
|
166
|
+
examples = []
|
|
167
|
+
|
|
168
|
+
for file_path in code_files[:50]: # Limit
|
|
169
|
+
try:
|
|
170
|
+
content = file_path.read_text(encoding='utf-8', errors='ignore')
|
|
171
|
+
ext = file_path.suffix
|
|
172
|
+
file_type = extensions.get(ext, 'jsx')
|
|
173
|
+
|
|
174
|
+
# Check for i18n usage
|
|
175
|
+
has_i18n = any(re.search(p, content) for p in I18N_PATTERNS)
|
|
176
|
+
if has_i18n:
|
|
177
|
+
files_with_i18n += 1
|
|
178
|
+
|
|
179
|
+
# Check for hardcoded strings
|
|
180
|
+
patterns = HARDCODED_PATTERNS.get(file_type, [])
|
|
181
|
+
|
|
182
|
+
for pattern in patterns:
|
|
183
|
+
matches = re.findall(pattern, content)
|
|
184
|
+
if matches and not has_i18n:
|
|
185
|
+
files_with_hardcoded += 1
|
|
186
|
+
if len(examples) < 5:
|
|
187
|
+
examples.append(f"{file_path.name}: {str(matches[0])[:30]}...")
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
except:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
passed.append(f"Analyzed {len(code_files)} code files")
|
|
194
|
+
|
|
195
|
+
if files_with_i18n > 0:
|
|
196
|
+
passed.append(f"{files_with_i18n} files use i18n patterns")
|
|
197
|
+
|
|
198
|
+
if files_with_hardcoded > 0:
|
|
199
|
+
issues.append(f"{files_with_hardcoded} files may have hardcoded strings")
|
|
200
|
+
for ex in examples[:3]:
|
|
201
|
+
issues.append(f" ā {ex}")
|
|
202
|
+
else:
|
|
203
|
+
passed.append("No obvious hardcoded strings detected")
|
|
204
|
+
|
|
205
|
+
return {'passed': passed, 'issues': issues}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main():
|
|
209
|
+
project_path = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
|
|
210
|
+
|
|
211
|
+
print(f"\n{'='*60}")
|
|
212
|
+
print(f"[AGT-KIT i18n CHECKER] Internationalization Audit")
|
|
213
|
+
print(f"{'='*60}")
|
|
214
|
+
print(f"Project: {project_path}")
|
|
215
|
+
print(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
216
|
+
print("-"*60)
|
|
217
|
+
|
|
218
|
+
# Check locale files
|
|
219
|
+
locale_files = find_locale_files(project_path)
|
|
220
|
+
locale_result = check_locale_completeness(locale_files)
|
|
221
|
+
|
|
222
|
+
# Check hardcoded strings
|
|
223
|
+
code_result = check_hardcoded_strings(project_path)
|
|
224
|
+
|
|
225
|
+
# Print results
|
|
226
|
+
print("\nš LOCALE FILES")
|
|
227
|
+
print("-"*40)
|
|
228
|
+
for item in locale_result['passed']:
|
|
229
|
+
print(f" ā
{item}")
|
|
230
|
+
for item in locale_result['issues']:
|
|
231
|
+
print(f" ā ļø {item}")
|
|
232
|
+
|
|
233
|
+
print("\nš» CODE ANALYSIS")
|
|
234
|
+
print("-"*40)
|
|
235
|
+
for item in code_result['passed']:
|
|
236
|
+
print(f" ā
{item}")
|
|
237
|
+
for item in code_result['issues']:
|
|
238
|
+
print(f" ā ļø {item}")
|
|
239
|
+
|
|
240
|
+
# Summary
|
|
241
|
+
all_issues = locale_result['issues'] + code_result['issues']
|
|
242
|
+
critical = sum(1 for i in all_issues if 'hardcoded' in i.lower() or 'missing' in i.lower())
|
|
243
|
+
|
|
244
|
+
print("\n" + "="*60)
|
|
245
|
+
|
|
246
|
+
passed = critical == 0
|
|
247
|
+
|
|
248
|
+
if passed:
|
|
249
|
+
print("ā
i18n CHECK PASSED")
|
|
250
|
+
else:
|
|
251
|
+
print(f"ā ļø i18n CHECK: {critical} issues found")
|
|
252
|
+
|
|
253
|
+
output = {
|
|
254
|
+
"script": "i18n_checker",
|
|
255
|
+
"skill": "i18n-localization",
|
|
256
|
+
"project": str(project_path),
|
|
257
|
+
"locale_files": len(locale_files),
|
|
258
|
+
"issues": len(all_issues),
|
|
259
|
+
"passed": passed
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
print("\n" + json.dumps(output, indent=2))
|
|
263
|
+
sys.exit(0 if passed else 1)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
main()
|