diff --git a/.dockerignore b/.dockerignore index 08e0db2..b512aa6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -23,13 +23,30 @@ frontend/out frontend/.env* frontend/*.log -# Project +# Project data .antigravity .gemini tmp data mydata +notebook_data +surreal_data +surreal-data +surreal_single_data *.db *.log docker.env -.env \ No newline at end of file +.env +docker-compose* + +# Documentation & CI (not needed in image) +docs +.github + +# IDE and OS files +.vscode +.idea +*.swp +*.swo +*~ +.DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd293f..d6b2ecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.5.0] - 2026-01-15 + +### Added +- Internationalization (i18n) support with Chinese (Simplified and Traditional) translations (#371, closes #344, #349, #360) +- Frontend test infrastructure with Vitest (#371) +- Language toggle component for switching UI language (#371) +- Date localization using date-fns locales (#371) +- Error message translation system (#371) + +### Fixed +- Accessibility improvements: added missing `id`, `name`, and `autoComplete` attributes to form inputs (#371) +- Added `DialogDescription` to dialogs for Radix UI accessibility compliance (#371) +- Fixed "Collapsible is changing from uncontrolled to controlled" warning in SettingsForm (#371) +- Fixed lint command for Next.js 16 compatibility (`eslint` instead of `next lint`) + +### Changed +- Dockerfile optimizations: better layer caching, `--no-install-recommends` for smaller images (#371) +- Dockerfile.single refactored into 3 separate build stages for better caching (#371) + ## [1.4.0] - 2026-01-14 ### Added diff --git a/README.dev.md b/README.dev.md index 1c7d27e..eac28b0 100644 --- a/README.dev.md +++ b/README.dev.md @@ -244,6 +244,47 @@ uv sync cd frontend && npm install package-name ``` +### Adding a New Language (i18n) + +Open Notebook supports internationalization. To add a new language: + +1. **Create locale file**: Copy an existing locale as template + ```bash + cp frontend/src/lib/locales/en-US/index.ts frontend/src/lib/locales/pt-BR/index.ts + ``` + +2. **Translate all strings** in the new file. The structure includes: + - `common`: Shared UI elements (buttons, labels) + - `notebooks`, `sources`, `notes`: Feature-specific strings + - `chat`, `search`, `podcasts`: Module-specific strings + - `apiErrors`: Error message translations + +3. **Register the locale** in `frontend/src/lib/locales/index.ts`: + ```typescript + import { ptBR } from './pt-BR' + + export const locales = { + 'en-US': enUS, + 'zh-CN': zhCN, + 'zh-TW': zhTW, + 'pt-BR': ptBR, // Add your locale + } + ``` + +4. **Add date-fns locale** in `frontend/src/lib/utils/date-locale.ts`: + ```typescript + import { zhCN, enUS, zhTW, ptBR } from 'date-fns/locale' + + const LOCALE_MAP: Record = { + 'zh-CN': zhCN, + 'zh-TW': zhTW, + 'en-US': enUS, + 'pt-BR': ptBR, // Add your locale + } + ``` + +5. **Test**: Switch languages using the language toggle in the UI header. + ### Database Migrations Database migrations run **automatically** when the API starts. diff --git a/README.md b/README.md index 05a1660..e4f72ec 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ In a world dominated by Artificial Intelligence, having the ability to think - πŸŽ™οΈ **Generate professional podcasts** - Advanced multi-speaker podcast generation - πŸ” **Search intelligently** - Full-text and vector search across all your content - πŸ’¬ **Chat with context** - AI conversations powered by your research +- 🌐 **Multi-language UI** - Chinese (Simplified & Traditional) support, more languages coming soon! Learn more about our project at [https://www.open-notebook.ai](https://www.open-notebook.ai) diff --git a/frontend/package.json b/frontend/package.json index 3e0e6e9..f63dd24 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "dev": "next dev", "build": "next build", "start": "node start-server.js", - "lint": "next lint", + "lint": "eslint src/", "test": "vitest run", "test:watch": "vitest", "test:ui": "vitest --ui" diff --git a/frontend/src/CLAUDE.md b/frontend/src/CLAUDE.md index c02e0fd..3dd7cbc 100644 --- a/frontend/src/CLAUDE.md +++ b/frontend/src/CLAUDE.md @@ -15,6 +15,7 @@ User interactions trigger mutations/queries via hooks, which communicate with th - **`lib/api/CLAUDE.md`**: Axios client, FormData handling, interceptors - **`lib/hooks/CLAUDE.md`**: TanStack Query wrappers, SSE streaming, context building - **`lib/stores/CLAUDE.md`**: Zustand auth/modal state, localStorage persistence +- **`lib/locales/CLAUDE.md`**: Internationalization (i18n) system, translation files - **`components/ui/CLAUDE.md`**: Radix UI primitives, CVA styling, accessibility ## Architectural Layers @@ -60,6 +61,12 @@ User interactions trigger mutations/queries via hooks, which communicate with th - API request/response shapes, domain models (Notebook, Source, Note, etc.) - Ensures type safety across API calls and store mutations +#### `lib/locales/` β€” Internationalization (i18n) +- **Locale files** (`en-US/`, `zh-CN/`, `zh-TW/`): Translation strings organized by feature +- **`i18n.ts`**: i18next configuration with language detection +- **`use-translation.ts`**: Custom hook with Proxy-based `t.section.key` access pattern +- **Pattern**: Components call `useTranslation()` hook; access strings via `t.common.save`, `t.notebooks.title` + ## Data & Control Flow Walkthrough ### Example: Notebook Chat @@ -123,12 +130,13 @@ User interactions trigger mutations/queries via hooks, which communicate with th ## Providers & Context Setup -**Root layout** (`app/layout.tsx`) wraps app with: -1. `ThemeProvider` β€” next-themes for light/dark mode -2. `QueryProvider` β€” TanStack Query client -3. `ErrorBoundary` β€” React error boundary -4. `ConnectionGuard` β€” checks backend connectivity on startup -5. `Toaster` β€” sonner toast notification system +**Root layout** (`app/layout.tsx`) wraps app with (outermost β†’ innermost): +1. `ErrorBoundary` β€” React error boundary (catches all render errors) +2. `ThemeProvider` β€” next-themes for light/dark mode +3. `QueryProvider` β€” TanStack Query client +4. `I18nProvider` β€” i18next initialization and language loading overlay +5. `ConnectionGuard` β€” checks backend connectivity on startup +6. `Toaster` β€” sonner toast notification system (inside ConnectionGuard) ## Important Gotchas & Design Decisions diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx index a9ba722..0863e40 100644 --- a/frontend/src/components/ui/alert-dialog.tsx +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -53,7 +53,6 @@ function AlertDialogContent({ 1000 accesses to same key in 1s triggers error and breaks recursion ## Testing Patterns diff --git a/frontend/src/lib/locales/CLAUDE.md b/frontend/src/lib/locales/CLAUDE.md new file mode 100644 index 0000000..7e430bd --- /dev/null +++ b/frontend/src/lib/locales/CLAUDE.md @@ -0,0 +1,141 @@ +# Locales Module (i18n) + +Internationalization system providing multi-language UI support using i18next with type-safe translation access. + +## Architecture + +``` +lib/ +β”œβ”€β”€ i18n.ts # i18next initialization and configuration +β”œβ”€β”€ i18n-events.ts # Language change event emitters +β”œβ”€β”€ hooks/ +β”‚ └── use-translation.ts # Custom hook with Proxy-based API +β”œβ”€β”€ utils/ +β”‚ └── date-locale.ts # date-fns locale mapping +└── locales/ + β”œβ”€β”€ index.ts # Locale registry and type exports + β”œβ”€β”€ en-US/index.ts # English translations + β”œβ”€β”€ zh-CN/index.ts # Simplified Chinese translations + └── zh-TW/index.ts # Traditional Chinese translations +``` + +## Key Components + +- **`i18n.ts`**: i18next initialization with language detection (localStorage β†’ browser) +- **`i18n-events.ts`**: Event emitters for language change start/end (used by loading overlay) +- **`locales/index.ts`**: Central registry exporting all locales and `LanguageCode` type +- **`use-translation.ts`**: Custom hook providing `t` object with nested property access + +## Translation Structure + +Each locale file exports a flat object with nested keys: + +```typescript +export const enUS = { + common: { + save: 'Save', + cancel: 'Cancel', + delete: 'Delete', + // ... + }, + notebooks: { + title: 'Notebooks', + createNew: 'Create Notebook', + // ... + }, + // ... other sections +} +``` + +**Sections**: +- `common`: Shared UI elements (buttons, labels, actions) +- `notebooks`, `sources`, `notes`: Feature-specific strings +- `chat`, `search`, `podcasts`: Module-specific strings +- `models`, `transformations`, `settings`: Configuration UI +- `advanced`: System administration strings +- `apiErrors`: Backend error message translations + +## Usage Pattern + +```typescript +import { useTranslation } from '@/lib/hooks/use-translation' + +function MyComponent() { + const { t, language, setLanguage } = useTranslation() + + // Nested property access (Proxy-based) + return

{t.notebooks.title}

+ + // With interpolation + return

{t.common.updated.replace('{time}', timeAgo)}

+ + // Change language + await setLanguage('zh-CN') +} +``` + +## Important Patterns + +- **Proxy-based access**: `t.section.key` instead of `t('section.key')` for better DX +- **Type safety**: `TranslationKeys` type derived from `enUS` locale +- **Language persistence**: Saved to localStorage, auto-detected on load +- **Fallback**: Falls back to `en-US` if key missing in current locale +- **Date localization**: Use `getDateLocale(language)` from `utils/date-locale.ts` + +## Key Dependencies + +- `i18next`: Core internationalization framework +- `react-i18next`: React bindings for i18next +- `i18next-browser-languagedetector`: Auto-detect browser language +- `date-fns/locale`: Date formatting locales + +## How to Add a New Language + +1. Create locale folder: `locales/pt-BR/index.ts` +2. Copy structure from `en-US/index.ts` and translate all strings +3. Register in `locales/index.ts`: + ```typescript + import { ptBR } from './pt-BR' + export const resources = { + // ...existing + 'pt-BR': { translation: ptBR }, + } + export const languages: Language[] = [ + // ...existing + { code: 'pt-BR', label: 'PortuguΓͺs' }, + ] + ``` +4. Add to `utils/date-locale.ts`: + ```typescript + import { ptBR } from 'date-fns/locale' + const LOCALE_MAP = { ...existing, 'pt-BR': ptBR } + ``` + +## Important Quirks & Gotchas + +- **Proxy depth limit**: `useTranslation` limits nesting to 4 levels to prevent infinite loops +- **Blocked properties**: React internals (`__proto__`, `$$typeof`, etc.) are blocked from Proxy traversal +- **Loop detection**: Access counts reset every 1s; >1000 accesses triggers error and breaks recursion +- **String methods**: `.replace()`, `.split()` work on translated strings via Proxy magic +- **Language change events**: `emitLanguageChangeStart/End` used by `LanguageLoadingOverlay` for UX +- **No SSR**: `useSuspense: false` disables React Suspense for i18next (avoids hydration issues) +- **All keys required**: Missing keys in non-English locales fall back to English; keep locales in sync + +## Testing Patterns + +```typescript +// Mock useTranslation in tests (see test/setup.ts) +vi.mock('@/lib/hooks/use-translation', () => ({ + useTranslation: () => ({ + t: enUS, // Use English locale directly + language: 'en-US', + setLanguage: vi.fn(), + }), +})) + +// Test locale completeness +import { enUS, zhCN } from '@/lib/locales' +const enKeys = Object.keys(flatten(enUS)) +const zhKeys = Object.keys(flatten(zhCN)) +expect(zhKeys).toEqual(enKeys) // All keys present +``` diff --git a/pyproject.toml b/pyproject.toml index bf23385..42ebda6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "open-notebook" -version = "1.4.0" +version = "1.5.0" description = "An open source implementation of a research assistant, inspired by Google Notebook LM" authors = [ {name = "Luis Novo", email = "lfnovo@gmail.com"} diff --git a/uv.lock b/uv.lock index 308032f..58f3c23 100644 --- a/uv.lock +++ b/uv.lock @@ -2375,7 +2375,7 @@ wheels = [ [[package]] name = "open-notebook" -version = "1.4.0" +version = "1.5.0" source = { editable = "." } dependencies = [ { name = "ai-prompter" },