# Localization

Magic provides a Laravel-style localisation system that lets you retrieve and switch translation strings at runtime without needing BuildContext.

- [Introduction](#introduction)
- [Configuration](#configuration)
- [Defining Translation Strings](#defining-translation-strings)
- [Retrieving Translations](#retrieving-translations)
    - [The trans() Helper](#the-trans-helper)
    - [Pluralization](#pluralization)
    - [Checking Key Existence](#checking-key-existence)
- [Locale State](#locale-state)
    - [Current Locale](#current-locale)
    - [Loaded State](#loaded-state)
    - [Supported Locales](#supported-locales)
- [Changing Locale](#changing-locale)
    - [Setting the Locale](#setting-the-locale)
    - [Auto-Detecting Locale](#auto-detecting-locale)
    - [Setting Supported Locales at Runtime](#setting-supported-locales-at-runtime)
- [Reacting to Locale Changes](#reacting-to-locale-changes)
- [Flutter Integration (Delegate)](#flutter-integration-delegate)
- [Automatic HTTP Headers](#automatic-http-headers)
- [CLI Commands](#cli-commands)
- [Development: Hot Restart](#development-hot-restart)
- [Related Documentation](#related-documentation)

## Introduction

Magic provides a Laravel-style localisation system that allows you to retrieve strings in various languages **without needing BuildContext**.

```dart
// Anywhere in your code, no context needed
Text(trans('welcome', {'name': 'Magic'}))  // "Welcome, Magic!"
Text(trans('auth.failed'))                  // "Authentication failed."
```

## Configuration

### Enabling Localization

Add `LocalizationServiceProvider` to your providers in `config/app.dart`:

```dart
'providers': [
  (app) => LocalizationServiceProvider(app),
  // ... other providers
],
```

### Localization Configuration

Create `lib/config/localization.dart`:

```dart
Map<String, dynamic> get localizationConfig => {
  'localization': {
    'locale': 'en',
    'fallback_locale': 'en',
    'supported_locales': ['en', 'tr', 'es', 'de'],
    'auto_detect_locale': true,
    'path': 'assets/lang',
  },
};
```

### Configuration Options

| Key | Default | Description |
|:---|:---|:---|
| `locale` | `'en'` | Default locale |
| `fallback_locale` | `'en'` | Fallback when translation is missing |
| `supported_locales` | `['en']` | Supported locale codes |
| `auto_detect_locale` | `false` | Auto-detect from device on boot |
| `path` | `'assets/lang'` | Translation JSON files path |

### Register Assets

Add translation files to your `pubspec.yaml`:

```yaml
flutter:
  assets:
    - assets/lang/en.json
    - assets/lang/tr.json
    - assets/lang/es.json
```

> [!NOTE]
> A catalogue that does not load is not an error: every key then renders as
> itself, because `Lang.get` answers the key when it has no sentence. That is
> deliberate, so a broken file never blanks an interface, but it means the
> symptom of a missing asset and the symptom of a missing key look identical.
> `JsonAssetLoader` logs a warning naming the path when it gives up, which is
> the difference between the two; the warning is skipped when no `log` service
> is bound, so it is silent in a widget test that never calls `Magic.init`.

## Defining Translation Strings

Translation strings are stored as JSON files in `assets/lang/`:

```json
{
  "welcome": "Welcome, :name!",
  "auth": {
    "login": "Login",
    "register": "Register",
    "logout": "Logout",
    "failed": "These credentials do not match our records.",
    "throttle": "Too many login attempts. Please try again in :seconds seconds."
  },
  "validation": {
    "required": "The :attribute field is required.",
    "email": "The :attribute must be a valid email address.",
    "min": {
      "string": "The :attribute must be at least :min characters."
    }
  },
  "attributes": {
    "email": "email address",
    "password": "password",
    "password_confirmation": "password confirmation"
  }
}
```

### Nested Keys

Use dot notation to access nested translations:

```dart
trans('auth.failed')           // "These credentials do not match..."
trans('validation.required')   // "The :attribute field is required."
```

## Retrieving Translations

### Basic Usage

```dart
// Simple translation
WText(trans('auth.login'))

// With parameters
WText(trans('welcome', {'name': user.name}))
// Output: "Welcome, John!"

// Nested keys
WText(trans('auth.throttle', {'seconds': '60'}))
// Output: "Too many login attempts. Please try again in 60 seconds."
```

### The trans() Helper

The `trans()` function is a globally available helper that delegates to `Lang.get`:

```dart
String trans(String key, [Map<String, dynamic>? replace])
```

- Returns the translation for the current locale
- Falls back to `fallback_locale` **per key**, so a key missing from the
  current locale's catalogue is served from the fallback's rather than
  rendering raw
- Returns the key itself if neither catalogue defines it
- Replaces `:param` placeholders with provided values

The fallback catalogue is loaded once, alongside the locale's own, and the
locale's strings are layered over it. A fallback that will not load is a
logged warning rather than a failure: the locale's own strings still render.

You can also call `Lang.get` directly, which is equivalent:

```dart
final message = Lang.get('welcome', {'name': 'Magic'});
```

### Pluralization

A sentence carrying a number needs more than one wording, and `trans()` cannot give it one: it is a lookup plus a replacement, so the same line renders at every count. Use `transChoice()`, Laravel's `trans_choice`, which takes the count and picks a segment from a pipe-separated line.

```dart
String transChoice(String key, int number, [Map<String, dynamic>? replace])
```

```json
{
  "apples": "There is one apple|There are :count apples"
}
```

```dart
transChoice('apples', 1); // "There is one apple"
transChoice('apples', 4); // "There are 4 apples"
```

`:count` is substituted from the number you pass, so you never pass it twice. An explicit `count` in `replace` still wins, which is how you render a spelled-out number.

#### Inline conditions

Segments may carry an explicit count or range, and the first that matches wins. A range accepts `*` on either bound.

```json
{
  "inbox": "{0} Nothing here|[1,19] :count messages|[20,*] Lots of messages"
}
```

```dart
transChoice('inbox', 0);  // "Nothing here"
transChoice('inbox', 7);  // "7 messages"
transChoice('inbox', 99); // "Lots of messages"
```

Write a line that is **all** conditions or none. A half-conditional line shifts its own positional segments, because a condition that matched nothing is stripped and what remains is read by index.

#### The plural rules are per language

This is the part that surprises people, and it is why the count is not simply compared to one.

| Language | Forms | Rule |
|:---|:---|:---|
| `tr`, `ja`, `ko`, `zh`, `id`, `vi` and others | 1 | every count takes segment 0 |
| `en`, `de`, `es`, `nl`, `pt`, `sv` and others | 2 | `n == 1` takes segment 0 |
| `fr`, `hi`, `fil`, `am` and others | 2 | `n == 0 \|\| n == 1` takes segment 0 |
| `ru`, `uk`, `hr`, `sr`, `bs`, `be` | 3 | by the last digit |
| `ar` | 6 | zero, one, two, few, many, other |

Other languages (for example `cs`, `pl` and `ro`) carry rules of their own.

Turkish has no plural agreement after a number, so a Turkish line's second segment is never reached. If your app's first locale is Turkish, Japanese or Chinese, a two-segment English line will look correct throughout development and be wrong the first time the second locale renders.

An unlisted language answers segment 0, which is Laravel's own default: it reads as "this language does not agree with its number" rather than as English. The full table lives in `MessageSelector`, ported from Laravel's `getPluralIndex`.

`Lang.choice` is the facade form and is equivalent:

```dart
Lang.choice('apples', 4);
```

### Checking Key Existence

Use `Lang.has` to check whether a translation key exists for the current locale before retrieving it:

```dart
if (Lang.has('errors.payment_declined')) {
  return trans('errors.payment_declined');
}
```

## Locale State

### Current Locale

`Lang.current` returns the active `Locale`:

```dart
final locale = Lang.current; // Locale('tr')
print(locale.languageCode);  // "tr"
```

### Loaded State

`Lang.isLoaded` returns `true` once translations have been loaded for the current locale. Use this to guard against calling `trans()` before the localisation service has finished booting:

```dart
if (Lang.isLoaded) {
  print(trans('welcome'));
}
```

### Supported Locales

`Lang.supportedLocales` returns the list of `Locale` objects your app supports, as configured in `localization.supported_locales`:

```dart
final locales = Lang.supportedLocales;
// [Locale('en'), Locale('tr'), Locale('es')]
```

## Changing Locale

### Setting the Locale

Call `Lang.setLocale` to switch the active locale at runtime. By default it also triggers `Magic.reload()` so all widgets rebuild with the new strings:

```dart
// Change locale and rebuild the app
await Lang.setLocale(Locale('tr'));

// Load translations without triggering app reload
await Lang.setLocale(Locale('tr'), reload: false);
```

### Language Picker Example

```dart
WFormSelect<String>(
  value: Lang.current.languageCode,
  options: [
    SelectOption(value: 'en', label: 'English'),
    SelectOption(value: 'tr', label: 'Türkçe'),
    SelectOption(value: 'es', label: 'Español'),
  ],
  onChange: (code) async {
    await Lang.setLocale(Locale(code!));
    Magic.toast(trans('common.language_changed'));
  },
  label: trans('settings.language'),
)
```

### Auto-Detecting Locale

`Lang.detectLocale` returns the best matching `Locale` from the device or browser without changing the current locale. It compares device preferences against `supported_locales` and returns the closest match:

```dart
final detected = Lang.detectLocale();
print(detected); // e.g., Locale('tr')
```

`Lang.detectAndSetLocale` combines detection and activation in one call. It returns the locale that was set:

```dart
// On app start, pick the user's device language automatically
final locale = await Lang.detectAndSetLocale();
print(locale); // e.g., Locale('tr')
```

You can also use `auto_detect_locale: true` in the localisation config to have `LocalizationServiceProvider` call this automatically during boot.

### Setting Supported Locales at Runtime

If you need to change the supported locale list after boot (for example, after loading user preferences from the server), use `Lang.setSupportedLocales`:

```dart
Lang.setSupportedLocales([Locale('en'), Locale('fr'), Locale('de')]);
```

## Reacting to Locale Changes

The `Translator` underlying `Lang` extends Flutter's `ChangeNotifier`. Use `Lang.addListener` and `Lang.removeListener` to subscribe to locale changes without a `BuildContext`:

```dart
class LocaleAwareService {
  LocaleAwareService() {
    Lang.addListener(_onLocaleChanged);
  }

  void _onLocaleChanged() {
    // Re-format any locale-sensitive values
    print('Locale changed to: ${Lang.current}');
  }

  void dispose() {
    Lang.removeListener(_onLocaleChanged);
  }
}
```

Listeners fire after every successful `setLocale` or `detectAndSetLocale` call, so they are a reliable hook for updating any state that depends on the current locale outside the widget tree.

## Flutter Integration (Delegate)

When you use a plain `MaterialApp` from `package:material_ui/material_ui.dart` (rather than `MagicApp`), you must pass `Lang.delegate` to `localizationsDelegates` so Flutter's localisation lifecycle triggers translation reloads when the device locale changes. `GlobalMaterialLocalizations.delegates` already carries the Material, Cupertino and Widgets delegates:

```dart
MaterialApp(
  localizationsDelegates: [
    Lang.delegate,
    ...GlobalMaterialLocalizations.delegates,
  ],
  supportedLocales: Lang.supportedLocales,
  home: const HomePage(),
)
```

`Lang.delegate` is a `const LocalizationsDelegate<Translator>`. It implements `isSupported` by checking against `localization.supported_locales` from config and delegates `load` to the `Translator` singleton.

> [!NOTE]
> When you use `MagicApp`, the delegate is wired automatically by the framework when `LocalizationServiceProvider` is registered. You only need to add it manually when bypassing `MagicApp`.

## Automatic HTTP Headers

When `LocalizationServiceProvider` is registered, Magic automatically attaches two headers to every outgoing HTTP request via `LocalizationInterceptor`. No manual setup is required.

| Header | Value | Example |
|:---|:---|:---|
| `Accept-Language` | Current locale language code | `en`, `tr`, `es` |
| `X-Timezone` | Current IANA timezone identifier | `Europe/Istanbul`, `America/New_York` |

Both values are resolved at request-time, so they always reflect the locale and timezone that are active when the request is dispatched.

```dart
// When the current locale is 'tr' and timezone is 'Europe/Istanbul',
// every Http.get/post/put/delete call automatically includes:
//
//   Accept-Language: tr
//   X-Timezone: Europe/Istanbul
//
// This works transparently, no configuration needed.
final response = await Http.get('/api/profile');
```

> [!NOTE]
> `LocalizationInterceptor` is registered automatically by `LocalizationServiceProvider`. You do not need to add it to your provider list or network configuration manually.

### Timezone Detection

Set `auto_detect_timezone` to `true` in `config/localization.dart` (it defaults to
`false`) and Magic resolves the device's real IANA timezone identifier on boot,
reading it from the platform's native APIs via the `flutter_timezone` package. The
resolved value is sent as the `X-Timezone` header on every outgoing request, so the
backend can interpret the client's timestamps.

```dart
// config/localization.dart
'auto_detect_timezone': true,
'timezone': 'UTC', // the fallback when detection cannot resolve a zone
```

You do not need any timezone detection code of your own. When detection cannot
resolve a valid identifier, the configured `timezone` stays in place; Magic does
not guess a zone from the device's UTC offset, because an offset does not identify
a zone (many zones share an offset and differ in DST rules).

## CLI Commands

### Create Translation File

```bash
dart run magic:dowel make:lang fr
dart run magic:dowel make:lang de
dart run magic:dowel make:lang tr
```

This command creates `assets/lang/<code>.json`. When `assets/lang/en.json` exists, the new file gets a copy of its key tree with every value copied verbatim, ready for a translator; otherwise the file is an empty `{}`. Use `--from=<code>` to copy another catalogue and `--force` to overwrite an existing file:

```bash
dart run magic:dowel make:lang tr --from=en
```

The command does not edit `pubspec.yaml`: add the new file under `flutter: assets:` yourself (see Register Assets under [Configuration](#configuration)).

> [!TIP]
> Keep your translation keys organised by feature (auth, validation, users, etc.) for easier maintenance.

## Development: Hot Restart

During development, Magic attempts to bypass Flutter's asset bundle cache so that translation JSON changes can be picked up on **hot restart** (`Shift+R`) without a full rebuild.

| Platform | Mechanism | Reliability |
|:---|:---|:---|
| Web (Chrome) | Fetches JSON via HTTP with cache-busting query parameter | Verified |
| macOS / Linux / Windows | Reads JSON from disk via `dart:io` | Best-effort (works when `flutter run` sets the working directory to the project root) |
| iOS / Android | Attempts disk read via `dart:io`, falls back to `rootBundle` | Limited (asset files are typically not on disk) |

This behaviour is **debug-mode only** (`kDebugMode`). Release builds use Flutter's standard `rootBundle` with full caching for optimal performance.

> [!NOTE]
> Hot **reload** (lowercase `r`) only reloads Dart code and cannot pick up asset changes. Use hot **restart** (`Shift+R`) to see translation updates.

## Related Documentation

- [Validation](https://anilcancakir.com/magic/docs/digging-deeper/validation.md)
- [Carbon](https://anilcancakir.com/magic/docs/digging-deeper/carbon.md)
- [Configuration](https://anilcancakir.com/magic/docs/getting-started/configuration.md)
- [HTTP Client](https://anilcancakir.com/magic/docs/basics/http-client.md)
