|
|
|
@@ -0,0 +1,307 @@ |
|
|
|
# Default Language Setting Implementation Plan |
|
|
|
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
|
|
|
|
|
|
|
**Goal:** Allow admin to configure a global default language in System Settings, which is forced for unauthenticated users and logged-in users without a personal language preference. |
|
|
|
|
|
|
|
**Architecture:** Backend adds a `DefaultLanguage` option to the existing flat-key option system (same pattern as `SystemName`, `ServerAddress`). The value flows to frontend via `/api/status`. Frontend applies it in two places: `PageLayout.jsx` (for unauthenticated users) and `UserContext` (for logged-in users without preference). |
|
|
|
|
|
|
|
**Tech Stack:** Go (backend), React + Semi UI (frontend), i18next (i18n) |
|
|
|
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-04-17-default-language-setting-design.md` |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
## File Map |
|
|
|
|
|
|
|
| File | Action | Responsibility | |
|
|
|
|------|--------|---------------| |
|
|
|
| `common/constants.go` | Modify | Declare `DefaultLanguage` variable | |
|
|
|
| `model/option.go` | Modify | Handle `DefaultLanguage` in option update switch | |
|
|
|
| `controller/misc.go` | Modify | Expose `default_language` in `/api/status` response | |
|
|
|
| `web/src/components/settings/SystemSetting.jsx` | Modify | Admin UI: language dropdown in system settings | |
|
|
|
| `web/src/components/layout/PageLayout.jsx` | Modify | Apply default language for unauthenticated users | |
|
|
|
| `web/src/context/User/index.jsx` | Modify | Fall back to default language when user has no preference | |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
### Task 1: Backend — Add DefaultLanguage variable and option handling |
|
|
|
|
|
|
|
**Files:** |
|
|
|
- Modify: `common/constants.go:17-18` (after `TopUpLink` declaration) |
|
|
|
- Modify: `model/option.go:457-458` (after `Logo` case in `updateOptionMap` switch) |
|
|
|
|
|
|
|
- [ ] **Step 1: Add `DefaultLanguage` variable to `common/constants.go`** |
|
|
|
|
|
|
|
In `common/constants.go`, after line 18 (`var TopUpLink = ""`), add: |
|
|
|
|
|
|
|
```go |
|
|
|
var DefaultLanguage = "" // admin-configured default language; empty = follow browser detection |
|
|
|
``` |
|
|
|
|
|
|
|
- [ ] **Step 2: Add case handler in `model/option.go`** |
|
|
|
|
|
|
|
In `model/option.go`, after the `case "Logo":` block (line 457-458), add: |
|
|
|
|
|
|
|
```go |
|
|
|
case "DefaultLanguage": |
|
|
|
common.DefaultLanguage = value |
|
|
|
``` |
|
|
|
|
|
|
|
- [ ] **Step 3: Verify the Go code compiles** |
|
|
|
|
|
|
|
Run: `go build ./...` |
|
|
|
Expected: no errors |
|
|
|
|
|
|
|
- [ ] **Step 4: Commit** |
|
|
|
|
|
|
|
```bash |
|
|
|
git add common/constants.go model/option.go |
|
|
|
git commit -m "feat(default-language): add DefaultLanguage variable and option handler" |
|
|
|
``` |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
### Task 2: Backend — Expose default_language in /api/status |
|
|
|
|
|
|
|
**Files:** |
|
|
|
- Modify: `controller/misc.go:89` (after `"default_use_auto_group"` line) |
|
|
|
|
|
|
|
- [ ] **Step 1: Add `default_language` field to GetStatus response** |
|
|
|
|
|
|
|
In `controller/misc.go`, inside `GetStatus()`, after the line containing `"default_use_auto_group": setting.DefaultUseAutoGroup,` (line 89), add: |
|
|
|
|
|
|
|
```go |
|
|
|
"default_language": common.DefaultLanguage, |
|
|
|
``` |
|
|
|
|
|
|
|
Place it right before the blank line at line 90, aligned with the surrounding entries. |
|
|
|
|
|
|
|
- [ ] **Step 2: Verify the Go code compiles** |
|
|
|
|
|
|
|
Run: `go build ./...` |
|
|
|
Expected: no errors |
|
|
|
|
|
|
|
- [ ] **Step 3: Commit** |
|
|
|
|
|
|
|
```bash |
|
|
|
git add controller/misc.go |
|
|
|
git commit -m "feat(default-language): expose default_language in /api/status response" |
|
|
|
``` |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
### Task 3: Frontend — Add DefaultLanguage dropdown in System Settings |
|
|
|
|
|
|
|
**Files:** |
|
|
|
- Modify: `web/src/components/settings/SystemSetting.jsx` |
|
|
|
|
|
|
|
- [ ] **Step 1: Add `DefaultLanguage` to `inputs` state** |
|
|
|
|
|
|
|
In `web/src/components/settings/SystemSetting.jsx`, in the `inputs` useState (around line 49-112), add `DefaultLanguage: ''` after `ServerAddress: ''` (line 102): |
|
|
|
|
|
|
|
```javascript |
|
|
|
ServerAddress: '', |
|
|
|
DefaultLanguage: '', |
|
|
|
``` |
|
|
|
|
|
|
|
- [ ] **Step 2: Add `submitDefaultLanguage` function** |
|
|
|
|
|
|
|
After `submitServerAddress` (line 315-318), add a new function: |
|
|
|
|
|
|
|
```javascript |
|
|
|
const submitDefaultLanguage = async () => { |
|
|
|
await updateOptions([{ key: 'DefaultLanguage', value: inputs.DefaultLanguage || '' }]); |
|
|
|
}; |
|
|
|
``` |
|
|
|
|
|
|
|
- [ ] **Step 3: Add language dropdown UI in the 通用设置 Card** |
|
|
|
|
|
|
|
In the `通用设置` `Form.Section` (line 715-733), inside the `<Row>` block, after the `ServerAddress` `<Col>` closing tag (line 728) and before the `</Row>` closing tag (line 729), add a new `<Col>`: |
|
|
|
|
|
|
|
```jsx |
|
|
|
<Col xs={24} sm={24} md={24} lg={12} xl={12}> |
|
|
|
<Form.Select |
|
|
|
field='DefaultLanguage' |
|
|
|
label={t('默认语言')} |
|
|
|
placeholder={t('未设置时跟随浏览器语言')} |
|
|
|
optionList={[ |
|
|
|
{ label: t('自动(跟随浏览器)'), value: '' }, |
|
|
|
{ label: '简体中文', value: 'zh-CN' }, |
|
|
|
{ label: '繁體中文', value: 'zh-TW' }, |
|
|
|
{ label: 'English', value: 'en' }, |
|
|
|
{ label: 'Français', value: 'fr' }, |
|
|
|
{ label: '日本語', value: 'ja' }, |
|
|
|
{ label: 'Русский', value: 'ru' }, |
|
|
|
{ label: 'Tiếng Việt', value: 'vi' }, |
|
|
|
]} |
|
|
|
extraText={t( |
|
|
|
'设置后,未登录用户和未设置语言偏好的已登录用户将强制使用此语言', |
|
|
|
)} |
|
|
|
/> |
|
|
|
</Col> |
|
|
|
``` |
|
|
|
|
|
|
|
Also change the `ServerAddress` Col from `md={24} lg={24} xl={24}` to `md={24} lg={12} xl={12}` so both fields sit side by side on large screens. |
|
|
|
|
|
|
|
- [ ] **Step 4: Add save button for DefaultLanguage** |
|
|
|
|
|
|
|
After the existing `submitServerAddress` button (line 730-732), add: |
|
|
|
|
|
|
|
```jsx |
|
|
|
<Button onClick={submitDefaultLanguage}> |
|
|
|
{t('保存默认语言')} |
|
|
|
</Button> |
|
|
|
``` |
|
|
|
|
|
|
|
- [ ] **Step 5: Verify frontend compiles** |
|
|
|
|
|
|
|
Run: `cd web && bun run build` |
|
|
|
Expected: build succeeds |
|
|
|
|
|
|
|
- [ ] **Step 6: Commit** |
|
|
|
|
|
|
|
```bash |
|
|
|
git add web/src/components/settings/SystemSetting.jsx |
|
|
|
git commit -m "feat(default-language): add language dropdown in system settings" |
|
|
|
``` |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
### Task 4: Frontend — Apply default language for unauthenticated users |
|
|
|
|
|
|
|
**Files:** |
|
|
|
- Modify: `web/src/components/layout/PageLayout.jsx:87-100` (loadStatus function) |
|
|
|
|
|
|
|
- [ ] **Step 1: Modify `loadStatus` to apply default language** |
|
|
|
|
|
|
|
In `web/src/components/layout/PageLayout.jsx`, in the `loadStatus` function (line 87-100), after `setStatusData(data)` (line 93) and before the `} else {` (line 94), add: |
|
|
|
|
|
|
|
```javascript |
|
|
|
// Apply admin-configured default language for unauthenticated users |
|
|
|
if (data.default_language && !localStorage.getItem('user')) { |
|
|
|
i18n.changeLanguage(data.default_language); |
|
|
|
} |
|
|
|
``` |
|
|
|
|
|
|
|
- [ ] **Step 2: Update the existing localStorage language fallback logic** |
|
|
|
|
|
|
|
In the same file, the existing `useEffect` (line 102-120) reads `localStorage.getItem('i18nextLng')` and calls `i18n.changeLanguage(savedLang)`. This logic should be kept as-is — it handles the case where `default_language` is not set (admin chose "auto"). No changes needed to this block. |
|
|
|
|
|
|
|
- [ ] **Step 3: Verify frontend compiles** |
|
|
|
|
|
|
|
Run: `cd web && bun run build` |
|
|
|
Expected: build succeeds |
|
|
|
|
|
|
|
- [ ] **Step 4: Commit** |
|
|
|
|
|
|
|
```bash |
|
|
|
git add web/src/components/layout/PageLayout.jsx |
|
|
|
git commit -m "feat(default-language): apply default language for unauthenticated users" |
|
|
|
``` |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
### Task 5: Frontend — Fall back to default language for logged-in users without preference |
|
|
|
|
|
|
|
**Files:** |
|
|
|
- Modify: `web/src/context/User/index.jsx:20-45` |
|
|
|
|
|
|
|
- [ ] **Step 1: Import StatusContext** |
|
|
|
|
|
|
|
At the top of `web/src/context/User/index.jsx`, add `StatusContext` import after the existing imports: |
|
|
|
|
|
|
|
```javascript |
|
|
|
import { StatusContext } from '../Status'; |
|
|
|
``` |
|
|
|
|
|
|
|
- [ ] **Step 2: Access StatusContext inside UserProvider** |
|
|
|
|
|
|
|
Inside `UserProvider` (line 29), before the `useEffect`, add: |
|
|
|
|
|
|
|
```javascript |
|
|
|
const [statusState] = React.useContext(StatusContext); |
|
|
|
``` |
|
|
|
|
|
|
|
- [ ] **Step 3: Update language sync logic** |
|
|
|
|
|
|
|
Replace the existing `useEffect` (lines 34-45) with: |
|
|
|
|
|
|
|
```javascript |
|
|
|
// Sync language preference when user data is loaded |
|
|
|
useEffect(() => { |
|
|
|
if (state.user?.setting) { |
|
|
|
try { |
|
|
|
const settings = JSON.parse(state.user.setting); |
|
|
|
if (settings.language && settings.language !== i18n.language) { |
|
|
|
i18n.changeLanguage(settings.language); |
|
|
|
} else if (!settings.language && statusState.status?.default_language) { |
|
|
|
// No personal preference — fall back to admin default |
|
|
|
i18n.changeLanguage(statusState.status.default_language); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
// Ignore parse errors |
|
|
|
} |
|
|
|
} |
|
|
|
}, [state.user?.setting, statusState.status?.default_language, i18n]); |
|
|
|
``` |
|
|
|
|
|
|
|
- [ ] **Step 4: Verify frontend compiles** |
|
|
|
|
|
|
|
Run: `cd web && bun run build` |
|
|
|
Expected: build succeeds |
|
|
|
|
|
|
|
- [ ] **Step 5: Commit** |
|
|
|
|
|
|
|
```bash |
|
|
|
git add web/src/context/User/index.jsx |
|
|
|
git commit -m "feat(default-language): fall back to default language for users without preference" |
|
|
|
``` |
|
|
|
|
|
|
|
--- |
|
|
|
|
|
|
|
### Task 6: Integration verification |
|
|
|
|
|
|
|
- [ ] **Step 1: Start full-stack dev server** |
|
|
|
|
|
|
|
Terminal 1: `cd web && bun run dev` |
|
|
|
Terminal 2: `go run main.go` |
|
|
|
|
|
|
|
- [ ] **Step 2: Test admin setting** |
|
|
|
|
|
|
|
1. Login as admin, navigate to Settings → System Settings (系统设置) |
|
|
|
2. Find the "默认语言" dropdown in the 通用设置 section |
|
|
|
3. Select "English" and click "保存默认语言" |
|
|
|
4. Verify success toast appears |
|
|
|
5. Refresh the page — the dropdown should still show "English" |
|
|
|
|
|
|
|
- [ ] **Step 3: Test unauthenticated user behavior** |
|
|
|
|
|
|
|
1. Open an incognito/private browser window |
|
|
|
2. Visit the site |
|
|
|
3. Expected: The page renders in English (not following browser language) |
|
|
|
4. The header language selector still shows English as active |
|
|
|
|
|
|
|
- [ ] **Step 4: Test logged-in user with personal preference** |
|
|
|
|
|
|
|
1. Login as a regular user |
|
|
|
2. Go to personal settings, set language to "Français" |
|
|
|
3. Refresh — page should be in French (personal preference overrides admin default) |
|
|
|
|
|
|
|
- [ ] **Step 5: Test logged-in user without personal preference** |
|
|
|
|
|
|
|
1. Login as a user who has never set a language preference |
|
|
|
2. Expected: Page renders in English (admin default), not browser language |
|
|
|
|
|
|
|
- [ ] **Step 6: Test "auto" mode** |
|
|
|
|
|
|
|
1. As admin, set "默认语言" back to "自动(跟随浏览器)" |
|
|
|
2. Save, then test in incognito — should follow browser language again |
|
|
|
|
|
|
|
- [ ] **Step 7: Verify /api/status returns the field** |
|
|
|
|
|
|
|
```bash |
|
|
|
curl -s http://localhost:3000/api/status | python -m json.tool | grep default_language |
|
|
|
``` |
|
|
|
|
|
|
|
Expected: `"default_language": "en"` (or the set value, or `""` if auto) |