21
21
*
22
22
* Locale-based, built on top of PHP internationalization.
23
23
*
24
+ * @phpstan-type LoadedStrings array<string, array<string, array<string, string>|string>|string|list<string>>
25
+ *
24
26
* @see \CodeIgniter\Language\LanguageTest
25
27
*/
26
28
class Language
@@ -30,20 +32,19 @@ class Language
30
32
* from files for faster retrieval on
31
33
* second use.
32
34
*
33
- * @var array
35
+ * @var array<non-empty-string, array<non-empty-string, LoadedStrings>>
34
36
*/
35
37
protected $ language = [];
36
38
37
39
/**
38
- * The current language/ locale to work with.
40
+ * The current locale to work with.
39
41
*
40
- * @var string
42
+ * @var non-empty- string
41
43
*/
42
44
protected $ locale ;
43
45
44
46
/**
45
- * Boolean value whether the intl
46
- * libraries exist on the system.
47
+ * Boolean value whether the `intl` extension exists on the system.
47
48
*
48
49
* @var bool
49
50
*/
@@ -53,10 +54,13 @@ class Language
53
54
* Stores filenames that have been
54
55
* loaded so that we don't load them again.
55
56
*
56
- * @var array
57
+ * @var array<non-empty-string, list<non-empty-string>>
57
58
*/
58
59
protected $ loadedFiles = [];
59
60
61
+ /**
62
+ * @param non-empty-string $locale
63
+ */
60
64
public function __construct (string $ locale )
61
65
{
62
66
$ this ->locale = $ locale ;
@@ -69,6 +73,8 @@ public function __construct(string $locale)
69
73
/**
70
74
* Sets the current locale to use when performing string lookups.
71
75
*
76
+ * @param non-empty-string|null $locale
77
+ *
72
78
* @return $this
73
79
*/
74
80
public function setLocale (?string $ locale = null )
@@ -89,80 +95,91 @@ public function getLocale(): string
89
95
* Parses the language string for a file, loads the file, if necessary,
90
96
* getting the line.
91
97
*
98
+ * @param array<array-key, float|int|string> $args
99
+ *
92
100
* @return list<string>|string
93
101
*/
94
102
public function getLine (string $ line , array $ args = [])
95
103
{
96
- // if no file is given, just parse the line
104
+ // 1. Format the line as- is if it does not have a file.
97
105
if (! str_contains ($ line , '. ' )) {
98
106
return $ this ->formatMessage ($ line , $ args );
99
107
}
100
108
101
- // Parse out the file name and the actual alias.
102
- // Will load the language file and strings.
109
+ // 2. Get the formatted line using the file and line extracted from $line and the current locale.
103
110
[$ file , $ parsedLine ] = $ this ->parseLine ($ line , $ this ->locale );
104
111
105
112
$ output = $ this ->getTranslationOutput ($ this ->locale , $ file , $ parsedLine );
106
113
107
- if ($ output === null && strpos ($ this ->locale , '- ' )) {
114
+ // 3. If not found, try the locale without region (e.g., 'en-US' -> 'en').
115
+ if ($ output === null && str_contains ($ this ->locale , '- ' )) {
108
116
[$ locale ] = explode ('- ' , $ this ->locale , 2 );
109
117
110
118
[$ file , $ parsedLine ] = $ this ->parseLine ($ line , $ locale );
111
119
112
120
$ output = $ this ->getTranslationOutput ($ locale , $ file , $ parsedLine );
113
121
}
114
122
115
- // if still not found, try English
123
+ // 4. If still not found, try English.
116
124
if ($ output === null ) {
117
125
[$ file , $ parsedLine ] = $ this ->parseLine ($ line , 'en ' );
118
126
119
127
$ output = $ this ->getTranslationOutput ('en ' , $ file , $ parsedLine );
120
128
}
121
129
130
+ // 5. Fallback to the original line if no translation was found.
122
131
$ output ??= $ line ;
123
132
124
133
return $ this ->formatMessage ($ output , $ args );
125
134
}
126
135
127
136
/**
128
- * @return array |string|null
137
+ * @return list<string> |string|null
129
138
*/
130
139
protected function getTranslationOutput (string $ locale , string $ file , string $ parsedLine )
131
140
{
132
141
$ output = $ this ->language [$ locale ][$ file ][$ parsedLine ] ?? null ;
142
+
133
143
if ($ output !== null ) {
134
144
return $ output ;
135
145
}
136
146
137
- foreach (explode ('. ' , $ parsedLine ) as $ row ) {
138
- if (! isset ($ current )) {
139
- $ current = $ this ->language [$ locale ][$ file ] ?? null ;
140
- }
147
+ // Fallback: try to traverse dot notation
148
+ $ current = $ this ->language [$ locale ][$ file ] ?? null ;
149
+
150
+ if (is_array ($ current )) {
151
+ foreach (explode ('. ' , $ parsedLine ) as $ segment ) {
152
+ $ output = $ current [$ segment ] ?? null ;
153
+
154
+ if ($ output === null ) {
155
+ break ;
156
+ }
141
157
142
- $ output = $ current [ $ row ] ?? null ;
143
- if ( is_array ( $ output )) {
144
- $ current = $ output ;
158
+ if ( is_array ( $ output )) {
159
+ $ current = $ output ;
160
+ }
145
161
}
146
- }
147
162
148
- if ($ output !== null ) {
149
- return $ output ;
163
+ if ($ output !== null && ! is_array ($ output )) {
164
+ return $ output ;
165
+ }
150
166
}
151
167
152
- $ row = current ( explode ( ' . ' , $ parsedLine ));
153
- $ key = substr ( $ parsedLine , strlen ( $ row ) + 1 ) ;
168
+ // Final fallback: try two-level access manually
169
+ [ $ first , $ rest ] = explode ( ' . ' , $ parsedLine , 2 ) + [ '' , '' ] ;
154
170
155
- return $ this ->language [$ locale ][$ file ][$ row ][$ key ] ?? null ;
171
+ return $ this ->language [$ locale ][$ file ][$ first ][$ rest ] ?? null ;
156
172
}
157
173
158
174
/**
159
175
* Parses the language string which should include the
160
176
* filename as the first segment (separated by period).
177
+ *
178
+ * @return array{non-empty-string, non-empty-string}
161
179
*/
162
180
protected function parseLine (string $ line , string $ locale ): array
163
181
{
164
- $ file = substr ($ line , 0 , strpos ($ line , '. ' ));
165
- $ line = substr ($ line , strlen ($ file ) + 1 );
182
+ [$ file , $ line ] = explode ('. ' , $ line , 2 );
166
183
167
184
if (! isset ($ this ->language [$ locale ][$ file ]) || ! array_key_exists ($ line , $ this ->language [$ locale ][$ file ])) {
168
185
$ this ->load ($ file , $ locale );
@@ -174,10 +191,10 @@ protected function parseLine(string $line, string $locale): array
174
191
/**
175
192
* Advanced message formatting.
176
193
*
177
- * @param array |string $message
178
- * @param list< string> $args
194
+ * @param list<string> |string $message
195
+ * @param array<array-key, float|int| string> $args
179
196
*
180
- * @return array| string
197
+ * @return ($message is list< string> ? list<string> : string)
181
198
*/
182
199
protected function formatMessage ($ message , array $ args = [])
183
200
{
@@ -194,32 +211,27 @@ protected function formatMessage($message, array $args = [])
194
211
}
195
212
196
213
$ formatted = MessageFormatter::formatMessage ($ this ->locale , $ message , $ args );
214
+
197
215
if ($ formatted === false ) {
198
216
// Format again to get the error message.
199
217
try {
200
- $ fmt = new MessageFormatter ($ this ->locale , $ message );
201
- $ formatted = $ fmt ->format ($ args );
202
- $ fmtError = ' " ' . $ fmt ->getErrorMessage () . ' " ( ' . $ fmt ->getErrorCode () . ' ) ' ;
218
+ $ formatter = new MessageFormatter ($ this ->locale , $ message );
219
+ $ formatted = $ formatter ->format ($ args );
220
+ $ fmtError = sprintf ( ' "%s" (%d) ' , $ formatter ->getErrorMessage (), $ formatter ->getErrorCode ()) ;
203
221
} catch (IntlException $ e ) {
204
- $ fmtError = ' " ' . $ e ->getMessage () . ' " ( ' . $ e ->getCode () . ' ) ' ;
222
+ $ fmtError = sprintf ( ' "%s" (%d) ' , $ e ->getMessage (), $ e ->getCode ()) ;
205
223
}
206
224
207
- $ argsString = implode (
208
- ', ' ,
209
- array_map (static fn ($ element ): string => '" ' . $ element . '" ' , $ args ),
210
- );
211
- $ argsUrlEncoded = implode (
212
- ', ' ,
213
- array_map (static fn ($ element ): string => '" ' . rawurlencode ($ element ) . '" ' , $ args ),
214
- );
215
-
216
- log_message (
217
- 'error ' ,
218
- 'Language.invalidMessageFormat: $message: " ' . $ message
219
- . '", $args: ' . $ argsString
220
- . ' (urlencoded: ' . $ argsUrlEncoded . '), '
221
- . ' MessageFormatter Error: ' . $ fmtError ,
222
- );
225
+ $ argsAsString = sprintf ('"%s" ' , implode ('", " ' , $ args ));
226
+ $ urlEncodedArgs = sprintf ('"%s" ' , implode ('", " ' , array_map (rawurlencode (...), $ args )));
227
+
228
+ log_message ('error ' , sprintf (
229
+ 'Invalid message format: $message: "%s", $args: %s (urlencoded: %s), MessageFormatter Error: %s ' ,
230
+ $ message ,
231
+ $ argsAsString ,
232
+ $ urlEncodedArgs ,
233
+ $ fmtError ,
234
+ ));
223
235
224
236
return $ message . "\n【Warning】Also, invalid string(s) was passed to the Language class. See log file for details. " ;
225
237
}
@@ -232,7 +244,7 @@ protected function formatMessage($message, array $args = [])
232
244
* will return the file's contents, otherwise will merge with
233
245
* the existing language lines.
234
246
*
235
- * @return list<mixed>| null
247
+ * @return ($return is true ? LoadedStrings : null)
236
248
*/
237
249
protected function load (string $ file , string $ locale , bool $ return = false )
238
250
{
@@ -270,29 +282,35 @@ protected function load(string $file, string $locale, bool $return = false)
270
282
}
271
283
272
284
/**
273
- * A simple method for including files that can be
274
- * overridden during testing.
285
+ * A simple method for including files that can be overridden during testing.
286
+ *
287
+ * @return LoadedStrings
275
288
*/
276
289
protected function requireFile (string $ path ): array
277
290
{
278
291
$ files = service ('locator ' )->search ($ path , 'php ' , false );
279
292
$ strings = [];
280
293
281
294
foreach ($ files as $ file ) {
282
- // On some OS's we were seeing failures
283
- // on this command returning boolean instead
284
- // of array during testing, so we've removed
285
- // the require_once for now.
286
295
if (is_file ($ file )) {
287
- $ strings [] = require $ file ;
296
+ // On some OS, we were seeing failures on this command returning boolean instead
297
+ // of array during testing, so we've removed the require_once for now.
298
+ $ loadedStrings = require $ file ;
299
+
300
+ if (is_array ($ loadedStrings )) {
301
+ /** @var LoadedStrings $loadedStrings */
302
+ $ strings [] = $ loadedStrings ;
303
+ }
288
304
}
289
305
}
290
306
291
- if (isset ($ strings [1 ])) {
292
- $ string = array_shift ($ strings );
307
+ $ count = count ($ strings );
308
+
309
+ if ($ count > 1 ) {
310
+ $ base = array_shift ($ strings );
293
311
294
- $ strings = array_replace_recursive ($ string , ...$ strings );
295
- } elseif (isset ( $ strings [ 0 ]) ) {
312
+ $ strings = array_replace_recursive ($ base , ...$ strings );
313
+ } elseif ($ count === 1 ) {
296
314
$ strings = $ strings [0 ];
297
315
}
298
316
0 commit comments