Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
Translator
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
8 / 8
24
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 translate
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 translateDomain
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 translatePlural
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 translateDomainPlural
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 pluralFrom
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 catalog
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 pluralArgs
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Verba;
6
7/**
8 * Resolves messages for one locale across an ordered cascade of domains.
9 *
10 * The first domain whose catalog holds a translation wins; a miss falls back
11 * to the message id itself. Domains map to the directory that holds their
12 * `<domain>.<locale>.php` catalog files.
13 *
14 * @api
15 */
16final class Translator
17{
18    /** @var list<string> */
19    private array $order;
20
21    /** @var array<string, Catalog> */
22    private array $catalogs = [];
23
24    /**
25     * @param array<string, string> $domains Domain name to i18n directory, in cascade order.
26     */
27    public function __construct(
28        public readonly string $locale,
29        private array $domains,
30    ) {
31        $this->order = array_keys($domains);
32    }
33
34    /**
35     * @param array<array-key, string|int|float> $args
36     */
37    public function translate(string $id, array $args = []): string
38    {
39        foreach ($this->order as $domain) {
40            $entry = $this->catalog($domain)->get($id);
41
42            if (is_string($entry)) {
43                return Interpolate::apply($entry, $args);
44            }
45        }
46
47        return Interpolate::apply($id, $args);
48    }
49
50    /**
51     * @param array<array-key, string|int|float> $args
52     */
53    public function translateDomain(string $domain, string $id, array $args = []): string
54    {
55        $entry = array_key_exists($domain, $this->domains) ? $this->catalog($domain)->get($id) : null;
56
57        if (is_string($entry)) {
58            return Interpolate::apply($entry, $args);
59        }
60
61        return Interpolate::apply($id, $args);
62    }
63
64    /**
65     * @param array<array-key, string|int|float> $args
66     */
67    public function translatePlural(string $one, string $many, int $n, array $args = []): string
68    {
69        foreach ($this->order as $domain) {
70            $form = $this->pluralFrom($this->catalog($domain), $one, $n, $args);
71
72            if ($form !== null) {
73                return $form;
74            }
75        }
76
77        return Interpolate::apply($n === 1 ? $one : $many, self::pluralArgs($args, $n));
78    }
79
80    /**
81     * @param array<array-key, string|int|float> $args
82     */
83    public function translateDomainPlural(
84        string $domain,
85        string $one,
86        string $many,
87        int $n,
88        array $args = [],
89    ): string {
90        if (array_key_exists($domain, $this->domains)) {
91            $form = $this->pluralFrom($this->catalog($domain), $one, $n, $args);
92
93            if ($form !== null) {
94                return $form;
95            }
96        }
97
98        return Interpolate::apply($n === 1 ? $one : $many, self::pluralArgs($args, $n));
99    }
100
101    /**
102     * @param array<array-key, string|int|float> $args
103     */
104    private function pluralFrom(Catalog $catalog, string $one, int $n, array $args): ?string
105    {
106        $entry = $catalog->get($one);
107
108        if (is_array($entry)) {
109            if ($entry === []) {
110                return Interpolate::apply($one, self::pluralArgs($args, $n));
111            }
112
113            $idx = $catalog->form($n);
114            $form = $entry[$idx] ?? $entry[array_key_last($entry)];
115
116            return Interpolate::apply($form, self::pluralArgs($args, $n));
117        }
118
119        if (is_string($entry)) {
120            return Interpolate::apply($entry, self::pluralArgs($args, $n));
121        }
122
123        return null;
124    }
125
126    private function catalog(string $domain): Catalog
127    {
128        return $this->catalogs[$domain] ??= Catalog::load(
129            $this->domains[$domain] . '/' . $domain . '.' . $this->locale . '.php',
130            $this->locale,
131        );
132    }
133
134    /**
135     * Positional args are passed through untouched; named (or empty) args gain a
136     * `:count` placeholder bound to $n unless the caller already set `count`.
137     *
138     * @param array<array-key, string|int|float> $args
139     * @return array<array-key, string|int|float>
140     */
141    private static function pluralArgs(array $args, int $n): array
142    {
143        if ($args !== [] && array_is_list($args)) {
144            return $args;
145        }
146
147        /** @var array<array-key, string|int|float> $args */
148        if (!array_key_exists('count', $args)) {
149            $args['count'] = $n;
150        }
151
152        return $args;
153    }
154}