Completed
Push — master ( 4ea5af...f482fd )
by Rasmus
03:31
created

lang::load()   D

Complexity

Conditions 9
Paths 24

Size

Total Lines 41
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 9

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 41
ccs 21
cts 21
cp 1
rs 4.909
cc 9
eloc 20
nc 24
nop 1
crap 9
1
<?php
2
3
namespace mindplay;
4
5
use ComposerLocator;
6
use ReflectionFunction;
7
8
/**
9
 * This class acts as a pseudo-namespace for translation functions
10
 *
11
 * Language codes are two-letter ISO-639-1 language codes, such as "en", "da", "es", etc.
12
 *
13
 * Translation domain names take the form "{vendor}/{package}", where the package name
14
 * may contain several names separated by slashes.
15
 *
16
 * @link https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
17
 */
18
abstract class lang
19
{
20
    /**
21
     * @var string default language code
22
     */
23
    const DEFAULT_LANGUAGE = "en";
24
25
    /**
26
     * Use this property to set an error callback - you can use this for error reporting in test-suites.
27
     *
28
     * @var callable function ($message) : void
29
     */
30
    public static $on_error;
31
32
    /**
33
     * @var string active language code
34
     */
35
    protected static $code = self::DEFAULT_LANGUAGE;
36
    
37
    /**
38
     * @var (string|callable)[][] map where "{domain}/{code}" => translation strings or callables
39
     */
40
    protected static $lang = [];
41
42
    /**
43
     * @var string[] map where "{domain}" or "{domain}/{code}" => absolute path to language file
44
     */
45
    protected static $paths = [];
46
47
    /**
48
     * Change the current active language code.
49
     *
50
     * @param string $code two-letter ISO-639-1 language code (such as "en", "da", "es", etc.)
51
     *
52
     * @link https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
53
     */
54 1
    public static function set($code)
55
    {
56 1
        self::$code = $code;
57 1
    }
58
59
    /**
60
     * Register the physical base path of language files for a given translation domain name.
61
     *
62
     * @param string $domain translation domain name
63
     * @param string $path   absolute path to language directory (or base filename, without ".php")
64
     */
65 1
    public static function register($domain, $path)
66
    {
67 1
        self::$paths[$domain] = $path;
68 1
    }
69
70
    /**
71
     * Translate the given text in the given domain and substitute the given tokens.
72
     *
73
     * @param string     $domain translation domain name
74
     * @param string     $text   english text
75
     * @param array|null $tokens map where token name => replacement string
76
     *
77
     * @return string
78
     */
79 1
    public static function text($domain, $text, array $tokens = null)
80
    {
81 1
        return self::translate(self::$code, $domain, $text, $tokens);
82
    }
83
84
    /**
85
     * Obtain a translation callback for a given domain, optionally for a specific language.
86
     *
87
     * The returned function has the following signature:
88
     *
89
     *     function (string $text, array $tokens) : string
90
     *
91
     * This may be useful in a view/template, for example, so you don't have to repeat the
92
     * domain name for every call. It may also be useful to inject this function as a dependency.
93
     *
94
     * @param string      $domain translation domain name
95
     * @param string|null $code   optional language code (defaults to the current language)
96
     *
97
     * @return callable function ($text, array $tokens) : string
98
     */
99 1
    public static function domain($domain, $code = null)
100
    {
101
        return function ($text, array $tokens = null) use ($domain, $code) {
102 1
            return self::translate($code ?: self::$code, $domain, $text, $tokens);
103 1
        };
104
    }
105
106
    /**
107
     * This is the lowest-level function, which requires every parameter to be given explicitly.
108
     *
109
     * @param string $code   two-letter ISO-639-1 language code
110
     * @param string $domain translation domain name
111
     * @param string $text   english text
112
     * @param array  $tokens map where token name => replacement string
113
     *
114
     * @return string
115
     */
116 1
    public static function translate($code, $domain, $text, array $tokens = null)
117
    {
118 1
        $name = "{$domain}/{$code}";
119
120 1
        if (! isset(self::$lang[$name])) {
121 1
            self::load($name);
122
        }
123
124 1
        $has_template = isset(self::$lang[$name][$text]);
125
126 1
        if (self::$on_error && !$has_template) {
127 1
            call_user_func(self::$on_error, "missing translation of '{$text}' for: {$name}");
128
        }
129
130 1
        $template = $has_template
131 1
            ? self::$lang[$name][$text]
132 1
            : $text;
133
134 1
        if (is_callable($template)) {
135
            // perform translation with a user-defined function:
136
137 1
            $args = [];
138
139 1
            if ($tokens) {
140 1
                $func = new ReflectionFunction($template);
141
142 1
                foreach ($func->getParameters() as $param) {
143 1
                    $args[] = isset($tokens[$param->name])
144 1
                        ? $tokens[$param->name]
145 1
                        : "{{$param->name}}"; // ignore missing tokens
146
                }
147
            }
148
149 1
            return call_user_func_array($template, $args);
150
        } else {
151 1
            if ($tokens) {
152
                // perform translation using simple string substitution:
153
154 1
                return strtr(
155
                    $template,
156
                    array_combine(
157
                        array_map(
158 1
                            function ($key) {
159 1
                                return "{{$key}}";
160 1
                            },
161
                            array_keys($tokens)
162
                        ),
163
                        $tokens
164
                    )
165
                );
166
            } else {
167
                // no token substitution required:
168
169 1
                return $template;
170
            }
171
        }
172
    }
173
174
    /**
175
     * Reset the internal state of the language registry.
176
     *
177
     * This may be useful for unit-testing or other special situations.
178
     */
179 1
    public static function reset()
180
    {
181 1
        self::$code = lang::DEFAULT_LANGUAGE;
182 1
        self::$lang = [];
183 1
        self::$paths = [];
184 1
    }
185
186
    /**
187
     * Internally find a load a given language file.
188
     *
189
     * @param string $name full language file base name, e.g. "{domain}/{code}"
190
     */
191 1
    protected static function load($name)
192
    {
193 1
        $domain_names = explode('/', $name);
194
195 1
        while (count($domain_names)) {
196 1
            $parent_domain = implode('/', $domain_names);
197
            
198 1
            if (isset(self::$paths[$parent_domain])) {
199 1
                $path = self::$paths[$parent_domain] . substr($name, strlen($parent_domain)) . '.php';
200
201 1
                if (file_exists($path)) {
202 1
                    self::$lang[$name] = require $path;
203
204 1
                    break;
205
                }
206
            }
207
208 1
            if (count($domain_names) === 2 && ComposerLocator::isInstalled($parent_domain)) {
0 ignored issues
show
Bug introduced by
The method isInstalled() does not seem to exist on object<ComposerLocator>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
209
                // attempt mapping by convention 
210
                
211 1
                $path = ComposerLocator::getPath($parent_domain)
0 ignored issues
show
Bug introduced by
The method getPath() does not seem to exist on object<ComposerLocator>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
212 1
                    . "/lang" . substr($name, strlen($parent_domain)) . '.php';
213
214 1
                if (file_exists($path)) {
215 1
                    self::$lang[$name] = require $path;
216
                    
217 1
                    break;
218
                }
219
            } 
220
221 1
            array_pop($domain_names);
222
        }
223
224 1
        if (! isset(self::$lang[$name])) {
225 1
            self::$lang[$name] = []; // no translation file available
226
227 1
            if (self::$on_error) {
228 1
                call_user_func(self::$on_error, "no translation file found for: {$name}");
229
            }
230
        }
231 1
    }
232
}
233