Code

< 40 %
40-60 %
> 60 %
1
<?php declare(strict_types=1);
2
3
namespace Koded\I18n;
4
5
use Koded\Stdlib\Config;
6
use function array_combine;
7
use function array_keys;
8
use function array_map;
9
use function ini_set;
10
use function locale_get_default;
11
use function locale_set_default;
12
use function str_replace;
13
use function strtr;
14
use function vsprintf;
15
16
interface I18nFormatter
17
{
18
    /**
19
     * Message formatter for argument replacement in the message.
20
     *
21
     * @param string $string
22
     * @param array $arguments
23
     * @return string The message with applied arguments (if any)
24
     */
25
    public function format(string $string, array $arguments): string;
26
}
27
28
final class StrtrFormatter implements I18nFormatter
29
{
30 1
    public function format(string $string, array $arguments): string
31
    {
32 1
        return empty($arguments) ? $string : strtr($string, $arguments);
33
    }
34
}
35
36
final class DefaultFormatter implements I18nFormatter
37
{
38 9
    public function format(string $string, array $arguments): string
39
    {
40 9
        return empty($arguments) ? $string : vsprintf($string, $arguments);
41
    }
42
}
43
44
final class CurlyFormatter implements I18nFormatter
45
{
46 5
    public function format(string $string, array $arguments): string
47
    {
48 5
        if (empty($arguments)) {
49 2
            return $string;
50
        }
51 3
        $keys = $vals = [];
52 3
        foreach ($arguments as $k => $v) {
53 3
            $keys[] = "{{$k}}";
54 3
            $vals[] = (string) $v;
55
        }
56 3
        return str_replace($keys, $vals, $string);
57
    }
58
}
59
60
61
class I18n
62
{
63
    public const DEFAULT_LOCALE = 'en_US';
64
65
    /*
66
     * Default configuration parameters for all catalogs.
67
     * Values are taken by the explicitly set default locale,
68
     * or the first loaded catalog instance.
69
     */
70
    private static ?string $catalog = null;
71
    private static ?string $formatter = null;
72
    private static ?string $directory = null;
73
    private static ?string $locale = null;
74
75
    /** @var array<string, I18nCatalog> */
76
    private static array $catalogs = [];
77
78
    // @codeCoverageIgnoreStart
79
    private function __construct() {}
80
    // @codeCoverageIgnoreEnd
81
82 9
    public static function translate(
83
        string $string,
84
        array  $arguments = [],
85
        string $locale = ''): string
86
    {
87 9
        return self::catalog($locale)->translate('messages', $string, $arguments);
88
    }
89
90 12
    public static function locale(): string
91
    {
92 12
        return self::$locale ??= I18nCatalog::normalizeLocale(locale_get_default());
93
    }
94
95
    /**
96
     * @return array{string, I18nCatalog}
97
     */
98 23
    public static function catalogs(): array
99
    {
100 23
        return self::$catalogs;
101
    }
102
103 18
    static public function catalog(string $locale): I18nCatalog
104
    {
105 18
        empty($locale) and $locale = self::locale();
106 18
        return self::catalogs()[$locale] ?? self::registerCatalog($locale);
107
    }
108
109
    /**
110
     * @return array<string, string>
111
     */
112 1
    public static function languages(): array
113
    {
114 1
        return array_combine(
115 1
            array_keys(self::$catalogs),
116 1
            array_map(static fn(I18nCatalog $c) => $c->language(), self::$catalogs)
117 1
        );
118
    }
119
120
    /**
121
     * @return array{locale:string, catalogs:<string, array{class: string, formatter:string, dir:string, locale:string}>}
122
     */
123 1
    public static function info(): array
124
    {
125 1
        $catalogs = [];
126 1
        foreach (self::$catalogs as $locale => $instance) {
127 1
            $catalogs[$locale] = [
128 1
                'class' => $instance::class,
129 1
                'formatter' => $instance->formatter()::class,
130 1
                'dir' => $instance->directory(),
131 1
                'locale' => $instance->locale(),
132 1
            ];
133
        }
134 1
        return [
135 1
            'locale' => self::$locale,
136 1
            'catalogs' => $catalogs,
137 1
        ];
138
    }
139
140 25
    public static function register(
141
        I18nCatalog $catalog,
142
        bool        $asDefault = false): void
143
    {
144 25
        $locale = $catalog->locale();
145 25
        if ($asDefault || empty(self::$catalogs)) {
146 25
            self::setDefaultLocale($locale);
147 25
            self::$directory = $catalog->directory();
148 25
            self::$formatter = $catalog->formatter()::class;
149 25
            self::$catalog = $catalog::class;
150
        }
151 25
        self::$catalogs[$locale] = $catalog;
152
    }
153
154 26
    public static function flush(): void
155
    {
156 26
        self::$catalogs = [];
157 26
        self::$directory = null;
158 26
        self::$formatter = null;
159 26
        self::$catalog = null;
160 26
        self::$locale = null;
161 26
        ini_set('intl.default_locale', '');
162 26
        locale_set_default('');
163
    }
164
165 8
    private static function registerCatalog(string $locale): I18nCatalog
166
    {
167 8
        return self::$catalogs[$locale] = I18nCatalog::new((new Config)->import([
168 8
            'translation.locale' => $locale,
169 8
            'translation.dir' => self::$directory,
170 8
            'translation.formatter' => self::$formatter,
171 8
            'translation.catalog' => self::$catalog,
172 8
        ]));
173
    }
174
175 25
    private static function setDefaultLocale(string $locale): void
176
    {
177 25
        self::$locale = $locale;
178 25
        ini_set('intl.default_locale', $locale);
179 25
        locale_set_default($locale);
180
    }
181
}
182