chore: establish learning module baseline

This commit is contained in:
inman committed 2026-08-16 14:58:47 +08:00
commit 2d04197f3f
2409 files changed
+533050

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
# Translation Guide
## Adding a new language
1. Copy `locales/en-US.json` to `locales/<code>.json` (e.g. `ja-JP.json`)
2. Append an entry to the end of the `supportedLocales` array in `locales.ts` — do not reorder existing entries, as the first locale for each language prefix (e.g. `zh-CN` for `zh`) is used as the default when the browser sends a bare language code:
```ts
{ code: 'ja-JP', label: '日本語', shortLabel: 'JA' },
```
3. Translate all values in the new JSON file. Keys must remain identical.
## Interpolation
This project uses i18next with the default double-brace syntax: `{{variable}}`.
Example: `"Hi, {{name}}"` will render as `"Hi, Alice"` when called with `t('key', { name: 'Alice' })`.
Do NOT remove or rename interpolation variables — they are referenced in code.
## Keys with design intent
Not every key needs explanation, but the following have non-obvious UX context that affects how they should be translated.
| Key | Where it appears | Translation notes |
|-----|-----------------|-------------------|
| `home.greetingWithName` | Top-left of homepage, clickable pill that opens nickname editor | This is a **call-to-action** — the greeting doubles as an entry point for users to set their nickname. The translation must include `{{name}}` and read naturally with the default nickname (see `profile.defaultNickname`). Avoid generic greetings that hide the name (e.g. don't translate as just "Welcome"). |
| `profile.defaultNickname` | Pre-filled in the greeting and the nickname input field | Shown before the user sets a real name. Pick a warm, gender-neutral word that: (1) feels natural in the greeting, (2) clearly signals "this is a placeholder you should replace". Avoid cold terms like "User" or formal terms like "Student". Examples: EN "Learner", ZH "同学". |
| `profile.bioPlaceholder` | Textarea placeholder in the profile editor | The bio is fed to the AI teacher to personalize lessons. The placeholder should hint at this — tell users *why* filling it in helps. |
| `generation.textTruncated` / `generation.imageTruncated` | Toast warnings during PDF-based course generation | These are technical warnings shown briefly. Keep them short and factual. `textTruncated` has `{{n}}` (character count), `imageTruncated` has `{{total}}` and `{{max}}`. |
| `agentBar.readyToLearn` | Classroom page, above the agent role list | Conversational prompt to set the mood before class starts. Should feel inviting, not instructional. |
| `settings.agentsCollaboratingCount` | Settings panel, multi-agent mode description | Contains `{{count}}`. This is a status label, not a button — keep it descriptive. |
+19
View File
@@ -0,0 +1,19 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import { supportedLocales } from './locales';
import { defaultLocale } from './types';
i18n
.use(initReactI18next)
.use(resourcesToBackend((language: string) => import(`./locales/${language}.json`)))
.init({
lng: defaultLocale,
fallbackLng: defaultLocale,
supportedLngs: supportedLocales.map((l) => l.code),
interpolation: {
escapeValue: false,
},
});
export default i18n;
+13
View File
@@ -0,0 +1,13 @@
import i18n from './config';
export { type Locale, defaultLocale } from './types';
export { type LocaleEntry, supportedLocales } from './locales';
export type TranslationKey = string;
export function translate(locale: string, key: string): string {
return i18n.t(key, { lng: locale });
}
export function getClientTranslation(key: string): string {
return i18n.t(key);
}
+28
View File
@@ -0,0 +1,28 @@
export type LocaleEntry = {
code: string;
/** Native name shown in dropdown, e.g. '简体中文' */
label: string;
/** Short label shown on the toggle button, e.g. 'CN' */
shortLabel: string;
};
/**
* Supported locales registry.
*
* To add a new language:
* 1. Create `lib/i18n/locales/<code>.json` (copy an existing file as template)
* 2. Add an entry here
*/
export const supportedLocales = [
{ code: 'zh-CN', label: '简体中文', shortLabel: 'CN' },
{ code: 'zh-TW', label: '繁體中文', shortLabel: 'TW' },
{ code: 'en-US', label: 'English', shortLabel: 'EN' },
{ code: 'ja-JP', label: '日本語', shortLabel: 'JA' },
{ code: 'ru-RU', label: 'Русский', shortLabel: 'RU' },
{ code: 'ar-SA', label: 'العربية', shortLabel: 'AR' },
{ code: 'pt-BR', label: 'Português (Brasil)', shortLabel: 'BR' },
{ code: 'ko-KR', label: '한국어', shortLabel: 'KO' },
{ code: 'es-MX', label: 'Español (México)', shortLabel: 'ES' },
{ code: 'fr-FR', label: 'Français', shortLabel: 'FR' },
{ code: 'vi-VN', label: 'Tiếng Việt', shortLabel: 'VI' },
] as const satisfies readonly LocaleEntry[];
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+5
View File
@@ -0,0 +1,5 @@
import { supportedLocales } from './locales';
export type Locale = (typeof supportedLocales)[number]['code'];
export const defaultLocale: Locale = 'zh-CN';