# Translations and Language Files

https://dev.wisecp.com/es/translations-and-language-files

Nothing a person reads is written in code. Text lives in language files, is looked up by key, and a missing key answers false rather than the text you expected.

## Overview

An installation serves more than one language at once, chosen per visitor. A literal string in a condition, an exception or a template is not a shortcut. It is text one group of users will never see in their own language, and that no translator can reach.

Two lookups cover everything: one for the root language files, one for the per-screen files under `cm/`. Both walk a slash separated path into nested arrays.

## Reference

```php
public static function g($key = '', $replaces = [], $slang = ''): string|int|array|bool;
public static function gc($name = '', $replaces = [], $slang = ''): string|int|array|bool;
public static function selected(): string;

// Instance, reached through the singleton: Language::$init->rank_list()
public function rank_list($status = 'active'): array;

// The instance methods the two static helpers delegate to. Note the argument
// order: get() puts the language second, get_cm() puts the replacements second.
public function get($arg = null, $slang = '', $replaces = []): string|int|array|bool;
public function get_cm($arg = null, $replaces = [], $slang = ''): string|int|array|bool;
```

- **Language::g()**: A key from a root language file. Returns the text, or `false` when any segment of the path is missing.
- **Language::gc()**: A key from a per-screen file under `cm/`. Use this in helpers, modules and exceptions, since it names its file rather than relying on what the current screen loaded.
- **Language::selected()**: The language code being served, such as `en`. Falls back to the installation's own language when the language layer is not up yet. That makes it safe on the command line.
- **Language::$init->rank_list()**: The installation's languages in display order. An instance method: there is no static `Language::rank_list()`.

### Which File Each Lookup Reads

| Call | Reads | Path shape |
| --- | --- | --- |
| `Language::g("needs/button-save")` | Root language files: `needs`, `date`, `errors`, `actions`, `package` | `{file}/{key}/{subkey}` |
| `Language::gc("admin/services/page-list")` | Per-screen files under `cm/`, grouped by area | `{area}/{file}/{key}`, area being `admin`, `website` or `system` |
| `Language::gc("theme/hero-title")` | The active theme's own language file | `theme/{key}` |

Every slash is one array level, not part of a name. A flat key written as `'job-title/daily'` is never found; it has to be a nested array.

### Placeholders and the Replacement Map

The second argument maps the **literal placeholder text**, braces or colon included, to its replacement. It is a plain string replacement, so the keys must match the file byte for byte.

```php
// coremio/locale/en/cm/admin/departments.php
return [
    'error1'  => 'Name is required for {lang}.',

    // The content/variables shape declares which placeholders exist. Lookups
    // return the content string, never the wrapper.
    'welcome' => [
        'content'   => 'Hello {name}, you have {count} messages.',
        'variables' => '{name},{count}',
    ],
];

// The call. Keys carry their own braces; ':name' style keys work the same way.
$msg = Language::gc("admin/departments/error1", ['{lang}' => "TR"]);      // "Name is required for TR."
$hi  = Language::gc("admin/departments/welcome", ['{name}' => $name, '{count}' => 3]);

// Third argument forces one language instead of the one being served.
$tr  = Language::gc("admin/departments/error1", ['{lang}' => "TR"], "tr");
```

### What the Language List Returns

The row shape depends on the argument, so the two cannot be swapped. `active` returns display rows built for a switcher. `all` returns the raw package record of every installed language, including the disabled ones.

```php
// Language::$init->rank_list()          enabled languages, ordered by rank
[
    'rank' => 2, 'local' => 0, 'selected' => true, 'key' => 'en',
    'name' => 'English', 'global-name' => 'English',
    'link' => 'https://example.test/en/services',        // this page in that language
    'cc' => '', 'cname' => 'Worldwide', 'pc' => 1,
    'flag-img' => 'https://example.test/resources/assets/images/flags/en.svg',
];

// Language::$init->rank_list("all")     every installed language, package record
// The whole record, in this order. 'key' and 'country-name' are added by the call;
// the rest is the package file as it sits on disk.
[
    'create-date' => '2018-06-21 10:15:48',
    'name' => 'English', 'show-name' => 'English',
    'country-id' => 0, 'country-code' => '',
    'code' => 'en', 'code-hyphen' => 'en_US',
    'scharacters' => '', 'charset-code' => 'UTF-8',
    'phone-code' => 1, 'currency' => 1, 'rank' => 2,
    'permalink' => true, 'prefix' => 'enabled', 'status' => true,
    'local' => false, 'rtl' => false,
    'key' => 'en', 'country-name' => 'Worldwide',
];
```

### Text That Carries a Number

```php
public static function plural(string $key, int|float $n, array $replaces = [], string $slang = ''): string;
public static function plural_set(string $key, string $slang = ''): array;
public static function plural_category(int|float $n, string $slang = ''): string;
```

- **plural()**: Picks the form for a count. Never choose it yourself with `n === 1` — that holds only for two-form languages. Russian wants three forms, Arabic six.
- **plural_set()**: Every form as a map, for a count only the browser knows. The server sends the set and `wcpPlural()` chooses in the page.
- **plural_category()**: The category a number falls into: `one`, `few`, `many`, `other` and so on.

### Forms With a Language Strip

```php
public static function form_langs(array $defined = []): array;
public static function posted_langs(): array;
public static function removed_langs(): array;
```

- **form_langs()**: The languages a record's strip opens with: the primary one, plus whatever that record already carries.
- **posted_langs()**: The languages the submitted form declared. Walk these when saving, not `rank_list()`. A language the operator never opened still posts empty values, and writing those over a stored translation erases it.
- **removed_langs()**: The languages the operator closed and confirmed. The primary language can never be one of them.

### Where Text Lives

- **coremio/locale/{lang}/**: Application text, one directory per language, split into packages that mirror the screens.
- **Module files**: Each module carries its own `lang/` directory, loaded into `$this->lang` by the constructor. Its text travels with it rather than being added to the application's.
- **Theme files**: A theme carries its own wording too, reached through the `theme/` prefix, so two themes can name the same thing differently.
- **Translated records**: Content an operator writes lives in the database, one row per language beside the record.

## Example

```php
// Inside a screen whose package is already loaded.
$title = Language::g("needs/button-save");

// Anywhere else: name the file, then the key.
throw new Exception(Language::gc("admin/services/error-not-found"));

// Joining a translation table for the language being served.
$lang  = Language::selected();
$rows  = WDB::select("t1.id, COALESCE(t2.name, t1.name) AS name")
    ->from("products AS t1")
    ->join("LEFT", "products_lang AS t2", "t2.owner_id=t1.id AND t2.lang='" . $lang . "'")
    ->build();

// A language switcher.
foreach (Language::$init->rank_list() as $l)
    $items[] = ['label' => $l["name"], 'href' => $l["link"], 'on' => $l["selected"]];
```

```php
// coremio/locale/en/cm/admin/services.php   and the same key in every other language
return [
    'page-list'        => 'Services',
    'error-not-found'  => 'Service not found.',

    // Nested, so the path is admin/services/errors/timeout
    'errors' => [
        'timeout' => 'The provider timed out.',
    ],
];

// Written back from code when a screen edits a language file.
Language::save("admin/services", $data, "en");
```

## Pitfalls

> **Replacements are second, the language is third**
> 
> Both static helpers take the replacement map in position two and the language code in position three. The instance method behind `g()` reverses them. A snippet copied from inside the class puts a language code where a map belongs. Passing the language second to the static call silently produces no substitution and no error.

> **A missing key answers false**
> 
> Not an empty string, and not the key itself. Written as `Language::gc("...") ?: "Some text"` the fallback hides the miss forever. The key looks alive while the language file never had it. Check a doubtful key on the command line and expect a string, not a boolean.

> **A nested key is not a key with a slash in it**
> 
> The path form walks into nested arrays, one level per slash. A key defined flat as `'meta-detail/title'` is unreachable, and searching for the last segment alone finds the wrong entry or nothing.

> **The first language list in a request fixes the shape for the rest**
> 
> Both arguments share one cached list and `all` overwrites it. Once anything in the request has asked for `all`, every later `rank_list()` answers with package records instead of display rows. `link`, `selected`, `global-name` and `flag-img` are absent, and the enabled-only filter is gone with them. A switcher shown after an administrative screen on the same request is where this appears. Each missing key is a logged warning rather than an error. Take the display rows before anything asks for `all`, or keep your own copy of them.

> **Add the key to every language, not only yours**
> 
> Adding text is a change to every language file the installation ships. Records in the database behave differently. A row with no translation for the active language shows its own value instead. That is why the two are handled by different code.

## Related Articles

- [Coding Conventions](https://dev.wisecp.com/en/coding-conventions)
- [Module Language Files](https://dev.wisecp.com/en/module-language-files)
- [Translating a Theme](https://dev.wisecp.com/en/translating-a-theme)
- [Overriding Text and Templates](https://dev.wisecp.com/en/overriding-text-and-templates)
