From ed6ce65638bb913eee923aafffafe9c80524a591 Mon Sep 17 00:00:00 2001 From: Jarda Svoboda <20700514+jarda-svoboda@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:21:45 +0100 Subject: [PATCH 1/3] Improve docs --- CONTRIBUTING.md | 450 +++++++++++++++++++ README.md | 260 +++++++---- docs/ARCHITECTURE.md | 545 +++++++++++++++++++++++ docs/BEST_PRACTICES.md | 904 ++++++++++++++++++++++++++++++++++++++ docs/GETTING_STARTED.md | 530 ++++++++++++++++++++++ docs/INDEX.md | 279 ++++++++++++ docs/README.md | 747 ++++++++++++++++++++++++++++--- docs/TROUBLESHOOTING.md | 946 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 4533 insertions(+), 128 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/BEST_PRACTICES.md create mode 100644 docs/GETTING_STARTED.md create mode 100644 docs/INDEX.md create mode 100644 docs/TROUBLESHOOTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..49d6b0f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,450 @@ +# Contributing to sveltekit-i18n + +Thank you for your interest in contributing to `sveltekit-i18n`! We welcome contributions from the community. + +> **Note:** We're currently looking for maintainers to help with the project. If you're interested, please see [this issue](https://github.com/sveltekit-i18n/lib/issues/197). + +## Table of Contents + +- [Ecosystem Overview](#ecosystem-overview) +- [Repository Structure](#repository-structure) +- [Getting Started](#getting-started) +- [Git Workflow](#git-workflow) +- [Commit Guidelines](#commit-guidelines) +- [Making Changes](#making-changes) +- [Testing](#testing) +- [Pull Request Process](#pull-request-process) +- [Code Standards](#code-standards) +- [Release Process](#release-process-maintainers-only) +- [Getting Help](#getting-help) + +## Ecosystem Overview + +The `sveltekit-i18n` ecosystem consists of three separate repositories: + +- **[sveltekit-i18n/lib](https://github.com/sveltekit-i18n/lib)** (this repository) – Main user-facing package that combines base with default parser +- **[@sveltekit-i18n/base](https://github.com/sveltekit-i18n/base)** – Core i18n functionality with parser support +- **[@sveltekit-i18n/parsers](https://github.com/sveltekit-i18n/parsers)** – Message parsers (parser-default, parser-icu) + +This repository (`lib`) is what most users install and is the main entry point for contributions. + +## Repository Structure + +``` +lib/ +├── src/ # Source code (TypeScript) +│ ├── index.ts # Main entry point +│ └── types.ts # Type definitions +├── tests/ # Test files (Jest) +│ ├── specs/ # Test specifications +│ └── data/ # Test data and fixtures +├── examples/ # Working SvelteKit example apps +├── docs/ # User documentation +│ ├── INDEX.md +│ ├── GETTING_STARTED.md +│ ├── ARCHITECTURE.md +│ ├── BEST_PRACTICES.md +│ └── TROUBLESHOOTING.md +├── dist/ # Built files (git-ignored) +└── package.json +``` + +## Getting Started + +### Prerequisites + +- Node.js 18+ (or current LTS version) +- npm or pnpm +- Git + +### Setup + +```bash +# Clone the repository +git clone https://github.com/sveltekit-i18n/lib.git +cd lib + +# Install dependencies +npm install +``` + +### Development Commands + +```bash +npm run dev # Watch mode with auto-rebuild +npm run build # Production build +npm test # Run tests +npm run lint # Check code style (runs automatically on commit) +``` + +## Git Workflow + +We use a **rebase-based workflow** to maintain a linear, clean git history. + +### Branch Strategy + +- `master` – Stable, production-ready code +- Use descriptive branch names for your work + +### Workflow + +```bash +# Create a feature branch (use any descriptive name) +git checkout -b my-descriptive-branch-name + +# Make changes and commit atomically +git add . +git commit -m "Add feature X" + +# Keep your branch updated (rebase, NOT merge) +git fetch origin +git rebase origin/master + +# Push (use force-with-lease after rebase) +git push origin my-descriptive-branch-name --force-with-lease +``` + +### Important Guidelines + +- **Use rebase**, not merge commits +- **Keep linear history** – no merge commits in the history +- **Force-push with `--force-with-lease`** (safer than `--force`) +- Rebase your branch on `master` before creating a PR + +## Commit Guidelines + +### Atomic Commits + +Each commit must be **self-contained and meaningful**: + +- ✅ Works independently (can be cherry-picked) +- ✅ Has a single, clear purpose +- ✅ Passes tests on its own +- ✅ Has a descriptive commit message + +### Commit Message Format + +``` +Brief description in imperative mood (max 72 characters) + +Optional longer explanation if needed. Explain WHAT and WHY, +not HOW (the code shows how). + +Reference related issues if applicable: +Fixes #123 +Relates to #456 +``` + +### Good Commit Examples + +``` +Add locale switcher component +Fix translation loading race condition +Update API docs with TypeScript examples +Refactor loader matching logic for clarity +Add tests for route-based translation loading +``` + +### Bad Commit Examples + +``` +WIP +fixes +Update stuff +Changed some files +lots of changes +``` + +## Making Changes + +### Code Changes + +- All source code is in `src/` +- TypeScript strict mode is enforced +- ESLint runs automatically on commit (pre-commit hook) +- **No `any` types** – use proper typing or `unknown` if necessary + +### Documentation Changes + +- User documentation is in `docs/` +- All code comments must be in **English** +- Update relevant documentation when changing APIs +- Add examples for new features + +### Adding Examples + +- Examples are in `examples/` +- Each example should be a complete, working SvelteKit application +- Include a README explaining the use case and how it works + +## Testing + +### Running Tests + +```bash +# Run all tests +npm test + +# Watch mode +npm test -- --watch + +# Run specific test file +npm test -- path/to/test.spec.ts +``` + +### Writing Tests + +- Place tests in `tests/specs/` +- Test data and fixtures in `tests/data/` +- Use Jest with TypeScript +- Test both functionality and TypeScript types +- Aim for high coverage on core functionality + +### Test Structure + +```typescript +describe('feature name', () => { + it('should do something specific', () => { + // Arrange - set up test data + const input = { locale: 'en', key: 'test' }; + + // Act - execute the code + const result = someFunction(input); + + // Assert - verify the result + expect(result).toBe(expected); + }); +}); +``` + +## Pull Request Process + +### Before Creating a PR + +1. **Rebase on latest master:** `git rebase origin/master` +2. **Run tests:** `npm test` (all tests must pass) +3. **Run linter:** `npm run lint` (no errors) +4. **Build successfully:** `npm run build` +5. **Update documentation** if you changed APIs + +### PR Title + +- Descriptive and concise +- Use imperative mood: "Add feature" not "Added feature" +- Examples: "Add locale switching support", "Fix memory leak in loader cache" + +### PR Description Template + +```markdown +## What +Brief description of the changes made. + +## Why +Explanation of why this change is needed. Link to related issues. + +## How +How you implemented it (if the approach is complex or non-obvious). + +## Testing +How you tested the changes. Include test scenarios. + +## Checklist +- [ ] Tests pass locally +- [ ] Linter passes +- [ ] Documentation updated (if needed) +- [ ] All commits are atomic and well-described +- [ ] Branch rebased on latest master +``` + +### Review Process + +1. A maintainer will review your PR +2. Feedback may be requested – discussion is encouraged +3. Make requested changes in new commits +4. Once approved, maintainer will merge using rebase + +### Updating Your PR + +```bash +# Make requested changes +git add . +git commit -m "Address review feedback" + +# Rebase and clean up commits if needed +git rebase -i origin/master + +# Force push with updated commits +git push origin my-branch-name --force-with-lease +``` + +## Code Standards + +### TypeScript + +- **Strict mode enabled** – all strict checks enforced +- **No `any` types** – use proper types or `unknown` if type is truly unknown +- **Proper type definitions** for all exports +- Use `const` assertions where appropriate +- Leverage TypeScript's inference when possible + +### ESLint + +- Airbnb TypeScript configuration +- Auto-fixes run on commit (pre-commit hook) +- Follow existing code style in the repository +- Don't disable rules without good reason (and explanation) + +### Code Comments + +- **English only** – all comments and documentation in English +- Use JSDoc for public APIs +- Explain **WHY**, not WHAT (code shows what) +- Add comments for non-obvious logic + +**Example:** + +```typescript +// ✅ Good - explains why +// Use WeakMap to avoid memory leaks when components are destroyed +const cache = new WeakMap(); + +// ❌ Bad - just describes what code does +// Create a new WeakMap +const cache = new WeakMap(); +``` + +## Architecture Overview + +For developers working on the codebase, here's a brief technical overview: + +### Package Structure + +This package extends `@sveltekit-i18n/base` and pre-configures it with `@sveltekit-i18n/parser-default`: + +```typescript +import Base from '@sveltekit-i18n/base'; +import parser from '@sveltekit-i18n/parser-default'; + +class I18n extends Base { + constructor(config) { + // Normalize config to include default parser + super({ + ...config, + parser: parser(config.parserOptions) + }); + } +} +``` + +### Key Concepts + +- **Loaders** – Define how and when translations load +- **Routes** – Match loaders to specific routes +- **Parser** – Handles message interpolation +- **Preprocessing** – Transforms translation data after loading + +### For Detailed Architecture + +See the [Architecture Documentation](./docs/ARCHITECTURE.md) for in-depth explanation of how everything works. + +## Related Repositories + +### Contributing to Other Parts of the Ecosystem + +**Core functionality (@sveltekit-i18n/base):** +- Repository: https://github.com/sveltekit-i18n/base +- Contribute here for: Core i18n logic, store management, loader system + +**Parsers (@sveltekit-i18n/parsers):** +- Repository: https://github.com/sveltekit-i18n/parsers +- Contribute here for: Parser syntax changes, new parsers, modifier logic + +Each repository has its own issues and contribution guidelines. + +## Release Process (Maintainers Only) + +This section is for maintainers with publish access. + +### Version Bump + +```bash +# Patch release (bug fixes) +npm version patch + +# Minor release (new features, backwards compatible) +npm version minor + +# Major release (breaking changes) +npm version major +``` + +### Publishing + +```bash +# Build the package +npm run build + +# Publish to npm +npm publish + +# Push tags to GitHub +git push origin master --tags +``` + +### Changelog + +- Update `CHANGELOG.md` before releasing +- Follow [Semantic Versioning](https://semver.org/) +- Group changes by type: Features, Bug Fixes, Breaking Changes + +## Getting Help + +### Questions + +- **GitHub Discussions:** https://github.com/sveltekit-i18n/lib/discussions +- **Issues:** https://github.com/sveltekit-i18n/lib/issues + +### Reporting Bugs + +When reporting a bug, include: + +- `sveltekit-i18n` version +- `SvelteKit` version +- Node.js version +- Minimal reproduction (CodeSandbox, StackBlitz, or GitHub repo) +- Expected behavior vs. actual behavior +- Error messages and stack traces + +### Feature Requests + +When requesting a feature, include: + +- **Use case description** – What are you trying to achieve? +- **Proposed API** (if you have ideas) +- **Why current features don't work** – Have you tried existing approaches? +- **Alternatives considered** – What other solutions did you consider? + +### Discussions + +For general questions, ideas, or showcasing what you've built, use [GitHub Discussions](https://github.com/sveltekit-i18n/lib/discussions). + +## Maintainer Opportunity + +We're actively looking for maintainers to help with: + +- Reviewing pull requests +- Triaging issues +- Maintaining documentation +- Planning future direction + +If you're interested: + +1. Review the [maintainer opportunity issue](https://github.com/sveltekit-i18n/lib/issues/197) +2. Make consistent, quality contributions +3. Demonstrate understanding of the codebase and architecture +4. Reach out to express your interest + +--- + +Thank you for contributing to `sveltekit-i18n`! Your efforts help make internationalization easier for the SvelteKit community. 🌍 + diff --git a/README.md b/README.md index 69c24c1..88c06f6 100644 --- a/README.md +++ b/README.md @@ -7,126 +7,232 @@ # sveltekit-i18n -`sveltekit-i18n` is a tiny library with no external dependencies, built for [Svelte](https://github.com/sveltejs/svelte) and [SvelteKit](https://github.com/sveltejs/kit). It glues [@sveltekit-i18n/base](https://github.com/sveltekit-i18n/base) and [@sveltekit-i18n/parser-default](https://github.com/sveltekit-i18n/parsers/tree/master/parser-default) together to provide you the most straightforward `sveltekit-i18n` solution. -## Key features +A lightweight, powerful internationalization (i18n) library designed specifically for [SvelteKit](https://github.com/sveltejs/kit). This package combines [@sveltekit-i18n/base](https://github.com/sveltekit-i18n/base) with [@sveltekit-i18n/parser-default](https://github.com/sveltekit-i18n/parsers/tree/master/parser-default) to provide the quickest way to add multilingual support to your SvelteKit applications. -✅ SvelteKit ready\ -✅ SSR support\ -✅ Custom data sources – no matter if you are using local files or remote API to get your translations\ -✅ Module-based – your translations are loaded for visited pages only (and only once!)\ -✅ Component-scoped translations – you can create multiple instances with custom definitions\ -✅ Custom modifiers – you can modify the input data the way you really need\ -✅ TS support\ -✅ No external dependencies +## Why sveltekit-i18n? -## Usage +- 🚀 **SvelteKit-optimized** – Built specifically for SvelteKit with full SSR support +- 📦 **Minimal dependencies** – Only ecosystem packages (base + parser-default) +- ⚡ **Smart loading** – Translations load only for visited pages (lazy loading) +- 🎯 **Route-based** – Automatic translation loading based on your routes +- 🔧 **Flexible** – Support for custom data sources (local files, APIs, databases) +- 🌐 **Multiple parsers** – Choose the syntax that fits your needs +- 📝 **TypeScript** – Complete type definitions and typed API +- 🎨 **Component-scoped** – Create multiple translation instances for different parts of your app + +## Installation + +```bash +npm install sveltekit-i18n +``` + +## Quick Start + +### 1. Create your translation files + +```json +// src/lib/translations/en/common.json +{ + "greeting": "Hello, {{name}}!", + "nav.home": "Home", + "nav.about": "About" +} +``` + +```json +// src/lib/translations/cs/common.json +{ + "greeting": "Ahoj, {{name}}!", + "nav.home": "Domů", + "nav.about": "O nás" +} +``` + +### 2. Setup i18n configuration -Setup `translations.js` in your lib folder... ```javascript +// src/lib/translations/index.js import i18n from 'sveltekit-i18n'; /** @type {import('sveltekit-i18n').Config} */ -const config = ({ +const config = { loaders: [ { locale: 'en', key: 'common', - loader: async () => ( - await import('./en/common.json') - ).default, - }, - { - locale: 'en', - key: 'home', - routes: ['/'], // you can use regexes as well! - loader: async () => ( - await import('./en/home.json') - ).default, - }, - { - locale: 'en', - key: 'about', - routes: ['/about'], - loader: async () => ( - await import('./en/about.json') - ).default, + loader: async () => (await import('./en/common.json')).default, }, { locale: 'cs', key: 'common', - loader: async () => ( - await import('./cs/common.json') - ).default, - }, - { - locale: 'cs', - key: 'home', - routes: ['/'], - loader: async () => ( - await import('./cs/home.json') - ).default, - }, - { - locale: 'cs', - key: 'about', - routes: ['/about'], - loader: async () => ( - await import('./cs/about.json') - ).default, + loader: async () => (await import('./cs/common.json')).default, }, ], -}); +}; export const { t, locale, locales, loading, loadTranslations } = new i18n(config); ``` -...load your translations in `+layout.js`... +### 3. Load translations in your layout ```javascript +// src/routes/+layout.js import { loadTranslations } from '$lib/translations'; -/** @type {import('@sveltejs/kit').Load} */ +/** @type {import('./$types').LayoutLoad} */ export const load = async ({ url }) => { const { pathname } = url; + + const initLocale = 'en'; // determine from cookie, user preference, etc. + + await loadTranslations(initLocale, pathname); + + return {}; +}; +``` + +### 4. Use translations in your components - const initLocale = 'en'; // get from cookie, user session, ... +```svelte + + - await loadTranslations(initLocale, pathname); // keep this just before the `return` +

{$t('common.greeting', { name: 'World' })}

- return {}; -} + ``` -...and include your translations within pages and components. +## Key Features + +### Route-based Loading + +Load translations only for specific routes to optimize performance: + +```javascript +const config = { + loaders: [ + { + locale: 'en', + key: 'home', + routes: ['/'], // Load only on homepage + loader: async () => (await import('./en/home.json')).default, + }, + { + locale: 'en', + key: 'about', + routes: ['/about'], // Load only on about page + loader: async () => (await import('./en/about.json')).default, + }, + ], +}; +``` + +### Placeholders and Modifiers + +Use dynamic values in your translations: + +```json +{ + "welcome": "Welcome, {{name}}!", + "items": "You have {{count:number;}} {{count; 1:item; default:items;}}." +} +``` ```svelte -
- -

{$t('common.page', { pageName })}

-

{$t('home.content')}

-
+

{$t('welcome', { name: 'Alice' })}

+

{$t('items', { count: 5 })}

``` -## More info -[Docs](https://github.com/sveltekit-i18n/lib/tree/master/docs/README.md)\ -[Examples](https://github.com/sveltekit-i18n/lib/tree/master/examples)\ -[Changelog](https://github.com/sveltekit-i18n/lib/releases) +## Documentation + +**📖 [Complete Documentation Index](./docs/INDEX.md)** – Find everything in one place + +### Quick Links + +- 🚀 [Getting Started Guide](./docs/GETTING_STARTED.md) – 15-minute tutorial +- 🏗️ [Architecture Overview](./docs/ARCHITECTURE.md) – How everything works +- 📚 [API Documentation](./docs/README.md) – Complete reference +- ✨ [Best Practices](./docs/BEST_PRACTICES.md) – Production-ready patterns +- 🔧 [Troubleshooting](./docs/TROUBLESHOOTING.md) – Common issues & FAQ + +## Examples + +Explore working examples for different use cases: + +- [Multi-page app](./examples/multi-page) – Most common setup +- [Locale-based routing](./examples/locale-router) – SEO-friendly URLs (e.g., `/en/about`) +- [Component-scoped translations](./examples/component-scoped-ssr) – Isolated translation contexts +- [Custom parsers](./examples/parser-icu) – Using ICU message format +- [All examples](./examples) – Complete list of examples + +## Advanced Usage + +### Need a different parser? + +This library uses `@sveltekit-i18n/parser-default`. If you need ICU message format or want to create your own parser, use [@sveltekit-i18n/base](https://github.com/sveltekit-i18n/base) directly: + +```javascript +import i18n from '@sveltekit-i18n/base'; +import parser from '@sveltekit-i18n/parser-icu'; + +const config = { + parser: parser(), + // ... rest of config +}; +``` + +Learn more about [parsers](https://github.com/sveltekit-i18n/parsers). + +## TypeScript Support + +Full TypeScript support with complete type definitions for configuration and API: + +```typescript +import i18n, { type Config } from 'sveltekit-i18n'; + +const config: Config = { + loaders: [ + // ... your loaders + ], +}; + +export const { t, locale, locales, loading, loadTranslations } = new i18n(config); +``` + +**Note:** The library provides type definitions but does not automatically infer translation keys from your JSON files. You can create custom type-safe wrappers if needed (see [Best Practices](./docs/BEST_PRACTICES.md#typescript-patterns)). + +## Contributing + +We welcome contributions! Please read our [Contributing Guide](./CONTRIBUTING.md) for details on: + +- Development setup and workflow +- Git workflow (rebase-based, linear history) +- Commit guidelines (atomic commits) +- Pull request process +- Code standards and testing + +**Note:** We're currently looking for maintainers. If you're interested in helping maintain this project, please reach out via [this issue](https://github.com/sveltekit-i18n/lib/issues/197). + +## Changelog -## Roadmap +See [Releases](https://github.com/sveltekit-i18n/lib/releases) for version history. -### Q4 2025 +## Related Packages -[ ] - Update the examples -[ ] - Stabalise build & tests -[ ] - Consolidate issues +- [@sveltekit-i18n/base](https://github.com/sveltekit-i18n/base) – Core functionality with custom parser support +- [@sveltekit-i18n/parser-default](https://github.com/sveltekit-i18n/parsers/tree/master/parser-default) – Default message parser +- [@sveltekit-i18n/parser-icu](https://github.com/sveltekit-i18n/parsers/tree/master/parser-icu) – ICU message format parser -### Q1 2026 +## License -[ ] - Storage + State management overhall \ No newline at end of file +MIT diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..c09f94c --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,545 @@ +# Architecture Overview + +This document explains how `sveltekit-i18n` works internally and how the three packages interact with each other. Understanding the architecture will help you make informed decisions about which package to use and how to configure it for your needs. + +## Table of Contents + +- [Package Overview](#package-overview) +- [Package Relationships](#package-relationships) +- [Data Flow](#data-flow) +- [Loading Strategy](#loading-strategy) +- [When to Use Each Package](#when-to-use-each-package) +- [Core Concepts](#core-concepts) + +## Package Overview + +The `sveltekit-i18n` ecosystem consists of three main packages: + +### 1. @sveltekit-i18n/base + +**Role:** Core i18n functionality + +**Responsibilities:** +- Managing translation state (Svelte stores) +- Loading and caching translations +- Route matching logic +- Preprocessing translations +- Coordinating with parsers + +**What it doesn't include:** +- Message interpolation (delegates to parsers) +- Default parser + +**Use when:** You need custom parsers or maximum flexibility + +### 2. sveltekit-i18n (lib) + +**Role:** Complete solution with sensible defaults + +**Responsibilities:** +- Everything from `@sveltekit-i18n/base` +- Pre-configured with `@sveltekit-i18n/parser-default` +- Simplified API + +**Dependencies:** `@sveltekit-i18n/base` + `@sveltekit-i18n/parser-default` (no external dependencies) + +**Use when:** You want the quickest setup and are happy with default parser syntax + +### 3. @sveltekit-i18n/parsers + +**Role:** Message interpolation + +**Packages:** +- `@sveltekit-i18n/parser-default` – Simple placeholder/modifier syntax +- `@sveltekit-i18n/parser-icu` – ICU message format + +**Responsibilities:** +- Interpolating variables into translation strings +- Formatting (numbers, dates, currencies) +- Conditional rendering (plurals, gender, etc.) + +**Use when:** You need specific message syntax (included automatically with `sveltekit-i18n` or `@sveltekit-i18n/base`) + +## Package Relationships + +Here's how the packages relate to each other: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Your SvelteKit App │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Option 1: Use Complete Solution │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ sveltekit-i18n (lib) │ │ +│ │ ┌────────────────────────────────────────┐ │ │ +│ │ │ @sveltekit-i18n/base │ │ │ +│ │ │ (core functionality) │ │ │ +│ │ └────────────────────────────────────────┘ │ │ +│ │ ┌────────────────────────────────────────┐ │ │ +│ │ │ @sveltekit-i18n/parser-default │ │ │ +│ │ │ (included) │ │ │ +│ │ └────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ +│ Option 2: Use Base with Custom Parser │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ @sveltekit-i18n/base │ │ +│ │ (you provide the parser) │ │ +│ └──────────────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ @sveltekit-i18n/parser-icu │ │ +│ │ or your custom parser │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Dependency Tree + +``` +sveltekit-i18n +├── @sveltekit-i18n/base +└── @sveltekit-i18n/parser-default + +@sveltekit-i18n/base +└── (no dependencies) + +@sveltekit-i18n/parser-default +└── (no dependencies) + +@sveltekit-i18n/parser-icu +└── intl-messageformat +``` + +## Data Flow + +Here's how translations flow through the system: + +### 1. Configuration Phase + +```javascript +import i18n from 'sveltekit-i18n'; + +const config = { + loaders: [ + { + locale: 'en', + key: 'common', + routes: ['/'], + loader: async () => (await import('./en/common.json')).default, + }, + ], +}; + +const { t, locale, loadTranslations } = new i18n(config); +``` + +**What happens:** +1. i18n instance is created +2. Loaders are registered (not executed yet) +3. Svelte stores are initialized +4. Parser is configured + +### 2. Loading Phase + +```javascript +// In +layout.js +await loadTranslations('en', '/'); +``` + +**Flow:** + +``` +┌─────────────────────────────────────────────────────┐ +│ 1. loadTranslations('en', '/') │ +└────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 2. Match loaders │ +│ - locale === 'en' │ +│ - routes includes '/' OR routes is undefined │ +└────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 3. Execute loader functions │ +│ loader() → returns translation data │ +└────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 4. Preprocess translations │ +│ - 'full': flatten to dot notation │ +│ - 'preserveArrays': flatten but keep arrays │ +│ - 'none': no changes │ +│ - custom function: your logic │ +└────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 5. Store in translations store │ +│ { en: { 'common.greeting': 'Hello' } } │ +└────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 6. Set locale │ +│ locale.set('en') │ +└─────────────────────────────────────────────────────┘ +``` + +**Caching:** Each loader runs only once per locale. Results are cached in memory. + +### 3. Translation Phase + +```svelte +

{$t('common.greeting', { name: 'Alice' })}

+``` + +**Flow:** + +``` +┌─────────────────────────────────────────────────────┐ +│ 1. $t('common.greeting', { name: 'Alice' }) │ +└────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 2. Get current locale │ +│ locale.get() → 'en' │ +└────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 3. Lookup translation │ +│ translations['en']['common.greeting'] │ +│ → 'Hello, {{name}}!' │ +└────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 4. Parser.parse() │ +│ parse('Hello, {{name}}!', [{ name: 'Alice' }]) │ +│ → 'Hello, Alice!' │ +└────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 5. Return result │ +│ 'Hello, Alice!' │ +└─────────────────────────────────────────────────────┘ +``` + +### 4. Locale Switch + +```javascript +locale.set('cs'); +``` + +**Flow:** + +``` +┌─────────────────────────────────────────────────────┐ +│ 1. locale.set('cs') │ +└────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ 2. Check if translations exist │ +│ translations['cs'] ? │ +└────────────────┬────────────────────────────────────┘ + │ + ┌───────┴────────┐ + │ │ + ▼ ▼ + ┌─────────┐ ┌──────────┐ + │ Exists │ │ Missing │ + └────┬────┘ └────┬─────┘ + │ │ + │ ▼ + │ ┌─────────────────────┐ + │ │ Load translations │ + │ │ (matching loaders) │ + │ └────┬────────────────┘ + │ │ + └─────────┴────────┐ + ▼ + ┌───────────────────────────┐ + │ 3. Update locale store │ + │ triggers reactivity │ + └───────────┬───────────────┘ + │ + ▼ + ┌───────────────────────────┐ + │ 4. All $t() re-evaluate │ + │ UI updates │ + └───────────────────────────┘ +``` + +## Loading Strategy + +### Route-based Loading + +The library uses SvelteKit's routing to determine which translations to load: + +```javascript +const config = { + loaders: [ + // No routes specified → loads on every page + { + locale: 'en', + key: 'common', + loader: async () => (await import('./en/common.json')).default, + }, + + // Exact match → loads only on '/' + { + locale: 'en', + key: 'home', + routes: ['/'], + loader: async () => (await import('./en/home.json')).default, + }, + + // Regex → loads on matching routes + { + locale: 'en', + key: 'products', + routes: [/^\/products/], + loader: async () => (await import('./en/products.json')).default, + }, + ], +}; +``` + +**Matching algorithm:** + +```javascript +function shouldLoadTranslation(loader, currentRoute) { + // No routes specified → always load + if (!loader.routes || loader.routes.length === 0) { + return true; + } + + // Check each route pattern + for (const route of loader.routes) { + if (typeof route === 'string') { + // Exact string match + if (currentRoute === route) return true; + } else if (route instanceof RegExp) { + // Regex match + if (route.test(currentRoute)) return true; + } + } + + return false; +} +``` + +### Server vs Client Loading + +**Server-Side (SSR):** +- Translations load during `+layout.js` or `+page.js` load +- Data is serialized and sent to client +- Hydration picks up from there + +**Client-Side:** +- When locale changes, new translations load on client +- When navigating, route-specific translations load +- Loading state available via `$loading` store + +### Caching Strategy + +**In-Memory Cache:** +```javascript +{ + 'en': { + 'common': { /* translations */ }, + 'home': { /* translations */ }, + }, + 'cs': { + 'common': { /* translations */ }, + }, +} +``` + +**Cache Refresh:** +- Client: Never refreshes (single session) +- Server: Configurable via `cache` option (default: 24 hours) + +```javascript +const config = { + cache: 86400000, // 24 hours in milliseconds + // or: Number.POSITIVE_INFINITY (never refresh) +}; +``` + +## When to Use Each Package + +### Use `sveltekit-i18n` (lib) + +```javascript +import i18n from 'sveltekit-i18n'; +``` + +**When:** +- ✅ You want the quickest setup +- ✅ Default parser syntax is sufficient +- ✅ You don't need custom parsers +- ✅ You want zero dependencies + +**Best for:** Most projects, rapid prototyping, simple to medium complexity + +### Use `@sveltekit-i18n/base` + +```javascript +import i18n from '@sveltekit-i18n/base'; +import parser from '@sveltekit-i18n/parser-icu'; +``` + +**When:** +- ✅ You need ICU message format +- ✅ You want to create a custom parser +- ✅ You're migrating from another i18n library +- ✅ You need specific message syntax + +**Best for:** Complex projects, enterprise applications, specific requirements + +## Core Concepts + +### Svelte Stores + +The library uses Svelte stores for reactivity: + +```javascript +// Readable stores (read-only) +$t // Translation function +$locales // Available locales +$loading // Loading state +$initialized // Initialization state + +// Writable stores (can be updated) +$locale // Current locale +``` + +**Reactivity:** When stores update, all components using them re-render automatically. + +### Preprocessing + +Transforms nested objects into flat dot notation: + +**Input:** +```json +{ + "user": { + "profile": { + "name": "Name", + "email": "Email" + } + } +} +``` + +**Output (preprocess: 'full'):** +```json +{ + "user.profile.name": "Name", + "user.profile.email": "Email" +} +``` + +**Why?** Enables efficient lookups and simpler translation keys in code. + +### Parser Interface + +All parsers implement this interface: + +```typescript +interface Parser { + parse( + value: any, // Translation value + params: any[], // Parameters from $t() + locale: string, // Current locale + key: string // Translation key + ): string; +} +``` + +**Example implementation:** + +```javascript +const simpleParser = () => ({ + parse: (value, params) => { + const vars = params[0] || {}; + return String(value).replace( + /\{(\w+)\}/g, + (_, key) => vars[key] ?? key + ); + }, +}); +``` + +### Namespaces + +Namespaces organize translations into logical groups: + +``` +common → Shared UI, navigation, errors +home → Homepage content +products → Product-related text +checkout → Checkout flow +``` + +**Benefits:** +- Easier to manage +- Enables lazy loading +- Better code organization +- Multiple teams can work independently + +## Performance Considerations + +### Bundle Size + +**sveltekit-i18n:** +- Core: ~5KB (minified) +- Includes: base + parser-default +- No external dependencies + +**@sveltekit-i18n/base + parser-icu:** +- Base: ~5KB (zero dependencies) +- Parser-ICU: ~15KB + intl-messageformat dependency + +### Loading Performance + +**Best practices:** +1. Use route-based loading (don't load everything at once) +2. Keep common translations small +3. Use code splitting (dynamic imports) +4. Enable server-side caching + +### Runtime Performance + +- Translation lookup: O(1) (object property access) +- Parser execution: Varies by complexity +- Store updates: Svelte's efficient reactivity + +## Summary + +The `sveltekit-i18n` architecture is designed to be: + +- **Modular** – Use only what you need +- **Flexible** – Customize with parsers and config +- **Performant** – Lazy loading and efficient caching +- **TypeScript** – Complete type definitions +- **Developer-friendly** – Simple API, clear concepts + +Choose the right package for your needs and leverage the loading strategies to build efficient, multilingual SvelteKit applications. + +## See Also + +- [Getting Started](./GETTING_STARTED.md) – Learn by building +- [API Documentation](./README.md) – Complete reference +- [Best Practices](./BEST_PRACTICES.md) – Recommended patterns +- [Parsers](https://github.com/sveltekit-i18n/parsers) – Parser details + diff --git a/docs/BEST_PRACTICES.md b/docs/BEST_PRACTICES.md new file mode 100644 index 0000000..8c91a4e --- /dev/null +++ b/docs/BEST_PRACTICES.md @@ -0,0 +1,904 @@ +# Best Practices + +This guide covers recommended patterns, conventions, and tips for using `sveltekit-i18n` effectively in production applications. + +## Table of Contents + +- [Translation File Organization](#translation-file-organization) +- [Key Naming Conventions](#key-naming-conventions) +- [Performance Optimization](#performance-optimization) +- [TypeScript Patterns](#typescript-patterns) +- [SSR and CSR Considerations](#ssr-and-csr-considerations) +- [Component-Scoped Translations](#component-scoped-translations) +- [Dynamic Routes and Locales](#dynamic-routes-and-locales) +- [Content Management](#content-management) +- [Testing](#testing) +- [Production Deployment](#production-deployment) + +## Translation File Organization + +### Directory Structure + +Organize translations by locale and namespace: + +``` +src/lib/translations/ +├── index.js # i18n configuration +├── en/ +│ ├── common.json # Shared UI, navigation +│ ├── home.json # Homepage +│ ├── about.json # About page +│ ├── products.json # Product pages +│ └── errors.json # Error messages +├── cs/ +│ ├── common.json +│ ├── home.json +│ ├── about.json +│ ├── products.json +│ └── errors.json +└── de/ + ├── common.json + └── ... +``` + +**✅ Benefits:** +- Easy to find translations +- Clear separation by locale +- Scalable structure + +### Namespace Strategy + +#### Common Namespace + +Keep frequently used translations in a `common` namespace loaded on every page: + +```json +// common.json +{ + "app.name": "My App", + "nav.home": "Home", + "nav.about": "About", + "nav.products": "Products", + "button.save": "Save", + "button.cancel": "Cancel", + "button.delete": "Delete", + "error.required": "This field is required", + "error.invalid": "Invalid input" +} +``` + +**Keep it small** – Only essentials that appear across multiple pages. + +#### Page-Specific Namespaces + +Create separate namespaces for each major page or section: + +```javascript +const config = { + loaders: [ + // Common (all pages) + { locale: 'en', key: 'common', loader: async () => (await import('./en/common.json')).default }, + + // Page-specific (route-based) + { locale: 'en', key: 'home', routes: ['/'], loader: async () => (await import('./en/home.json')).default }, + { locale: 'en', key: 'about', routes: ['/about'], loader: async () => (await import('./en/about.json')).default }, + { locale: 'en', key: 'products', routes: [/^\/products/], loader: async () => (await import('./en/products.json')).default }, + ], +}; +``` + +**✅ Benefits:** +- Translations load only when needed +- Smaller initial bundle +- Easier to manage + +#### Feature-Based Organization + +For large apps, organize by feature instead of page: + +``` +translations/ +├── en/ +│ ├── common.json +│ ├── auth.json # Login, register, password reset +│ ├── checkout.json # Cart, payment, confirmation +│ ├── profile.json # User profile, settings +│ └── admin.json # Admin panel +``` + +```javascript +const config = { + loaders: [ + { locale: 'en', key: 'auth', routes: ['/login', '/register', '/reset-password'], loader: async () => (await import('./en/auth.json')).default }, + { locale: 'en', key: 'checkout', routes: [/^\/cart/, /^\/checkout/], loader: async () => (await import('./en/checkout.json')).default }, + ], +}; +``` + +### File Size Guidelines + +**Target sizes:** +- `common.json`: < 5 KB (essential shared content) +- Page-specific: < 20 KB per file +- If larger, consider splitting into sub-namespaces + +**Example split:** + +``` +products/ +├── list.json # Product listing page +├── detail.json # Product detail page +├── compare.json # Product comparison +└── reviews.json # Product reviews +``` + +## Key Naming Conventions + +### Use Dot Notation + +Organize keys hierarchically with dots: + +```json +{ + "user.profile.name": "Name", + "user.profile.email": "Email", + "user.settings.privacy": "Privacy", + "user.settings.notifications": "Notifications" +} +``` + +**✅ Clear structure, easy to find related translations** + +### Descriptive Names + +Use clear, descriptive key names: + +```json +// ❌ Bad +{ + "t1": "Welcome", + "btn": "Click", + "txt": "Hello" +} + +// ✅ Good +{ + "home.welcome.title": "Welcome", + "common.button.submit": "Submit", + "greeting.message": "Hello" +} +``` + +### Consistency Patterns + +Establish naming patterns and stick to them: + +```json +{ + // Pages + "home.title": "Home", + "about.title": "About", + "contact.title": "Contact", + + // Forms + "form.label.name": "Name", + "form.label.email": "Email", + "form.placeholder.search": "Search...", + "form.error.required": "Required", + + // Buttons + "button.submit": "Submit", + "button.cancel": "Cancel", + "button.save": "Save", + + // Messages + "message.success.saved": "Successfully saved", + "message.error.failed": "Operation failed" +} +``` + +### Avoid Deep Nesting + +**❌ Too deep (harder to maintain):** + +```json +{ + "pages.user.profile.settings.privacy.options.visibility.public": "Public" +} +``` + +**✅ Better (balanced):** + +```json +{ + "profile.privacy.public": "Public" +} +``` + +**Rule of thumb:** Max 3-4 levels deep + +### Context in Keys + +Include context when the same word has different meanings: + +```json +{ + "common.button.close": "Close", // Button text + "common.adjective.close": "Near", // Adjective + "store.status.open": "Open", // Store is open + "action.open": "Open file", // Action to open + "navigation.home": "Home", // Nav link + "address.home": "Home address" // Address type +} +``` + +## Performance Optimization + +### Lazy Loading + +Load translations only when needed using route-based loaders: + +```javascript +const config = { + loaders: [ + // Always load + { locale: 'en', key: 'common', loader: async () => (await import('./en/common.json')).default }, + + // Load on specific routes + { locale: 'en', key: 'admin', routes: [/^\/admin/], loader: async () => (await import('./en/admin.json')).default }, + ], +}; +``` + +**✅ Impact:** +- Reduces initial bundle size +- Faster page loads +- Better performance metrics + +### Preload Critical Routes + +Preload translations for likely next pages: + +```svelte + + + +``` + +### Cache Configuration + +Set appropriate cache duration for your use case: + +```javascript +// Production: Long cache (good performance) +const config = { + cache: 86400000, // 24 hours +}; + +// With CMS: Short cache (content updates frequently) +const config = { + cache: 300000, // 5 minutes +}; + +// Static content: Infinite cache +const config = { + cache: Number.POSITIVE_INFINITY, +}; +``` + +### Bundle Splitting + +Use dynamic imports for large translation files: + +```javascript +// ❌ Static import (always in bundle) +import translations from './large-translations.json'; + +// ✅ Dynamic import (lazy loaded) +loader: async () => (await import('./large-translations.json')).default +``` + +### Avoid Fallback Locale (If Possible) + +`fallbackLocale` doubles translation loading: + +```javascript +// ❌ Loads both 'cs' and 'en' translations +const config = { + fallbackLocale: 'en', +}; + +// ✅ Complete all translations (no fallback needed) +// Only loads current locale +``` + +Use fallback only during development or partial translations. + +## TypeScript Patterns + +### Type-Safe Configuration + +```typescript +import i18n, { type Config } from 'sveltekit-i18n'; + +const config: Config = { + loaders: [ + { + locale: 'en', + key: 'common', + loader: async () => (await import('./en/common.json')).default, + }, + ], +}; +``` + +### Translation Key Types (Custom Pattern) + +The library doesn't automatically infer types from translation files. If you need type-safe translation keys, you can manually define them: + +```typescript +// translations.d.ts +export interface TranslationKeys { + 'common.greeting': { name: string }; + 'common.items': { count: number }; + 'home.title': never; // No params +} + +// translations.ts +import type { TranslationKeys } from './translations.d'; + +export const { t } = new i18n(config); + +// You'll need to create a typed wrapper for strict checking: +// export function typedT( +// key: K, +// ...params: TranslationKeys[K] extends never ? [] : [TranslationKeys[K]] +// ): string { +// return t.get(key, ...(params as any)); +// } +``` + +**Note:** This requires manual maintenance. Consider generating types from your translation files using tools like `typesafe-i18n` if you need automatic type inference. + +### Typed Helpers + +Create typed helper functions: + +```typescript +import { t } from '$lib/translations'; + +export function formatPrice(amount: number, currency: string = 'USD'): string { + return t.get('common.price', { amount }, { currency }); +} + +export function pluralize(count: number, singular: string, plural: string): string { + return count === 1 ? singular : plural; +} +``` + +### Module Augmentation + +Extend types for better IDE support: + +```typescript +// app.d.ts +declare module '$lib/translations' { + export const t: import('svelte/store').Readable< + (key: string, vars?: Record, props?: any) => string + > & { + get: (key: string, vars?: Record, props?: any) => string; + }; + + export const locale: import('svelte/store').Writable & { + get: () => string; + }; + + // ... other exports +} +``` + +## SSR and CSR Considerations + +### Load in Layout + +Always load translations in `+layout.js` for SSR: + +```javascript +// src/routes/+layout.js +import { loadTranslations } from '$lib/translations'; + +export const load = async ({ url }) => { + const { pathname } = url; + const initLocale = 'en'; // Determine from cookie, header, etc. + + await loadTranslations(initLocale, pathname); + + return {}; +}; +``` + +**✅ Benefits:** +- Translations ready on server +- No flash of untranslated content (FOUC) +- SEO-friendly + +### Handle Loading State + +Show proper loading indicators: + +```svelte + + +{#if $loading} +
Loading...
+{:else} +

{$t('home.title')}

+

{$t('home.content')}

+{/if} +``` + +### Avoid Client-Only Code in SSR + +```javascript +import { browser } from '$app/environment'; +import { loadTranslations, locale } from '$lib/translations'; + +export const load = async ({ url }) => { + // Get saved locale (client only) + const savedLocale = browser ? localStorage.getItem('locale') : null; + const initLocale = savedLocale || 'en'; + + await loadTranslations(initLocale, url.pathname); + + return {}; +}; +``` + +### Serialize Minimal Data + +Don't serialize entire translation objects: + +```javascript +// ❌ Bad (sends all translations to client) +export const load = async () => { + return { + translations: getAllTranslations(), + }; +}; + +// ✅ Good (library handles serialization) +export const load = async ({ url }) => { + await loadTranslations('en', url.pathname); + return {}; // Library handles everything +}; +``` + +## Component-Scoped Translations + +### When to Use + +Use component-scoped translations for: +- Reusable components with their own text +- Third-party component wrappers +- Component libraries + +### Setup Pattern + +``` +src/lib/components/DataTable/ +├── DataTable.svelte +├── translations.js +└── translations/ + ├── en.json + └── cs.json +``` + +**translations.js:** + +```javascript +import i18n from 'sveltekit-i18n'; + +const config = { + loaders: [ + { + locale: 'en', + key: 'dataTable', + loader: async () => (await import('./translations/en.json')).default, + }, + { + locale: 'cs', + key: 'dataTable', + loader: async () => (await import('./translations/cs.json')).default, + }, + ], +}; + +export const { t: tDataTable, loadTranslations: loadDataTableTranslations } = new i18n(config); +``` + +**DataTable.svelte:** + +```svelte + + + + + + + + + + +
{$tDataTable('dataTable.column.name')}{$tDataTable('dataTable.column.date')}
+``` + +### Shared Global Locale + +Keep components in sync with app locale: + +```javascript +// lib/translations/index.js (main app) +export const { locale } = new i18n(appConfig); + +// lib/components/DataTable/translations.js (component) +import { locale as appLocale } from '$lib/translations'; + +export const { t: tDataTable } = new i18n(componentConfig); + +// Use app locale, don't create separate locale store +export { appLocale as locale }; +``` + +## Dynamic Routes and Locales + +### Locale in URL Path + +For SEO-friendly locale routing: + +``` +src/routes/[lang]/ +├── +layout.js +├── +layout.svelte +├── +page.svelte +└── about/ + └── +page.svelte +``` + +**hooks.server.js:** + +```javascript +export const handle = async ({ event, resolve }) => { + const lang = event.params.lang || 'en'; + + // Validate locale + const supportedLocales = ['en', 'cs', 'de']; + if (!supportedLocales.includes(lang)) { + return new Response('Not found', { status: 404 }); + } + + return resolve(event); +}; +``` + +**+layout.js:** + +```javascript +import { loadTranslations } from '$lib/translations'; + +export const load = async ({ params, url }) => { + const { lang } = params; + + await loadTranslations(lang, url.pathname); + + return { lang }; +}; +``` + +### Generate Locale Links + +Helper for creating locale-aware links: + +```typescript +// lib/utils/i18n.ts +export function localePath(path: string, locale: string): string { + return `/${locale}${path}`; +} + +// Usage +import { localePath } from '$lib/utils/i18n'; +import { locale } from '$lib/translations'; + +const aboutLink = localePath('/about', $locale); +// → '/en/about' or '/cs/about' +``` + +### Language Switcher for URLs + +```svelte + + +{#each $locales as loc} + + {loc.toUpperCase()} + +{/each} +``` + +## Content Management + +### CMS Integration + +Load translations from headless CMS: + +```javascript +const config = { + loaders: [ + { + locale: 'en', + key: 'content', + loader: async () => { + const response = await fetch('https://api.cms.com/translations/en'); + return await response.json(); + }, + }, + ], + cache: 300000, // 5 minutes (CMS content changes frequently) +}; +``` + +### Database Loading + +```javascript +import { db } from '$lib/server/database'; + +const config = { + loaders: [ + { + locale: 'en', + key: 'dynamic', + loader: async () => { + const translations = await db.translations.findOne({ + locale: 'en', + key: 'dynamic', + }); + return translations.data; + }, + }, + ], +}; +``` + +### Mixing Static and Dynamic + +```javascript +const config = { + loaders: [ + // Static (fast, versioned with code) + { + locale: 'en', + key: 'common', + loader: async () => (await import('./en/common.json')).default, + }, + + // Dynamic (CMS, updates without deployment) + { + locale: 'en', + key: 'content', + loader: async () => { + const res = await fetch('/api/translations/en/content'); + return await res.json(); + }, + }, + ], +}; +``` + +## Testing + +### Mock Translations + +```typescript +// tests/helpers.ts +import { writable } from 'svelte/store'; + +export function mockTranslations() { + return { + t: writable((key: string) => key), // Returns key as-is + locale: writable('en'), + locales: writable(['en', 'cs']), + loading: writable(false), + initialized: writable(true), + }; +} +``` + +**Usage:** + +```typescript +import { render } from '@testing-library/svelte'; +import { mockTranslations } from './helpers'; +import MyComponent from './MyComponent.svelte'; + +// Mock the translations module +vi.mock('$lib/translations', () => mockTranslations()); + +test('renders component', () => { + const { getByText } = render(MyComponent); + expect(getByText('home.title')).toBeInTheDocument(); // Key appears as-is +}); +``` + +### Test with Real Translations + +```typescript +import { render, waitFor } from '@testing-library/svelte'; +import { loadTranslations } from '$lib/translations'; + +test('renders translated content', async () => { + await loadTranslations('en', '/'); + + const { getByText } = render(MyComponent); + + await waitFor(() => { + expect(getByText('Welcome Home')).toBeInTheDocument(); + }); +}); +``` + +## Production Deployment + +### Environment-Specific Config + +```javascript +import { dev } from '$app/environment'; + +const config = { + cache: dev ? 60000 : 86400000, // 1 min in dev, 24 hrs in prod + log: { + level: dev ? 'debug' : 'warn', + }, +}; +``` + +### Error Handling + +```javascript +const config = { + loaders: [ + { + locale: 'en', + key: 'common', + loader: async () => { + try { + return (await import('./en/common.json')).default; + } catch (error) { + console.error('Failed to load translations:', error); + // Return minimal fallback + return { + 'error.generic': 'An error occurred', + }; + } + }, + }, + ], +}; +``` + +### Prerendering + +For static sites, ensure translations load during prerender: + +```javascript +// +page.js +export const prerender = true; + +export const load = async ({ url }) => { + // Translations will be baked into HTML + await loadTranslations('en', url.pathname); + return {}; +}; +``` + +### CDN Optimization + +Host translation files on CDN: + +```javascript +const config = { + loaders: [ + { + locale: 'en', + key: 'common', + loader: async () => { + const res = await fetch('https://cdn.example.com/translations/en/common.json'); + return await res.json(); + }, + }, + ], +}; +``` + +### Monitoring + +Track translation loading performance: + +```javascript +const config = { + loaders: [ + { + locale: 'en', + key: 'common', + loader: async () => { + const start = performance.now(); + const translations = (await import('./en/common.json')).default; + const duration = performance.now() - start; + + console.log(`Loaded translations in ${duration}ms`); + + // Send to monitoring service + if (typeof analytics !== 'undefined') { + analytics.track('translations_loaded', { locale: 'en', duration }); + } + + return translations; + }, + }, + ], +}; +``` + +## Summary + +**Key Takeaways:** + +1. **Organization** – Use clear directory structure and naming conventions +2. **Performance** – Lazy load route-specific translations +3. **TypeScript** – Leverage types for safety and IDE support +4. **SSR** – Load translations in layouts for proper server-side rendering +5. **Component-scoped** – Use for reusable components with their own text +6. **Testing** – Mock translations for fast tests, use real ones for integration +7. **Production** – Configure caching, error handling, and monitoring + +## See Also + +- [Getting Started](./GETTING_STARTED.md) – Basic setup tutorial +- [Architecture Overview](./ARCHITECTURE.md) – How it all works +- [API Documentation](./README.md) – Complete API reference +- [Troubleshooting](./TROUBLESHOOTING.md) – Common issues and solutions +- [Examples](../examples) – Working code examples + diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 0000000..a3bddb2 --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,530 @@ +# Getting Started with sveltekit-i18n + +This guide will walk you through adding internationalization to your SvelteKit application using `sveltekit-i18n`. We'll start with the simplest setup and gradually explore more advanced features. + +## Table of Contents + +- [Installation](#installation) +- [Basic Concepts](#basic-concepts) +- [Your First Multilingual App](#your-first-multilingual-app) +- [Route-based Loading](#route-based-loading) +- [Switching Locales](#switching-locales) +- [Next Steps](#next-steps) + +## Installation + +Install the package using your preferred package manager: + +```bash +npm install sveltekit-i18n +``` + +That's it! The package has zero external dependencies. + +## Basic Concepts + +Before we dive in, let's understand the key concepts: + +### Locales + +A **locale** is a language identifier (like `en`, `cs`, `de`). Your app can support multiple locales. + +### Translation Keys + +Translations are stored as key-value pairs. Keys use dot notation: + +```json +{ + "common.greeting": "Hello", + "common.farewell": "Goodbye", + "home.title": "Welcome to our app" +} +``` + +### Loaders + +**Loaders** define how and when to load translations. They can load: +- All translations at once +- Translations for specific routes only +- Translations on demand + +### Namespaces + +A **namespace** (the `key` in loaders) groups related translations together (like `common`, `home`, `about`). This helps organize translations and enables lazy loading. + +## Your First Multilingual App + +Let's create a simple multilingual application from scratch. + +### Step 1: Create Translation Files + +First, create your translation files. We'll support English and Czech: + +``` +src/lib/translations/ +├── en/ +│ └── common.json +├── cs/ +│ └── common.json +└── index.js +``` + +Create English translations: + +```json +// src/lib/translations/en/common.json +{ + "app.name": "My Application", + "greeting": "Hello, {{name}}!", + "nav.home": "Home", + "nav.about": "About", + "farewell": "Goodbye!" +} +``` + +Create Czech translations: + +```json +// src/lib/translations/cs/common.json +{ + "app.name": "Moje Aplikace", + "greeting": "Ahoj, {{name}}!", + "nav.home": "Domů", + "nav.about": "O nás", + "farewell": "Nashledanou!" +} +``` + +### Step 2: Configure i18n + +Create your i18n configuration file: + +```javascript +// src/lib/translations/index.js +import i18n from 'sveltekit-i18n'; + +/** @type {import('sveltekit-i18n').Config} */ +const config = { + loaders: [ + { + locale: 'en', + key: 'common', + loader: async () => (await import('./en/common.json')).default, + }, + { + locale: 'cs', + key: 'common', + loader: async () => (await import('./cs/common.json')).default, + }, + ], +}; + +export const { t, locale, locales, loading, loadTranslations } = new i18n(config); +``` + +**What's happening here:** +- We import `sveltekit-i18n` +- We configure loaders for each locale and namespace +- We export `t` (translation function), `locale` (current locale), and other utilities + +### Step 3: Load Translations in Layout + +Load translations in your root layout: + +```javascript +// src/routes/+layout.js +import { loadTranslations } from '$lib/translations'; + +/** @type {import('./$types').LayoutLoad} */ +export const load = async ({ url }) => { + const { pathname } = url; + + // Determine the initial locale (we'll improve this later) + const initLocale = 'en'; + + await loadTranslations(initLocale, pathname); + + return {}; +}; +``` + +**Why in the layout?** +- Layouts run before pages +- Ensures translations are ready before any page renders +- Works with SSR (Server-Side Rendering) + +### Step 4: Use Translations in Components + +Now you can use translations anywhere in your app: + +```svelte + + + +

{$t('app.name')}

+

{$t('greeting', { name: userName })}

+

Current locale: {$locale}

+ + +``` + +**Key points:** +- Use `$t()` to get translations (the `$` makes it reactive) +- Pass variables as an object: `$t('key', { variable: value })` +- Access current locale with `$locale` + +### Step 5: Test Your App + +Run your development server: + +```bash +npm run dev +``` + +Visit `http://localhost:5173` and you should see your translated content! + +## Route-based Loading + +For larger apps, you don't want to load all translations at once. Let's add route-specific translations. + +### Add Page-Specific Translations + +Create translations for specific pages: + +``` +src/lib/translations/ +├── en/ +│ ├── common.json +│ ├── home.json +│ └── about.json +├── cs/ +│ ├── common.json +│ ├── home.json +│ └── about.json +└── index.js +``` + +```json +// src/lib/translations/en/home.json +{ + "title": "Welcome Home", + "content": "This is the homepage content." +} +``` + +```json +// src/lib/translations/en/about.json +{ + "title": "About Us", + "content": "Learn more about our company." +} +``` + +### Update Configuration + +Add route-specific loaders: + +```javascript +// src/lib/translations/index.js +import i18n from 'sveltekit-i18n'; + +/** @type {import('sveltekit-i18n').Config} */ +const config = { + loaders: [ + // Common translations (loaded on every page) + { + locale: 'en', + key: 'common', + loader: async () => (await import('./en/common.json')).default, + }, + { + locale: 'cs', + key: 'common', + loader: async () => (await import('./cs/common.json')).default, + }, + + // Home page translations (loaded only on '/') + { + locale: 'en', + key: 'home', + routes: ['/'], + loader: async () => (await import('./en/home.json')).default, + }, + { + locale: 'cs', + key: 'home', + routes: ['/'], + loader: async () => (await import('./cs/home.json')).default, + }, + + // About page translations (loaded only on '/about') + { + locale: 'en', + key: 'about', + routes: ['/about'], + loader: async () => (await import('./en/about.json')).default, + }, + { + locale: 'cs', + key: 'about', + routes: ['/about'], + loader: async () => (await import('./cs/about.json')).default, + }, + ], +}; + +export const { t, locale, locales, loading, loadTranslations } = new i18n(config); +``` + +**Benefits:** +- `common.json` loads on every page (for navigation, etc.) +- `home.json` loads only when visiting `/` +- `about.json` loads only when visiting `/about` +- Reduces initial bundle size +- Translations load only once per route + +### Use Route-Specific Translations + +```svelte + + + +

{$t('home.title')}

+

{$t('home.content')}

+ + +``` + +```svelte + + + +

{$t('about.title')}

+

{$t('about.content')}

+``` + +## Switching Locales + +Let's add a language switcher to your app. + +### Create a Language Switcher Component + +```svelte + + + +
+ {#each $locales as loc} + + {/each} +
+ + +``` + +### Add to Your Layout + +```svelte + + + +
+ +
+ +{#if $loading} +

Loading translations...

+{:else} + +{/if} +``` + +**What happens when switching:** +1. User clicks a language button +2. `locale.set(newLocale)` is called +3. Library loads translations for the new locale (if not already loaded) +4. All `$t()` calls re-evaluate with new translations +5. UI updates automatically (Svelte reactivity!) + +### Persisting Locale Preference + +Save the user's language preference: + +```javascript +// src/routes/+layout.js +import { browser } from '$app/environment'; +import { loadTranslations, locale } from '$lib/translations'; + +/** @type {import('./$types').LayoutLoad} */ +export const load = async ({ url }) => { + const { pathname } = url; + + // Get locale from localStorage or default to 'en' + const savedLocale = browser ? localStorage.getItem('locale') : null; + const initLocale = savedLocale || 'en'; + + await loadTranslations(initLocale, pathname); + + return {}; +}; +``` + +```svelte + + + + +``` + +## Advanced Features Preview + +### Placeholders + +Use variables in your translations: + +```json +{ + "greeting": "Hello, {{name}}!", + "items": "You have {{count}} items." +} +``` + +```javascript +$t('greeting', { name: 'Alice' }) +$t('items', { count: 5 }) +``` + +### Modifiers + +Format numbers, dates, and more: + +```json +{ + "price": "Price: {{amount:currency;}}", + "updated": "Updated {{date:ago;}}" +} +``` + +```javascript +$t('price', { amount: 99.99 }, { currency: 'USD' }) +$t('updated', { date: Date.now() - 3600000 }) +// → "Updated 1 hour ago" +``` + +### Conditionals + +Show different text based on conditions: + +```json +{ + "items": "You have {{count}} {{count; 1:item; default:items;}}." +} +``` + +```javascript +$t('items', { count: 1 }) // → "You have 1 item." +$t('items', { count: 5 }) // → "You have 5 items." +``` + +Learn more in the [API Documentation](./README.md). + +## Next Steps + +Now that you have a working multilingual app, explore these topics: + +### 📚 Learn More + +- **[API Documentation](./README.md)** – Complete API reference +- **[Architecture Overview](./ARCHITECTURE.md)** – How everything works +- **[Best Practices](./BEST_PRACTICES.md)** – Recommended patterns and organization +- **[Troubleshooting](./TROUBLESHOOTING.md)** – Common issues and solutions + +### 🎨 Examples + +Check out working examples for specific use cases: + +- **[Multi-page app](../examples/multi-page)** – Route-based loading (you just built this!) +- **[Locale routing](../examples/locale-router)** – SEO-friendly URLs (`/en/about`, `/cs/about`) +- **[Component-scoped](../examples/component-scoped-ssr)** – Isolated translation contexts +- **[Fallback locales](../examples/fallback-locale)** – Handling missing translations +- **[All examples](../examples)** – Browse all examples + +### 🔧 Advanced Topics + +- **Custom parsers** – Use ICU message format or create your own +- **TypeScript** – Complete type definitions for configuration and API +- **Dynamic loading** – Load translations from APIs +- **SEO** – Locale-based routing for better SEO + +### 💡 Tips + +1. **Keep it simple** – Start with one namespace (`common`) and split later if needed +2. **Use route-based loading** – Only load what you need +3. **Common translations** – Keep navigation and shared UI text in a `common` namespace +4. **Consistent keys** – Use dot notation and clear naming (e.g., `page.section.item`) + +## Need Help? + +- **[Troubleshooting Guide](./TROUBLESHOOTING.md)** – Common issues +- **[GitHub Issues](https://github.com/sveltekit-i18n/lib/issues)** – Report bugs or ask questions +- **[Examples](../examples)** – Working code you can reference + +Happy translating! 🌍 + diff --git a/docs/INDEX.md b/docs/INDEX.md new file mode 100644 index 0000000..7088023 --- /dev/null +++ b/docs/INDEX.md @@ -0,0 +1,279 @@ +# Documentation Index + +Complete documentation for the `sveltekit-i18n` ecosystem. Whether you're just getting started or need detailed API references, you'll find everything here. + +## 🚀 Getting Started + +New to sveltekit-i18n? Start here: + +### [Getting Started Guide](./GETTING_STARTED.md) +**Time to complete: 15 minutes** + +Step-by-step tutorial that walks you through: +- Installing the library +- Creating your first translations +- Loading translations in your app +- Using translations in components +- Switching between languages + +Perfect for: First-time users, quick setup + +--- + +## 📚 Core Documentation + +### [Architecture Overview](./ARCHITECTURE.md) +**Understanding how everything works** + +Learn about: +- Package relationships (base, lib, parsers) +- Data flow (configuration → loading → translation) +- Loading strategies (route-based, SSR/CSR) +- When to use each package +- Performance considerations + +Perfect for: Understanding internals, making architecture decisions + +### [API Documentation](./README.md) +**Complete reference for sveltekit-i18n** + +Includes: +- All configuration options with examples +- Parser options (modifierDefaults, customModifiers) +- Instance properties and methods ($t, $locale, etc.) +- Message format syntax +- TypeScript usage + +Perfect for: Day-to-day development, looking up specific APIs + +### [@sveltekit-i18n/base API Documentation](https://github.com/sveltekit-i18n/base/tree/master/docs/README.md) +**API reference for base package** + +Detailed documentation for: +- Core configuration options +- Loaders (local files, APIs, databases) +- Preprocessing strategies +- All stores and methods +- Advanced use cases + +Perfect for: Using base package with custom parsers + +--- + +## ✨ Best Practices + +### [Best Practices Guide](./BEST_PRACTICES.md) +**Recommended patterns for production apps** + +Covers: +- **Organization** – File structure, naming conventions +- **Performance** – Lazy loading, caching, bundle optimization +- **TypeScript** – Typed configuration, custom type-safe patterns +- **SSR/CSR** – Server-side rendering considerations +- **Component-scoped** – Isolated translation contexts +- **Dynamic routes** – Locale-based routing patterns +- **Content management** – CMS and database integration +- **Testing** – Mocking translations, test strategies +- **Production** – Deployment, monitoring, error handling + +Perfect for: Building production applications, scaling your i18n implementation + +--- + +## 🔧 Troubleshooting + +### [Troubleshooting & FAQ](./TROUBLESHOOTING.md) +**Solutions to common problems** + +Includes: +- **Common Issues** with step-by-step solutions: + - Translations not loading + - Keys displayed instead of values + - Flashing content (FOUC) + - Route-based loading problems + - Locale not changing + - TypeScript errors + - SSR errors + - Performance issues +- **Debugging tips** – Inspect stores, test loaders, enable logging +- **FAQ** – 15+ frequently asked questions +- **Known limitations** – What to be aware of + +Perfect for: Fixing issues, understanding limitations + +--- + +## 🎨 Parser Documentation + +### [Parsers Overview](https://github.com/sveltekit-i18n/parsers) +**Available parsers and creating custom ones** + +Learn about: +- Choosing between parsers +- Parser comparison +- Creating custom parsers +- Parser integration with base + +### [@sveltekit-i18n/parser-default](https://github.com/sveltekit-i18n/parsers/tree/master/parser-default) +**Default parser with placeholders and modifiers** + +Features: +- Simple placeholder syntax: `{{name}}` +- Built-in modifiers: number, date, currency, ago +- Conditionals: `{{count; 1:item; default:items;}}` +- Comparison operators: eq, ne, lt, gt, lte, gte +- Custom modifiers +- No external dependencies + +### [@sveltekit-i18n/parser-icu](https://github.com/sveltekit-i18n/parsers/tree/master/parser-icu) +**ICU message format parser** + +Features: +- Industry-standard ICU syntax +- Advanced pluralization +- Select format (gender, etc.) +- Number/date/time formatting +- Comprehensive Intl support + +--- + +## 💡 Examples + +### [All Examples](../examples) +**Working code you can learn from** + +Browse examples by use case: + +#### Basic Examples +- **[Single Load](../examples/single-load)** – Load all translations at once +- **[One Page](../examples/one-page)** – Simple one-page app +- **[Multi Page](../examples/multi-page)** – Multiple routes (most common) + +#### Routing Examples +- **[Locale Parameter](../examples/locale-param)** – URL parameter (`?lang=en`) +- **[Locale Router](../examples/locale-router)** – Path-based (`/en/about`) +- **[Locale Router Advanced](../examples/locale-router-advanced)** – Default locale without prefix +- **[Locale Router Static](../examples/locale-router-static)** – Static adapter optimized + +#### Advanced Examples +- **[Component Scoped CSR](../examples/component-scoped-csr)** – Client-side component translations +- **[Component Scoped SSR](../examples/component-scoped-ssr)** – Server-side component translations +- **[Fallback Locale](../examples/fallback-locale)** – Handling missing translations + +#### Parser Examples +- **[Parser Default](../examples/parser-default)** – Default parser features +- **[Parser ICU](../examples/parser-icu)** – ICU message format + +#### Configuration Examples +- **[Loaders](../examples/loaders)** – Different loader configurations +- **[Preprocess](../examples/preprocess)** – Preprocessing strategies + +Each example includes: +- Complete working code +- Live demo on Netlify +- README with explanations + +--- + +## 📦 Package Documentation + +### Main Library +- **[sveltekit-i18n](../../README.md)** – Main library README +- **[Changelog](https://github.com/sveltekit-i18n/lib/releases)** – Version history + +### Core Package +- **[@sveltekit-i18n/base](../../../base/README.md)** – Base package README +- **[Base Changelog](https://github.com/sveltekit-i18n/base/releases)** – Version history + +### Parsers +- **[Parsers](../../../parsers/README.md)** – Parsers overview +- **[parser-default](../../../parsers/parser-default/README.md)** – Default parser +- **[parser-icu](../../../parsers/parser-icu/README.md)** – ICU parser +- **[Parser Changelogs](https://github.com/sveltekit-i18n/parsers/releases)** – Version history + +--- + +## 🎯 Quick Links by Task + +### I want to... + +#### Learn the basics +→ [Getting Started Guide](./GETTING_STARTED.md) + +#### Understand how it works +→ [Architecture Overview](./ARCHITECTURE.md) + +#### Look up an API +→ [API Documentation](./README.md) or [Base API Documentation](https://github.com/sveltekit-i18n/base/tree/master/docs/README.md) + +#### Build a production app +→ [Best Practices Guide](./BEST_PRACTICES.md) + +#### Fix an issue +→ [Troubleshooting Guide](./TROUBLESHOOTING.md) + +#### See working code +→ [Examples](../examples) + +#### Use a different parser +→ [Parsers Overview](https://github.com/sveltekit-i18n/parsers) + +#### Create locale-based URLs +→ [Locale Router Example](../examples/locale-router) or [Best Practices: Dynamic Routes](./BEST_PRACTICES.md#dynamic-routes-and-locales) + +#### Optimize performance +→ [Best Practices: Performance](./BEST_PRACTICES.md#performance-optimization) or [Architecture: Performance](./ARCHITECTURE.md#performance-considerations) + +#### Add TypeScript +→ [Best Practices: TypeScript](./BEST_PRACTICES.md#typescript-patterns) or [API Docs: TypeScript](./README.md#typescript) + +#### Load from API/database +→ [Best Practices: Content Management](./BEST_PRACTICES.md#content-management) or [Base API: Loaders](https://github.com/sveltekit-i18n/base/tree/master/docs/README.md#loader-required) + +#### Test my translations +→ [Best Practices: Testing](./BEST_PRACTICES.md#testing) + +#### Deploy to production +→ [Best Practices: Production](./BEST_PRACTICES.md#production-deployment) + +--- + +## 📖 Reading Order + +### For Beginners +1. [Getting Started Guide](./GETTING_STARTED.md) – Learn by building +2. [Examples](../examples) – See more use cases +3. [Best Practices](./BEST_PRACTICES.md) – Level up your implementation + +### For Advanced Users +1. [Architecture Overview](./ARCHITECTURE.md) – Understand the system +2. [Base API Documentation](https://github.com/sveltekit-i18n/base/tree/master/docs/README.md) – Deep dive into APIs +3. [Parsers](https://github.com/sveltekit-i18n/parsers) – Custom message formats + +### For Troubleshooting +1. [Troubleshooting Guide](./TROUBLESHOOTING.md) – Find your issue +2. [FAQ](./TROUBLESHOOTING.md#frequently-asked-questions) – Common questions +3. [GitHub Issues](https://github.com/sveltekit-i18n/lib/issues) – Get help + +--- + +## 🤝 Contributing + +Interested in contributing to sveltekit-i18n? + +- **[GitHub Repository](https://github.com/sveltekit-i18n/lib)** – Main library +- **[Issues](https://github.com/sveltekit-i18n/lib/issues)** – Report bugs, request features +- **[Discussions](https://github.com/sveltekit-i18n/lib/discussions)** – Ask questions, share ideas + +**Note:** We're currently looking for maintainers. If you're interested in helping maintain this project, please see [this issue](https://github.com/sveltekit-i18n/lib/issues/197). + +--- + +## 📄 License + +MIT License – See individual repositories for details. + +--- + +**Can't find what you're looking for?** Check the [Troubleshooting Guide](./TROUBLESHOOTING.md#getting-help) for how to get help. + diff --git a/docs/README.md b/docs/README.md index f6aa97a..69d7824 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,95 +1,740 @@ -# API Docs +# sveltekit-i18n API Documentation -[Config](#config)\ -[Properties and methods](#instance-properties-and-methods)\ -[Parsers](#parsers) +Complete API reference for `sveltekit-i18n` – the complete i18n solution for SvelteKit with `@sveltekit-i18n/parser-default` included. +## Table of Contents -## Config -### `translations`?: __[Translations.T](https://github.com/sveltekit-i18n/base/blob/master/src/types.ts)__ -This property defines translations, which should be in place before `loaders` will trigger. It's useful for synchronous translations (e.g. locally defined language names which are same for all language mutations). +- [Configuration](#configuration) +- [Parser Options](#parser-options) +- [Instance Properties and Methods](#instance-properties-and-methods) +- [Message Format](#message-format) +- [TypeScript](#typescript) +- [See Also](#see-also) -### `loaders`?: __[Loader.LoaderModule[]](https://github.com/sveltekit-i18n/base/blob/master/src/types.ts)__ -You can use `loaders` to define your asyncronous translation load. All loaded data are stored so loader is triggered only once – in case there is no previous version of the translation. It can get refreshed according to `config.cache`.\ -Each loader can include: +## Configuration -`locale`: __string__ – locale (e.g. `en`, `de`) which is this loader for. +`sveltekit-i18n` includes all configuration options from `@sveltekit-i18n/base`, plus specific options for the default parser. -`key`: __string__ – represents the translation namespace. This key is used as a translation prefix so it should be module-unique. You can access your translation later using `$t('key.yourTranslation')`. It shouldn't include `.` (dot) character. +```javascript +import i18n from 'sveltekit-i18n'; -`loader`:__() => Promise>__ – is a function returning a `Promise` with translation data. You can use it to load files locally, fetch it from your API etc... +/** @type {import('sveltekit-i18n').Config} */ +const config = { + loaders: [/* ... */], + parserOptions: {/* parser-default specific options */}, + // All @sveltekit-i18n/base options available +}; -`routes`?: __Array__ – can define routes this loader should be triggered for. You can use Regular expressions too. For example `[/\/.ome/]` will be triggered for `/home` and `/rome` route as well (but still only once). Leave this `undefined` in case you want to load this module with any route (useful for common translations). +export const { t, locale, locales, loading, loadTranslations } = new i18n(config); +``` + +### Inherited from @sveltekit-i18n/base + +The following options work exactly as in `@sveltekit-i18n/base`: + +- `loaders` – Define how and when translations load +- `translations` – Synchronous translations +- `preprocess` – Transform translation data +- `initLocale` – Initialize with specific locale +- `fallbackLocale` – Fallback when translation missing +- `fallbackValue` – Default value for missing keys +- `cache` – Server-side cache duration +- `log` – Logging configuration + +**📖 Full documentation:** [@sveltekit-i18n/base API docs](https://github.com/sveltekit-i18n/base/tree/master/docs/README.md) + +### Quick Example + +```javascript +import i18n from 'sveltekit-i18n'; + +/** @type {import('sveltekit-i18n').Config} */ +const config = { + // Common translations loaded on every page + loaders: [ + { + locale: 'en', + key: 'common', + loader: async () => (await import('./en/common.json')).default, + }, + { + locale: 'cs', + key: 'common', + loader: async () => (await import('./cs/common.json')).default, + }, + ], + + // Optional: Configure default parser + parserOptions: { + modifierDefaults: { + number: { minimumFractionDigits: 2 }, + }, + }, +}; + +export const { t, locale, locales, loading, loadTranslations } = new i18n(config); +``` + +## Parser Options + +`sveltekit-i18n` uses `@sveltekit-i18n/parser-default` which supports placeholders and modifiers. + +### `parserOptions.modifierDefaults` + +Configure default formatting options for built-in modifiers. + +```javascript +const config = { + parserOptions: { + modifierDefaults: { + number: { + // Intl.NumberFormat options + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }, + date: { + // Intl.DateTimeFormat options + dateStyle: 'medium', + }, + ago: { + // Intl.RelativeTimeFormat options + numeric: 'auto', + format: 'auto', // 'auto' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year' + }, + currency: { + // Intl.NumberFormat currency options + style: 'currency', + currency: 'USD', + ratio: 1, // Conversion ratio + }, + }, + }, +}; +``` + +#### Number Modifier Defaults + +Control how numbers are formatted: + +```javascript +const config = { + parserOptions: { + modifierDefaults: { + number: { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + }, + }, +}; +``` + +**Translation:** + +```json +{ + "price": "Price: {{value:number;}}" +} +``` + +**Usage:** + +```javascript +$t('price', { value: 99 }) +// → "Price: 99.00" (with defaults) +// Without defaults → "Price: 99" +``` + +**Override per translation:** + +```javascript +$t('price', { value: 99.999 }, { minimumFractionDigits: 3 }) +// → "Price: 99.999" +``` + +#### Date Modifier Defaults + +Control date formatting: + +```javascript +const config = { + parserOptions: { + modifierDefaults: { + date: { + dateStyle: 'full', // 'full' | 'long' | 'medium' | 'short' + timeStyle: 'short', // 'full' | 'long' | 'medium' | 'short' + // Or use individual options: + // year: 'numeric', + // month: 'long', + // day: 'numeric', + }, + }, + }, +}; +``` + +**Translation:** + +```json +{ + "published": "Published: {{date:date;}}" +} +``` + +**Usage:** + +```javascript +$t('published', { date: new Date('2024-01-15') }) +// → "Published: Monday, January 15, 2024" (with dateStyle: 'full') +``` + +#### Ago Modifier Defaults + +Control relative time formatting: + +```javascript +const config = { + parserOptions: { + modifierDefaults: { + ago: { + numeric: 'auto', // 'auto' | 'always' + format: 'auto', // 'auto' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year' + }, + }, + }, +}; +``` + +**Translation:** + +```json +{ + "updated": "Updated {{time:ago;}}" +} +``` + +**Usage:** + +```javascript +const oneHourAgo = Date.now() - 3600000; +$t('updated', { time: oneHourAgo }) +// → "Updated 1 hour ago" (with numeric: 'auto') +// → "Updated in 1 hour" (with numeric: 'always') +``` + +**Format option:** + +```javascript +// Auto-detect best unit +ago: { format: 'auto' } // Uses most appropriate unit + +// Force specific unit +ago: { format: 'day' } // Always show in days +``` + +#### Currency Modifier Defaults + +Control currency formatting: + +```javascript +const config = { + parserOptions: { + modifierDefaults: { + currency: { + style: 'currency', + currency: 'EUR', // Default currency + ratio: 1, // Exchange rate conversion + }, + }, + }, +}; +``` + +**Translation:** + +```json +{ + "total": "Total: {{amount:currency;}}" +} +``` + +**Usage:** + +```javascript +$t('total', { amount: 99.99 }) +// → "Total: €99.99" (with currency: 'EUR') +``` + +**With conversion ratio:** + +```javascript +const config = { + parserOptions: { + modifierDefaults: { + currency: { + currency: 'CZK', + ratio: 25, // 1 EUR = 25 CZK + }, + }, + }, +}; + +$t('total', { amount: 10 }) // 10 EUR +// → "Total: 250 CZK" (10 * 25) +``` + +**Override per translation:** + +```javascript +$t('total', { amount: 99.99 }, { currency: 'GBP' }) +// → "Total: £99.99" +``` + +### `parserOptions.customModifiers` + +Add your own modifiers for specialized formatting. + +```javascript +const config = { + parserOptions: { + customModifiers: { + // Modifier name → implementation + upper: ({ value }) => String(value).toUpperCase(), + + lower: ({ value }) => String(value).toLowerCase(), + + capitalize: ({ value }) => { + const str = String(value); + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); + }, + }, + }, +}; +``` + +**Translation:** + +```json +{ + "title": "{{text:upper;}}", + "subtitle": "{{text:capitalize;}}" +} +``` + +**Usage:** + +```javascript +$t('title', { text: 'hello world' }) +// → "HELLO WORLD" + +$t('subtitle', { text: 'hello world' }) +// → "Hello world" +``` + +#### Complex Custom Modifiers + +Custom modifiers receive these parameters: + +```javascript +const config = { + parserOptions: { + customModifiers: { + myModifier: ({ value, options, props, defaultValue, locale }) => { + // value: the placeholder value + // options: parsed options from translation + // props: additional props from $t() call + // defaultValue: default value if set + // locale: current locale + + return transformedValue; + }, + }, + }, +}; +``` + +**Example: Conditional modifier with options** + +```javascript +const config = { + parserOptions: { + customModifiers: { + status: ({ value, options, defaultValue }) => { + const option = options.find(opt => opt.key === value); + return option?.value || defaultValue || value; + }, + }, + }, +}; +``` + +**Translation:** + +```json +{ + "order": "Status: {{status:status; pending:⏳ Pending; shipped:📦 Shipped; delivered:✅ Delivered; default:❓ Unknown;}}" +} +``` + +**Usage:** + +```javascript +$t('order', { status: 'shipped' }) +// → "Status: 📦 Shipped" +``` + +**Example: Truncate with props** + +```javascript +const config = { + parserOptions: { + customModifiers: { + truncate: ({ value, props }) => { + const maxLength = props?.maxLength || 50; + const str = String(value); + return str.length > maxLength ? str.slice(0, maxLength) + '...' : str; + }, + }, + }, +}; +``` + +**Translation:** -### `preprocess`?: __'full' | 'preserveArrays' | 'none' | (input: Translations.Input) => any__ -Defines a preprocess strategy or a custom preprocess function. Preprocessor runs immediately after the translation data load. This is set to `'full'` by default. +```json +{ + "description": "{{text:truncate;}}" +} +``` + +**Usage:** + +```javascript +$t('description', { text: 'Very long text here...' }, { maxLength: 20 }) +// → "Very long text here..." +``` + +**Example: Format with locale awareness** + +```javascript +const config = { + parserOptions: { + customModifiers: { + phone: ({ value, locale }) => { + const number = String(value).replace(/\D/g, ''); + + if (locale === 'us' || locale === 'en') { + // US format: (123) 456-7890 + return `(${number.slice(0,3)}) ${number.slice(3,6)}-${number.slice(6)}`; + } else if (locale === 'cs' || locale === 'cz') { + // Czech format: +420 123 456 789 + return `+420 ${number.slice(0,3)} ${number.slice(3,6)} ${number.slice(6)}`; + } + + return value; + }, + }, + }, +}; +``` + +**Translation:** -Examples for input: ```json -{"a": {"b": [{"c": {"d": 1}}, {"c": {"d": 2}}]}} +{ + "contact": "Call us: {{phone:phone;}}" +} +``` + +**Usage:** + +```javascript +// In English (en) +$t('contact', { phone: '1234567890' }) +// → "Call us: (123) 456-7890" + +// In Czech (cs) +$t('contact', { phone: '123456789' }) +// → "Call us: +420 123 456 789" ``` -`'full'` (default) setting will result in: +## Instance Properties and Methods + +`sveltekit-i18n` instances include all properties and methods from `@sveltekit-i18n/base`: + +- **`t`** – Translation function +- **`l`** – Locale-specific translation function +- **`locale`** – Current locale (writable) +- **`locales`** – All available locales +- **`loading`** – Loading state +- **`initialized`** – Initialization state +- **`translations`** – All loaded translations +- **`loadTranslations`** – Load translations for locale and route +- **`setLocale`** – Change current locale +- **`setRoute`** – Update current route +- And more... + +**📖 Full API reference:** [@sveltekit-i18n/base API docs](https://github.com/sveltekit-i18n/base/tree/master/docs/README.md#instance-properties-and-methods) + +### Quick Reference + +```javascript +const { t, locale, locales, loading, loadTranslations } = new i18n(config); + +// Translation function +$t('home.title') // Simple +$t('greeting', { name: 'Alice' }) // With variables +$t('price', { value: 99 }, { currency: 'EUR' }) // With props + +// In .js files +t.get('home.title') + +// Locale management +$locale // Current locale +locale.set('cs') // Change locale +$locales // ['en', 'cs', ...] + +// Loading state +$loading // true/false +await loading.toPromise() // Wait for load + +// Load translations +await loadTranslations('en', pathname) // In +layout.js +``` + +## Message Format + +`sveltekit-i18n` uses `@sveltekit-i18n/parser-default` for message interpolation. + +### Placeholders + +Simple variable substitution: + ```json -{"a.b.0.c.d": 1, "a.b.1.c.d": 2} +{ + "greeting": "Hello, {{name}}!", + "message": "You have {{count}} notifications." +} +``` + +```javascript +$t('greeting', { name: 'Alice' }) // → "Hello, Alice!" +$t('message', { count: 5 }) // → "You have 5 notifications." ``` -`'preserveArrays'` in: +### Default Values + +Fallback when variable is missing: + ```json -{"a.b": [{"c.d": 1}, {"c.d": 2}]} +{ + "welcome": "Welcome, {{name; default:Guest;}}!" +} ``` -`'none'` (nothing's changed): +```javascript +$t('welcome', { name: 'Bob' }) // → "Welcome, Bob!" +$t('welcome', {}) // → "Welcome, Guest!" +``` + +### Modifiers + +Transform values before display: + +#### Number + ```json -{"a": {"b": [{"c": {"d": 1}}, {"c": {"d": 2}}]}} +{ + "population": "Population: {{count:number;}}" +} +``` + +```javascript +$t('population', { count: 1000000 }) +// → "Population: 1,000,000" ``` -Custom preprocess function `(input) => JSON.parse(JSON.stringify(input).replace('1', '"🦄"'))` will output: +#### Date ```json -{"a": {"b": [{"c": {"d": "🦄"}}, {"c": {"d": 2}}]}} +{ + "published": "Published: {{date:date;}}" +} ``` -### `initLocale`?: __string__ -If you set this property, translations will be initialized immediately using this locale. +```javascript +$t('published', { date: new Date() }) +// → "Published: Jan 15, 2024" +``` + +#### Ago (Relative Time) + +```json +{ + "updated": "Updated {{time:ago;}}" +} +``` + +```javascript +$t('updated', { time: Date.now() - 3600000 }) +// → "Updated 1 hour ago" +``` + +#### Currency + +```json +{ + "price": "Price: {{amount:currency;}}" +} +``` + +```javascript +$t('price', { amount: 99.99 }, { currency: 'USD' }) +// → "Price: $99.99" +``` + +### Conditionals + +Show different text based on values: + +#### Simple Conditions + +```json +{ + "items": "You have {{count}} {{count; 1:item; default:items;}}." +} +``` -### `fallbackLocale`?: __string__ -If you set this property, translations are automatically loaded not for current `$locale` only, but for this locale as well. In case there is no translation for current `$locale`, fallback locale translation is used instead of translation key placeholder. This is also used as a fallback when unknown locale is set. +```javascript +$t('items', { count: 1 }) // → "You have 1 item." +$t('items', { count: 5 }) // → "You have 5 items." +``` + +#### Comparison Operators + +Available: `eq`, `ne`, `lt`, `lte`, `gt`, `gte` -Note that it's not recommended to use this property if you don't really need it. It may affect your data load. +```json +{ + "stock": "{{count:gt; 0:In stock ({{count}}); default:Out of stock;}}", + "age": "{{age:gte; 18:Adult; default:Minor;}}", + "status": "{{value:eq; active:✓ Active; inactive:✗ Inactive; default:Unknown;}}" +} +``` -### `fallbackValue`?: __any__ -By default, translation key is returned in case no translation is found for given translation key. For example, `$t('unknown.key')` will result in `'unknown.key'` output. You can set this output value using this config prop. +```javascript +$t('stock', { count: 5 }) // → "In stock (5)" +$t('stock', { count: 0 }) // → "Out of stock" +$t('age', { age: 25 }) // → "Adult" +$t('age', { age: 15 }) // → "Minor" +``` -### `cache`?: __number__ -When you are serving your app, translations are loaded only once on server. This property allows you to setup a refresh period in milliseconds when your translations are refetched on the server. The default value is `86400000` (24 hours). +### Nested Placeholders -Tip: You can set to `Number.POSITIVE_INFINITY` to disable server-side refreshing. +Combine placeholders and modifiers: -### `log.level`?: __'error' | 'warn' | 'debug'__ -You can manage log level using this property (default: `'warn'`). +```json +{ + "notification": "You have {{count:gt; 0:{{count}} new {{count; 1:message; default:messages;}}!; default:no messages.;}}" +} +``` -### `log.prefix`?: __string__ -You can prefix output logs using this property (default: `'[i18n]: '`). +```javascript +$t('notification', { count: 1 }) +// → "You have 1 new message!" -### `log.logger`?: __[Logger.T](https://github.com/sveltekit-i18n/base/blob/master/src/types.ts)__ -You can setup your custom logger using this property (default: `console`). +$t('notification', { count: 5 }) +// → "You have 5 new messages!" -### `parserOptions`?: __[Parser.Options](https://github.com/sveltekit-i18n/parsers/blob/master/parser-default/src/types.ts)__ -This property includes configuration related to `@sveltekit-i18n/parser-default`. +$t('notification', { count: 0 }) +// → "You have no messages." +``` -Read more about `parserOptions` [here](https://github.com/sveltekit-i18n/parsers/tree/master/parser-default#options). +**📖 Complete syntax guide:** [parser-default documentation](https://github.com/sveltekit-i18n/parsers/tree/master/parser-default#readme) + +## TypeScript + +Full TypeScript support with complete type definitions: + +```typescript +import i18n, { type Config } from 'sveltekit-i18n'; + +const config: Config = { + loaders: [ + { + locale: 'en', + key: 'common', + loader: async () => (await import('./en/common.json')).default, + }, + ], + parserOptions: { + modifierDefaults: { + number: { + minimumFractionDigits: 2, + }, + }, + }, +}; + +export const { t, locale, locales, loading, loadTranslations } = new i18n(config); +``` + +**What's Included:** +- ✅ Type definitions for all configuration options +- ✅ Typed API methods and stores +- ✅ Generic types for custom parsers +- ❌ Automatic inference of translation keys (not built-in) + +### Type-Safe Translations (Custom Pattern) + +The library doesn't automatically infer translation keys from JSON files, but you can implement your own type-safe pattern: + +```typescript +// translations.d.ts - Define your translation key types +export interface TranslationParams { + 'common.greeting': { name: string }; + 'common.items': { count: number }; + 'home.title': never; // No parameters +} + +// translations.ts - Create typed wrapper (optional) +import i18n, { type Config } from 'sveltekit-i18n'; + +const config: Config = { + // ... configuration +}; + +export const { t, locale, locales, loading, loadTranslations } = new i18n(config); + +// For stricter typing, you can create a wrapper: +// export function typedT( +// key: K, +// ...params: TranslationParams[K] extends never ? [] : [TranslationParams[K]] +// ): string { +// return t.get(key, ...(params as any)); +// } +``` +This pattern provides compile-time checking but requires manual maintenance of the `TranslationParams` interface. -## Instance properties and methods +## See Also -Each `sveltekit-i18n` instance includes all `@sveltekit-i18n/base` properties and methods described [here](https://github.com/sveltekit-i18n/base/blob/master/docs#instance-methods-and-properties). +### Documentation +- **[Getting Started](./GETTING_STARTED.md)** – Step-by-step tutorial +- **[Architecture Overview](./ARCHITECTURE.md)** – How everything works +- **[Best Practices](./BEST_PRACTICES.md)** – Recommended patterns +- **[Troubleshooting](./TROUBLESHOOTING.md)** – Common issues and solutions -## Parsers +### Related Packages -`sveltekit-i18n` library uses `@sveltekit-i18n/parser-default` by default. You can find more [here](https://github.com/sveltekit-i18n/parsers/tree/master/parser-default#readme), including the message format syntax. +- **[@sveltekit-i18n/base](https://github.com/sveltekit-i18n/base)** – Core functionality (for custom parsers) +- **[@sveltekit-i18n/parser-default](https://github.com/sveltekit-i18n/parsers/tree/master/parser-default)** – Default parser (included) +- **[@sveltekit-i18n/parser-icu](https://github.com/sveltekit-i18n/parsers/tree/master/parser-icu)** – ICU message format parser +- **[Parsers Overview](https://github.com/sveltekit-i18n/parsers)** – All available parsers -In case `@sveltekit-i18n/parser-default` syntax does not fit your needs, feel free to use standalone `@sveltekit-i18n/base` together with a [parser of your choice](https://github.com/sveltekit-i18n/parsers#readme) (or create your own!). +### Examples -See parsers in [Examples](https://github.com/sveltekit-i18n/lib/tree/master/examples#parsers) for more information. +- **[Multi-page App](../examples/multi-page)** – Most common setup +- **[Locale Routing](../examples/locale-router)** – SEO-friendly URLs +- **[All Examples](../examples)** – Complete list with live demos diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..edd61f6 --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,946 @@ +# Troubleshooting & FAQ + +This guide helps you diagnose and fix common issues when using `sveltekit-i18n`. If you don't find your issue here, check [GitHub Issues](https://github.com/sveltekit-i18n/lib/issues) or create a new one. + +## Table of Contents + +- [Common Issues](#common-issues) + - [Translations Not Loading](#translations-not-loading) + - [Translation Keys Displayed Instead of Values](#translation-keys-displayed-instead-of-values) + - [Translations Flash/Change After Page Load](#translations-flashchange-after-page-load) + - [Route-Based Loading Not Working](#route-based-loading-not-working) + - [Locale Not Changing](#locale-not-changing) + - [TypeScript Errors](#typescript-errors) + - [SSR Errors](#ssr-errors) + - [Performance Issues](#performance-issues) +- [Debugging Tips](#debugging-tips) +- [Frequently Asked Questions](#frequently-asked-questions) +- [Known Limitations](#known-limitations) +- [Getting Help](#getting-help) + +## Common Issues + +### Translations Not Loading + +**Symptoms:** +- Translation keys appear instead of values +- `$loading` is always `true` +- Console shows no errors + +**Possible Causes & Solutions:** + +#### 1. Forgot to call `loadTranslations` + +**Problem:** + +```javascript +// ❌ +layout.js +export const load = async ({ url }) => { + // Missing loadTranslations call + return {}; +}; +``` + +**Solution:** + +```javascript +// ✅ +layout.js +import { loadTranslations } from '$lib/translations'; + +export const load = async ({ url }) => { + await loadTranslations('en', url.pathname); + return {}; +}; +``` + +#### 2. Loader function not returning data + +**Problem:** + +```javascript +// ❌ Loader doesn't return anything +{ + locale: 'en', + key: 'common', + loader: async () => { + await import('./en/common.json'); // Missing .default and return + }, +} +``` + +**Solution:** + +```javascript +// ✅ Return the translation data +{ + locale: 'en', + key: 'common', + loader: async () => (await import('./en/common.json')).default, +} +``` + +#### 3. Import path is wrong + +**Problem:** + +```javascript +// ❌ Wrong path +loader: async () => (await import('./translations/en.json')).default +// File is actually at ./en/common.json +``` + +**Solution:** +- Check file path relative to your configuration file +- Use absolute imports if needed: `import('$lib/translations/en/common.json')` + +#### 4. JSON syntax error + +**Problem:** + +```json +// ❌ Invalid JSON (trailing comma) +{ + "greeting": "Hello", +} +``` + +**Solution:** + +```json +// ✅ Valid JSON +{ + "greeting": "Hello" +} +``` + +**Debugging:** +- Check browser console for import errors +- Try importing the JSON file directly in a test component + +--- + +### Translation Keys Displayed Instead of Values + +**Symptoms:** +- See `"home.title"` instead of `"Welcome"` +- Translations look correct in JSON files + +**Possible Causes & Solutions:** + +#### 1. Wrong translation key + +**Problem:** + +```svelte + +

{$t('home.titel')}

+``` + +```json +{ + "home.title": "Welcome" +} +``` + +**Solution:** +- Check spelling: `titel` → `title` +- Check key exists in translations +- Use `$translations` store to inspect loaded translations: + +```svelte +
{JSON.stringify($translations, null, 2)}
+``` + +#### 2. Namespace not included in key + +**Problem:** + +```javascript +// Loader configuration +{ + locale: 'en', + key: 'home', // ← Namespace + loader: async () => (await import('./en/home.json')).default, +} +``` + +```json +// en/home.json +{ + "title": "Welcome" +} +``` + +```svelte + +

{$t('title')}

+``` + +**Solution:** + +```svelte + +

{$t('home.title')}

+``` + +#### 3. Translation not loaded for current route + +**Problem:** + +```javascript +// home translations only load on '/' +{ + locale: 'en', + key: 'home', + routes: ['/'], + loader: async () => (await import('./en/home.json')).default, +} +``` + +```svelte + +

{$t('home.title')}

+``` + +**Solution:** +- Remove `routes` to load on all pages, or +- Add route to the list, or +- Move to `common` namespace + +#### 4. Preprocessing issue + +**Problem with preprocess: 'none':** + +```javascript +const config = { + preprocess: 'none', // No flattening +}; +``` + +```json +{ + "home": { + "title": "Welcome" + } +} +``` + +```svelte + +

{$t('home.title')}

+``` + +**Solution:** +- Use default preprocessing (`'full'`), or +- Access nested structure: `$t('home').title` + +--- + +### Translations Flash/Change After Page Load + +**Symptoms:** +- Translation keys briefly visible before correct translations appear +- Content "pops" or changes after page loads + +**Cause:** +Translations loading on client-side instead of server-side + +**Solution:** + +#### 1. Load in +layout.js (not +layout.svelte) + +**❌ Wrong (client-side only):** + +```svelte + + +``` + +**✅ Correct (server and client):** + +```javascript +// +layout.js +import { loadTranslations } from '$lib/translations'; + +export const load = async ({ url }) => { + await loadTranslations('en', url.pathname); // Runs on server + return {}; +}; +``` + +#### 2. Show loading state + +If client-side loading is necessary: + +```svelte + + +{#if $loading} +
Loading...
+{:else} +

{$t('home.title')}

+{/if} +``` + +--- + +### Route-Based Loading Not Working + +**Symptoms:** +- Translations always load regardless of route +- Or never load even on matching routes + +**Possible Causes & Solutions:** + +#### 1. Routes array is empty + +**Problem:** + +```javascript +{ + locale: 'en', + key: 'home', + routes: [], // ❌ Empty array + loader: async () => (await import('./en/home.json')).default, +} +``` + +**Solution:** + +```javascript +routes: ['/'] // ✅ Specify routes, or omit for all routes +``` + +#### 2. Regex not matching + +**Problem:** + +```javascript +routes: [/^products/] // ❌ Matches '/products' but not '/products/123' +``` + +**Solution:** + +```javascript +routes: [/^\/products/] // ✅ Note the \/ at start +``` + +Test your regex: + +```javascript +const pattern = /^\/products/; +console.log(pattern.test('/products')); // true +console.log(pattern.test('/products/123')); // true +console.log(pattern.test('/about')); // false +``` + +#### 3. Pathname includes locale prefix + +**Problem:** + +```javascript +// URL: /en/products +routes: ['/products'] // ❌ Doesn't match '/en/products' +``` + +**Solution:** + +```javascript +// Option 1: Include locale in route pattern +routes: ['/en/products', '/cs/products'] + +// Option 2: Use regex +routes: [/^\/[a-z]{2}\/products/] + +// Option 3: Strip locale before passing to loadTranslations +const pathname = url.pathname.replace(/^\/[a-z]{2}/, ''); +await loadTranslations(locale, pathname); +``` + +--- + +### Locale Not Changing + +**Symptoms:** +- `locale.set('cs')` doesn't change displayed translations +- Locale store updates but translations stay the same + +**Possible Causes & Solutions:** + +#### 1. Translations not loaded for new locale + +**Problem:** +Only English translations defined: + +```javascript +const config = { + loaders: [ + { locale: 'en', key: 'common', loader: async () => (await import('./en/common.json')).default }, + // ❌ Missing 'cs' loaders + ], +}; +``` + +**Solution:** +Add loaders for all locales: + +```javascript +const config = { + loaders: [ + { locale: 'en', key: 'common', loader: async () => (await import('./en/common.json')).default }, + { locale: 'cs', key: 'common', loader: async () => (await import('./cs/common.json')).default }, // ✅ + ], +}; +``` + +#### 2. Using `l` instead of `t` + +**Problem:** + +```svelte + +

{$l('en', 'home.title')}

+``` + +**Solution:** + +```svelte + +

{$t('home.title')}

+``` + +#### 3. Loader error + +Check console for errors when locale changes. Translation loading might be failing silently. + +--- + +### TypeScript Errors + +#### Type 'Config' is not generic + +**Error:** + +```typescript +const config: Config = { // ❌ Error here + // ... +}; +``` + +**Solution:** + +```typescript +import type { Config } from 'sveltekit-i18n'; + +const config: Config = { // ✅ Don't use generic for main lib + // ... +}; +``` + +For `@sveltekit-i18n/base`: + +```typescript +import type { Config } from '@sveltekit-i18n/base'; +import type { Config as ParserConfig } from '@sveltekit-i18n/parser-default'; + +const config: Config = { // ✅ Generic for base + // ... +}; +``` + +#### Cannot find module '$lib/translations' + +**Error:** + +```typescript +import { t } from '$lib/translations'; // ❌ Cannot find module +``` + +**Solution:** + +Check `svelte.config.js` has correct alias: + +```javascript +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { + preprocess: vitePreprocess(), + kit: { + alias: { + $lib: 'src/lib', // ✅ Ensure this is set + }, + }, +}; +``` + +--- + +### SSR Errors + +#### Error: window is not defined + +**Problem:** + +```javascript +// +layout.js +import { browser } from '$app/environment'; + +export const load = async () => { + const locale = localStorage.getItem('locale'); // ❌ Fails on server + // ... +}; +``` + +**Solution:** + +```javascript +import { browser } from '$app/environment'; + +export const load = async () => { + const locale = browser ? localStorage.getItem('locale') : null; // ✅ + // ... +}; +``` + +#### Translations not hydrating properly + +**Problem:** +Translations work on server but not on client after hydration. + +**Solution:** +Ensure `loadTranslations` is called in `+layout.js` (not `.server.js`): + +```javascript +// ✅ +layout.js (runs on both server and client) +export const load = async ({ url }) => { + await loadTranslations('en', url.pathname); + return {}; +}; +``` + +--- + +### Performance Issues + +#### Slow initial page load + +**Causes:** +1. Loading too many translations at once +2. Large translation files +3. No route-based loading + +**Solutions:** + +**1. Implement route-based loading:** + +```javascript +// ❌ Before: Everything loads always +{ locale: 'en', key: 'everything', loader: async () => (await import('./en/everything.json')).default } + +// ✅ After: Split by route +{ locale: 'en', key: 'common', loader: async () => (await import('./en/common.json')).default }, +{ locale: 'en', key: 'home', routes: ['/'], loader: async () => (await import('./en/home.json')).default }, +{ locale: 'en', key: 'products', routes: [/^\/products/], loader: async () => (await import('./en/products.json')).default }, +``` + +**2. Split large files:** + +``` +// ❌ Before +en/ + all.json (500 KB) + +// ✅ After +en/ + common.json (5 KB) + home.json (20 KB) + products.json (30 KB) + admin.json (50 KB) +``` + +**3. Measure and monitor:** + +```javascript +{ + locale: 'en', + key: 'common', + loader: async () => { + const start = performance.now(); + const data = (await import('./en/common.json')).default; + console.log(`Loaded in ${performance.now() - start}ms`); + return data; + }, +} +``` + +#### Memory issues (server) + +**Cause:** +Translations cached forever + +**Solution:** +Configure cache refresh: + +```javascript +const config = { + cache: 3600000, // 1 hour (instead of default 24 hours) +}; +``` + +--- + +## Debugging Tips + +### 1. Inspect Loaded Translations + +```svelte + + +
+ Debug: Loaded Translations +
{JSON.stringify($translations, null, 2)}
+
+ +

Current locale: {$locale}

+``` + +### 2. Check Loading State + +```svelte + + +

Loading: {$loading}

+

Initialized: {$initialized}

+``` + +### 3. Test Loaders Directly + +```javascript +// Test in browser console or separate script +const loader = async () => (await import('$lib/translations/en/common.json')).default; +const data = await loader(); +console.log(data); +``` + +### 4. Enable Debug Logging + +```javascript +const config = { + log: { + level: 'debug', // See all operations + }, +}; +``` + +### 5. Check Available Locales + +```svelte + + +

Available locales: {$locales.join(', ')}

+``` + +### 6. Verify Route Matching + +```javascript +// In +layout.js +export const load = async ({ url }) => { + console.log('Current pathname:', url.pathname); + await loadTranslations('en', url.pathname); + return {}; +}; +``` + +--- + +## Frequently Asked Questions + +### Can I use this library without SvelteKit? + +`sveltekit-i18n` is designed specifically for SvelteKit. For plain Svelte apps, consider: +- [svelte-i18n](https://github.com/kaisermann/svelte-i18n) +- [svelte-intl-precompile](https://github.com/cibernox/svelte-intl-precompile) + +### Can I use HTML in translations? + +No, translations are rendered as text. For HTML content, use `@html`: + +```svelte +

{@html $t('content.with.html')}

+``` + +**⚠️ Security Warning:** Only use `@html` with trusted content. Never use it with user-generated content. + +### How do I handle plurals? + +Use the default parser's conditional syntax: + +```json +{ + "items": "You have {{count}} {{count; 1:item; default:items;}}." +} +``` + +Or use ICU parser for advanced pluralization: + +```json +{ + "items": "You have {count, plural, =0 {no items} one {# item} other {# items}}." +} +``` + +### Can I change parser after initialization? + +Not recommended. Create a new instance instead: + +```javascript +const { t: tDefault } = new i18n(configWithDefaultParser); +const { t: tICU } = new i18n(configWithICUParser); +``` + +### How do I handle right-to-left (RTL) languages? + +The library doesn't handle RTL directly. Manage it in your app: + +```svelte + + +
+

{$t('content')}

+
+``` + +### Can I nest translation calls? + +No, `$t()` returns a string, not a translation key. Pre-compose your translations: + +```javascript +// ❌ Won't work +$t($t('dynamic.key')) + +// ✅ Use variables +const key = someCondition ? 'key1' : 'key2'; +$t(key) +``` + +### How do I translate dynamic content? + +Use placeholders: + +```json +{ + "welcome": "Welcome, {{name}}!", + "error": "{{field}} is required." +} +``` + +```javascript +$t('welcome', { name: userName }) +$t('error', { field: $t('form.field.email') }) +``` + +### Can I load translations from a database? + +Yes! Use async loader: + +```javascript +{ + locale: 'en', + key: 'dynamic', + loader: async () => { + const res = await fetch('/api/translations/en'); + return await res.json(); + }, +} +``` + +### How do I handle missing translations during development? + +Use `fallbackLocale` temporarily: + +```javascript +const config = { + fallbackLocale: 'en', // Fall back to English +}; +``` + +Or use a custom `fallbackValue`: + +```javascript +const config = { + fallbackValue: '🚧', // Show construction emoji +}; +``` + +### Does this work with SvelteKit adapters? + +Yes! Works with all adapters: +- `@sveltejs/adapter-auto` +- `@sveltejs/adapter-node` +- `@sveltejs/adapter-static` +- `@sveltejs/adapter-vercel` +- `@sveltejs/adapter-netlify` +- And others + +### How do I translate meta tags (SEO)? + +In `+page.js` or `+page.server.js`: + +```javascript +import { t } from '$lib/translations'; + +export const load = async () => { + return { + meta: { + title: t.get('page.title'), + description: t.get('page.description'), + }, + }; +}; +``` + +In `+page.svelte`: + +```svelte + + + + {data.meta.title} + + +``` + +--- + +## Known Limitations + +### 1. Cannot use dots in namespace keys + +```javascript +// ❌ Won't work correctly +{ key: 'pages.home' } + +// ✅ Use different separator or remove +{ key: 'pages_home' } +{ key: 'home' } +``` + +### 2. Translation keys are case-sensitive + +```json +{ + "Greeting": "Hello" +} +``` + +```javascript +$t('greeting') // ❌ Won't find 'Greeting' +$t('Greeting') // ✅ Correct +``` + +### 3. Locale identifiers must be lowercase + +```javascript +locale.set('EN') // ❌ Converts to 'en' +locale.set('en') // ✅ Correct +``` + +### 4. No automatic locale detection + +You must explicitly set the locale: + +```javascript +// Detect from browser +const browserLang = navigator.language.split('-')[0]; + +// Detect from URL +const urlLang = url.pathname.split('/')[1]; + +// Set locale +await loadTranslations(browserLang, url.pathname); +``` + +--- + +## Getting Help + +### Before Asking for Help + +1. **Check this guide** – Most issues are covered here +2. **Search existing issues** – Someone might have had the same problem +3. **Enable debug logging** – `log: { level: 'debug' }` +4. **Create a minimal reproduction** – Isolate the problem + +### Where to Get Help + +1. **[GitHub Issues](https://github.com/sveltekit-i18n/lib/issues)** – Bug reports and feature requests +2. **[GitHub Discussions](https://github.com/sveltekit-i18n/lib/discussions)** – Questions and community help +3. **[Examples](../examples)** – Working code you can reference + +### Creating a Good Issue + +Include: +- **SvelteKit version** (`npm list @sveltejs/kit`) +- **sveltekit-i18n version** (`npm list sveltekit-i18n`) +- **Minimal reproduction** (CodeSandbox, StackBlitz, or GitHub repo) +- **Expected behavior** vs **actual behavior** +- **Error messages** (full stack trace) +- **Configuration** (anonymized if needed) + +**Example:** + +```markdown +## Bug Report + +### Environment +- SvelteKit: 2.0.0 +- sveltekit-i18n: 2.4.2 +- Node: 20.10.0 + +### Issue +Translations flash on page load + +### Reproduction +https://stackblitz.com/edit/... + +### Expected +Translations should be ready on initial render + +### Actual +Keys briefly visible before translations load + +### Configuration +... paste your config ... +``` + +--- + +## See Also + +- [Getting Started](./GETTING_STARTED.md) – Setup tutorial +- [API Documentation](./README.md) – Complete API reference +- [Best Practices](./BEST_PRACTICES.md) – Recommended patterns +- [Architecture](./ARCHITECTURE.md) – How it works internally +- [Examples](../examples) – Working code examples + From 4ff8c89a14f7631edbe8ae43e154c4de56f19b35 Mon Sep 17 00:00:00 2001 From: Jarda Svoboda <20700514+jarda-svoboda@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:21:57 +0100 Subject: [PATCH 2/3] Add PR template --- .github/pull_request_template.md | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..91d9f0b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,43 @@ +## What + + + +## Why + + + +Fixes # +Relates to # + +## How + + + +## Testing + + + +## Checklist + +Before submitting this PR, please ensure: + +- [ ] **Tests pass locally** (`npm test`) +- [ ] **Linter passes** (`npm run lint`) +- [ ] **Build succeeds** (`npm run build`) +- [ ] **All commits are atomic** (each commit works independently) +- [ ] **Commits have clear messages** (imperative mood, descriptive) +- [ ] **Branch rebased on latest master** (`git rebase origin/master`) +- [ ] **Documentation updated** (if API or behavior changed) +- [ ] **Examples updated** (if adding new features) + +## Additional Notes + + + +--- + + + From 80fcd2a067c2ed225d8a1f0c2c9439ee02d98981 Mon Sep 17 00:00:00 2001 From: Jarda Svoboda <20700514+jarda-svoboda@users.noreply.github.com> Date: Sun, 30 Nov 2025 17:07:06 +0100 Subject: [PATCH 3/3] Add issue templates --- .github/ISSUE_TEMPLATE/bug_report.yml | 120 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 15 +++ .github/ISSUE_TEMPLATE/documentation.yml | 69 ++++++++++++ .github/ISSUE_TEMPLATE/feature_request.yml | 92 ++++++++++++++++ 4 files changed, 296 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..676b22c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,120 @@ +name: Bug Report +description: Report a bug or unexpected behavior +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug! Please fill out the information below to help us investigate. + + - type: textarea + id: description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + placeholder: What went wrong? + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Steps to reproduce the behavior, or a link to a minimal reproduction (CodeSandbox, StackBlitz, GitHub repo). + placeholder: | + 1. Configure i18n with... + 2. Navigate to... + 3. See error... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What did you expect to happen? + placeholder: What should have happened? + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual behavior + description: What actually happened? + placeholder: What actually happened? + validations: + required: true + + - type: input + id: sveltekit-i18n-version + attributes: + label: sveltekit-i18n version + description: What version of sveltekit-i18n are you using? + placeholder: "2.4.2" + validations: + required: true + + - type: input + id: sveltekit-version + attributes: + label: SvelteKit version + description: What version of SvelteKit are you using? + placeholder: "2.0.0" + validations: + required: true + + - type: input + id: node-version + attributes: + label: Node.js version + description: What version of Node.js are you using? + placeholder: "20.10.0" + validations: + required: true + + - type: dropdown + id: adapter + attributes: + label: SvelteKit Adapter + description: Which SvelteKit adapter are you using? + options: + - "@sveltejs/adapter-auto" + - "@sveltejs/adapter-node" + - "@sveltejs/adapter-static" + - "@sveltejs/adapter-vercel" + - "@sveltejs/adapter-netlify" + - "@sveltejs/adapter-cloudflare" + - "Other (specify in additional context)" + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Error logs + description: Please paste any relevant error messages or console logs. + render: shell + placeholder: Paste error logs here + + - type: textarea + id: additional + attributes: + label: Additional context + description: Add any other context about the problem here (browser, operating system, etc.). + placeholder: Any additional information that might help + + - type: checkboxes + id: checklist + attributes: + label: Checklist + description: Please confirm the following + options: + - label: I have searched existing issues to avoid duplicates + required: true + - label: I have provided a minimal reproduction (if possible) + required: false + - label: I have checked the [Troubleshooting Guide](https://github.com/sveltekit-i18n/lib/blob/master/docs/TROUBLESHOOTING.md) + required: false + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..aebcf96 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,15 @@ +blank_issues_enabled: false +contact_links: + - name: 💬 GitHub Discussions + url: https://github.com/sveltekit-i18n/lib/discussions + about: Ask questions, share ideas, or discuss the project + - name: 📖 Documentation + url: https://github.com/sveltekit-i18n/lib/tree/master/docs + about: Read the complete documentation + - name: 🔧 Troubleshooting Guide + url: https://github.com/sveltekit-i18n/lib/blob/master/docs/TROUBLESHOOTING.md + about: Common issues and solutions + - name: 💡 Examples + url: https://github.com/sveltekit-i18n/lib/tree/master/examples + about: Working example applications + diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 0000000..cf855fd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,69 @@ +name: Documentation Issue +description: Report an issue with documentation or suggest improvements +title: "[Docs]: " +labels: ["documentation"] +body: + - type: markdown + attributes: + value: | + Thanks for helping improve our documentation! + + - type: dropdown + id: doc-type + attributes: + label: Documentation type + description: What kind of documentation issue is this? + options: + - "Error or incorrect information" + - "Missing information" + - "Unclear or confusing explanation" + - "Broken link" + - "Typo or grammar" + - "Suggestion for improvement" + - "Other" + validations: + required: true + + - type: input + id: location + attributes: + label: Documentation location + description: Which documentation page or file is this about? + placeholder: "docs/GETTING_STARTED.md, README.md, etc." + validations: + required: true + + - type: textarea + id: current + attributes: + label: Current documentation + description: What does the current documentation say? (copy/paste or describe) + placeholder: The current docs say... + validations: + required: true + + - type: textarea + id: issue + attributes: + label: What's the issue? + description: What's wrong or missing? + placeholder: The issue is... + validations: + required: true + + - type: textarea + id: suggestion + attributes: + label: Suggested improvement + description: How should it be changed or what should be added? + placeholder: It should say... or Add section about... + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Additional context + description: Any other context that might help. + placeholder: Any additional information + diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..a4ec7b3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,92 @@ +name: Feature Request +description: Suggest a new feature or enhancement +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for suggesting a new feature! Please provide as much detail as possible. + + - type: textarea + id: problem + attributes: + label: Problem statement + description: Is your feature request related to a problem? Please describe what you're trying to achieve. + placeholder: I'm trying to do X but can't because... + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed solution + description: Describe the solution you'd like to see. + placeholder: I would like to be able to... + validations: + required: true + + - type: textarea + id: api + attributes: + label: Proposed API (if applicable) + description: If you have ideas about how the API should look, share them here. + placeholder: | + ```javascript + const config = { + newFeature: { ... } + }; + ``` + render: markdown + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Have you tried any workarounds or alternative approaches? + placeholder: I tried X but it didn't work because... + + - type: textarea + id: use-case + attributes: + label: Use case + description: Describe your use case. How would this feature benefit you and others? + placeholder: This would help with... + validations: + required: true + + - type: dropdown + id: scope + attributes: + label: Which package? + description: Which package would this feature be for? + options: + - "sveltekit-i18n (main lib)" + - "@sveltekit-i18n/base" + - "@sveltekit-i18n/parser-default" + - "@sveltekit-i18n/parser-icu" + - "New parser" + - "Not sure" + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Additional context + description: Add any other context, screenshots, or examples about the feature request. + placeholder: Any additional information + + - type: checkboxes + id: checklist + attributes: + label: Checklist + description: Please confirm the following + options: + - label: I have searched existing issues and discussions to avoid duplicates + required: true + - label: I have checked the [documentation](https://github.com/sveltekit-i18n/lib/tree/master/docs) and [examples](https://github.com/sveltekit-i18n/lib/tree/master/examples) + required: true + - label: This feature cannot be achieved with current functionality + required: true +