Skip to content

Commit 755d497

Browse files
committed
Merge branch 'upstream-main' into feature/channel-affinity-request-header
2 parents 192a837 + 18282e6 commit 755d497

1,510 files changed

Lines changed: 217158 additions & 2855 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
name: classic-to-default-sync
3+
description: Inspect a given commit's web/classic changes and sync all features/fixes to web/default. Use when the user provides a commit ID and wants to audit whether web/default already has the same features as web/classic, port missing features, improve suboptimal implementations, fix bugs, and remove redundant code. Trigger phrases include: "/classic-to-default-sync <hash>", "classic-to-default-sync <hash>", "sync classic to default", "port from classic", "compare classic commit", "classic 和 default 对比", "把这次 classic 的修改同步到 default", "查看这次提交 classic 中的修改并同步", or any request supplying a commit hash together with classic/default comparison intent.
4+
---
5+
6+
# Classic-to-Default Sync
7+
8+
Given a **commit ID**, audit all `web/classic` changes and ensure `web/default` reaches feature parity with the best possible implementation.
9+
10+
## Input
11+
12+
The user must supply a `<commit-id>`.
13+
14+
## Workflow
15+
16+
### Step 1 — Extract classic diff
17+
18+
```bash
19+
git show <commit-id> -- web/classic
20+
```
21+
22+
Read every changed file in `web/classic`. Identify the **logical changes** (new features, UI/UX improvements, bug fixes, config tweaks, removed dead code, etc.) — not just line diffs.
23+
24+
### Step 2 — Map to default counterparts
25+
26+
For each logical change found in Step 1, locate the equivalent file(s) in `web/default/src/`. Use Glob/Grep/SemanticSearch as needed. Consider that:
27+
28+
- `web/classic` uses **React 18 + Vite + Semi Design**
29+
- `web/default` uses **React 19 + Rsbuild + Base UI + Tailwind CSS**
30+
- Component names, file paths, and API shapes may differ; match by **functionality**, not filename.
31+
32+
### Step 3 — Triage each change
33+
34+
Classify every logical change as one of:
35+
36+
| Status | Meaning |
37+
|--------|---------|
38+
| ✅ Already present & optimal | No action needed |
39+
| ⚠️ Present but suboptimal | Improve: logic, layout, style, or code quality |
40+
| ❌ Missing | Implement from scratch in default's stack |
41+
42+
### Step 4 — Implement
43+
44+
For each **⚠️** or **** item:
45+
46+
1. **Read the target file(s) in `web/default`** before editing (required by project conventions).
47+
2. Implement using `web/default` conventions:
48+
- React 19 patterns (hooks, Suspense, etc.)
49+
- Base UI primitives where applicable
50+
- Tailwind CSS for styling (no inline styles or Semi Design imports)
51+
- `useTranslation()` + `t('English key')` for all user-visible strings
52+
- TypeScript — explicit types, no `any`
53+
- No dead code, no redundant comments
54+
3. Follow **Rule 6** (pointer types for optional relay DTOs) if touching relay-related TS types.
55+
4. After editing, run `ReadLints` on changed files and fix any introduced lint errors.
56+
57+
### Step 5 — i18n
58+
59+
If any new user-visible strings were added, run the i18n sync:
60+
61+
```bash
62+
cd web/default && bun run i18n:sync
63+
```
64+
65+
Then add missing translations for all supported locales (en, zh, fr, ja, ru, vi) following the **i18n-translate** skill.
66+
67+
### Step 6 — Report
68+
69+
Summarise the work in a concise table:
70+
71+
| # | Change (from classic commit) | Status | Action taken |
72+
|---|------------------------------|--------|--------------|
73+
| 1 || ✅ / ⚠️ / ❌ | None / Improved / Implemented |
74+
75+
If every item is ✅ with no action needed, simply reply: **"已完成 — web/default 已具备此次提交的所有功能,且实现质量良好,无需修改。"**
76+
77+
## Quality bar
78+
79+
- No unused imports, variables, or components
80+
- No commented-out code left behind
81+
- Consistent naming with surrounding `web/default` code
82+
- All interactive elements accessible (keyboard nav, ARIA labels where Radix doesn't provide them automatically)
83+
- No regressions: existing behaviour in `web/default` must not break
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
---
2+
name: i18n-translate
3+
description: >-
4+
Complete and maintain frontend i18n translations for this project. Covers
5+
finding missing translation keys, detecting untranslated entries, and adding
6+
translations for all supported locales (en, zh, fr, ja, ru, vi). Use when the
7+
user asks to add translations, fix i18n, complete missing translations, or
8+
when new UI text needs to be internationalized.
9+
---
10+
11+
# Frontend i18n Translation Workflow
12+
13+
## Overview
14+
15+
- Locale files: `web/default/src/i18n/locales/{en,zh,fr,ja,ru,vi}.json`
16+
- Format: flat JSON under `"translation"` key, keys are English source strings
17+
- Base locale: `en.json` (most keys), fallback: `zh` (Chinese)
18+
- Sync script: `bun run i18n:sync` (from `web/default/`)
19+
- All `t()` calls must have corresponding keys in every locale file
20+
21+
## Workflow
22+
23+
### Step 1: Run sync and read report
24+
25+
```bash
26+
cd web/default && bun run i18n:sync
27+
```
28+
29+
Read `web/default/src/i18n/locales/_reports/_sync-report.json` to see per-locale status (missingCount, extrasCount, untranslatedCount).
30+
31+
### Step 2: Find missing keys (used in code but not in locale files)
32+
33+
Create and run `web/default/scripts/find-missing-keys.mjs`:
34+
35+
```javascript
36+
import fs from 'node:fs/promises'
37+
import path from 'node:path'
38+
39+
const LOCALES_DIR = path.resolve('src/i18n/locales')
40+
const SRC_DIR = path.resolve('src')
41+
42+
const en = JSON.parse(await fs.readFile(path.join(LOCALES_DIR, 'en.json'), 'utf8'))
43+
const enKeys = new Set(Object.keys(en.translation))
44+
45+
const tCallRegex = /\bt\(\s*['"`]([^'"`\n]+?)['"`]\s*[,)]/g
46+
const tCallMultilineRegex = /\bt\(\s*['"`]([^'"`]+?)['"`]\s*\)/g
47+
48+
async function walkDir(dir) {
49+
const files = []
50+
const entries = await fs.readdir(dir, { withFileTypes: true })
51+
for (const entry of entries) {
52+
const fullPath = path.join(dir, entry.name)
53+
if (entry.isDirectory()) {
54+
if (['node_modules', '.git', 'locales', '_reports', '_extras'].includes(entry.name)) continue
55+
files.push(...(await walkDir(fullPath)))
56+
} else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
57+
files.push(fullPath)
58+
}
59+
}
60+
return files
61+
}
62+
63+
const files = await walkDir(SRC_DIR)
64+
const missingKeys = new Map()
65+
66+
for (const file of files) {
67+
const content = await fs.readFile(file, 'utf8')
68+
const relPath = path.relative(SRC_DIR, file)
69+
for (const regex of [tCallRegex, tCallMultilineRegex]) {
70+
regex.lastIndex = 0
71+
let match
72+
while ((match = regex.exec(content)) !== null) {
73+
const key = match[1]
74+
if (key.startsWith('{{') || key.includes('${')) continue
75+
if (!enKeys.has(key)) {
76+
if (!missingKeys.has(key)) missingKeys.set(key, [])
77+
missingKeys.get(key).push(relPath)
78+
}
79+
}
80+
}
81+
}
82+
83+
if (missingKeys.size === 0) {
84+
console.log('All t() keys found in en.json!')
85+
} else {
86+
console.log(`Found ${missingKeys.size} missing keys:\n`)
87+
for (const [key, files] of [...missingKeys.entries()].sort(([a], [b]) => a.localeCompare(b))) {
88+
console.log(` "${key}"`)
89+
for (const f of [...new Set(files)]) console.log(` -> ${f}`)
90+
}
91+
}
92+
```
93+
94+
### Step 3: Find untranslated entries (value equals English)
95+
96+
Create and run `web/default/scripts/find-untranslated.mjs`:
97+
98+
```javascript
99+
import fs from 'node:fs/promises'
100+
import path from 'node:path'
101+
102+
const LOCALES_DIR = path.resolve('src/i18n/locales')
103+
const en = JSON.parse(await fs.readFile(path.join(LOCALES_DIR, 'en.json'), 'utf8'))
104+
const enTrans = en.translation
105+
106+
// Brand names, URLs, technical terms — skip these
107+
const skipPatterns = [
108+
/^https?:\/\//, /^smtp\./, /^socks5:/, /^name@/, /^noreply@/,
109+
/^org-/, /^price_/, /^whsec_/, /^edit_this$/, /^my-status$/,
110+
/^_copy$/, /^gpt-/, /^checkout\./, /^footer\./, /^\[?\{/,
111+
/^"default/, /^\/status\//, /^\/your\//, /^example\.com/,
112+
/^AZURE_/, /^AccessKey/, /^OAuth/, /^Client /, /^Webhook URL/,
113+
/^API URL$/, /^Well-Known/, /^Worker URL$/, /^Uptime Kuma/,
114+
/^New API/, /^Baidu V2$/, /^Zhipu V4$/, /^Quota:$/,
115+
]
116+
117+
const brandNames = new Set([
118+
'AIGC2D','Anthropic','API2GPT','Claude','Cloudflare','Cohere','DeepSeek',
119+
'Discord','DoubaoVideo','FastGPT','Gemini','GitHub','Jimeng','JustSong',
120+
'LingYiWanWu','LinuxDO','Midjourney','MidjourneyPlus','MiniMax','Mistral',
121+
'MokaAI','Moonshot','NewAPI','OhMyGPT','Ollama','OpenAI','OpenAIMax',
122+
'OpenRouter','Passkey','Perplexity','QuantumNous','Replicate','SiliconFlow',
123+
'Stripe','Submodel','SunoAPI','Telegram','Tencent','Vertex AI','VolcEngine',
124+
'WeChat','Xinference','Xunfei','AI Proxy','One API',
125+
])
126+
127+
const locales = ['fr', 'ja', 'ru', 'zh', 'vi']
128+
129+
for (const locale of locales) {
130+
const locFile = JSON.parse(await fs.readFile(path.join(LOCALES_DIR, `${locale}.json`), 'utf8'))
131+
const locTrans = locFile.translation
132+
const untranslated = {}
133+
134+
for (const [key, enVal] of Object.entries(enTrans)) {
135+
const locVal = locTrans[key]
136+
if (locVal === undefined || locVal !== enVal) continue
137+
if (brandNames.has(key)) continue
138+
if (skipPatterns.some(p => p.test(key))) continue
139+
if (typeof enVal === 'string' && enVal.length < 4) continue
140+
if (/[a-zA-Z]{3,}/.test(String(enVal))) untranslated[key] = enVal
141+
}
142+
143+
const count = Object.keys(untranslated).length
144+
if (count > 0) {
145+
console.log(`\n=== ${locale} (${count} untranslated) ===`)
146+
for (const [k, v] of Object.entries(untranslated))
147+
console.log(` ${JSON.stringify(k)}: ${JSON.stringify(v)}`)
148+
} else {
149+
console.log(`\n=== ${locale}: all translated ===`)
150+
}
151+
}
152+
```
153+
154+
### Step 4: Add translations
155+
156+
Create `web/default/scripts/add-missing-keys.mjs` with this structure:
157+
158+
```javascript
159+
import fs from 'node:fs/promises'
160+
import path from 'node:path'
161+
162+
const LOCALES_DIR = path.resolve('src/i18n/locales')
163+
164+
function stableStringify(obj) {
165+
return JSON.stringify(obj, null, 2) + '\n'
166+
}
167+
168+
const newKeys = {
169+
en: { /* "key": "English value" */ },
170+
zh: { /* "key": "中文翻译" */ },
171+
fr: { /* "key": "Traduction française" */ },
172+
ja: { /* "key": "日本語翻訳" */ },
173+
ru: { /* "key": "Русский перевод" */ },
174+
vi: { /* "key": "Bản dịch tiếng Việt" */ },
175+
}
176+
177+
async function main() {
178+
let totalAdded = 0
179+
180+
for (const [locale, trans] of Object.entries(newKeys)) {
181+
const filePath = path.join(LOCALES_DIR, `${locale}.json`)
182+
const json = JSON.parse(await fs.readFile(filePath, 'utf8'))
183+
184+
let count = 0
185+
for (const [key, value] of Object.entries(trans)) {
186+
if (!Object.prototype.hasOwnProperty.call(json.translation, key)) {
187+
json.translation[key] = value
188+
count++
189+
} else if (json.translation[key] !== value) {
190+
json.translation[key] = value
191+
count++
192+
}
193+
}
194+
195+
if (count > 0) {
196+
json.translation = Object.fromEntries(
197+
Object.entries(json.translation).sort(([a], [b]) => a.localeCompare(b))
198+
)
199+
await fs.writeFile(filePath, stableStringify(json), 'utf8')
200+
}
201+
202+
console.log(`${locale}: ${count} translations applied`)
203+
totalAdded += count
204+
}
205+
206+
console.log(`\nTotal: ${totalAdded} translations applied`)
207+
}
208+
209+
main().catch((err) => { console.error(err); process.exitCode = 1 })
210+
```
211+
212+
Populate the `newKeys` object with actual translations for each locale.
213+
214+
### Step 5: Verify and clean up
215+
216+
```bash
217+
cd web/default
218+
node scripts/add-missing-keys.mjs # apply translations
219+
node scripts/find-missing-keys.mjs # verify: should say "All t() keys found"
220+
bun run i18n:sync # normalize file order
221+
```
222+
223+
Delete temporary scripts after completion.
224+
225+
## Translation Guidelines
226+
227+
| Language | Code | Notes |
228+
|----------|------|-------|
229+
| English | en | Base locale, key = value |
230+
| Chinese | zh | Fallback locale, must be complete |
231+
| French | fr | Many English cognates are valid (e.g., "Configuration") |
232+
| Japanese | ja | Use katakana for technical loanwords |
233+
| Russian | ru | Use formal register |
234+
| Vietnamese | vi | Use standard Vietnamese |
235+
236+
**Keep as English (do not translate):**
237+
- Brand/product names (OpenAI, Claude, Gemini, etc.)
238+
- URLs and email placeholders
239+
- Technical identifiers (JSON keys, API paths, model names)
240+
- Code-like strings (gpt-3.5-turbo, price_xxx, etc.)
241+
242+
**Always translate:**
243+
- UI labels, button text, error messages, descriptions
244+
- Time units (hours, minutes, months, years)
245+
- Action words (Move, Show, Delete, etc.)
246+
247+
## Key Rules
248+
249+
1. All scripts run from `web/default/` directory
250+
2. Use `node scripts/xxx.mjs` (ESM format with top-level await)
251+
3. Sort keys alphabetically when writing locale files
252+
4. Always run `bun run i18n:sync` as the final step
253+
5. Delete temporary scripts after completion
254+
6. The `{{variable}}` placeholders in keys must be preserved in all translations

0 commit comments

Comments
 (0)