Skip to content

Commit 3695de9

Browse files
committed
refactor: fix phpdoc and improve code in Language
1 parent 59de29e commit 3695de9

File tree

8 files changed

+191
-250
lines changed

8 files changed

+191
-250
lines changed

system/Common.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,8 @@ function is_windows(?bool $mock = null): bool
729729
* A convenience method to translate a string or array of them and format
730730
* the result with the intl extension's MessageFormatter.
731731
*
732+
* @param array<array-key, float|int|string> $args
733+
*
732734
* @return list<string>|string
733735
*/
734736
function lang(string $line, array $args = [], ?string $locale = null)

system/Language/Language.php

Lines changed: 80 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
*
2222
* Locale-based, built on top of PHP internationalization.
2323
*
24+
* @phpstan-type LoadedStrings array<string, array<string, array<string, string>|string>|string|list<string>>
25+
*
2426
* @see \CodeIgniter\Language\LanguageTest
2527
*/
2628
class Language
@@ -30,20 +32,19 @@ class Language
3032
* from files for faster retrieval on
3133
* second use.
3234
*
33-
* @var array
35+
* @var array<non-empty-string, array<non-empty-string, LoadedStrings>>
3436
*/
3537
protected $language = [];
3638

3739
/**
38-
* The current language/locale to work with.
40+
* The current locale to work with.
3941
*
40-
* @var string
42+
* @var non-empty-string
4143
*/
4244
protected $locale;
4345

4446
/**
45-
* Boolean value whether the intl
46-
* libraries exist on the system.
47+
* Boolean value whether the `intl` extension exists on the system.
4748
*
4849
* @var bool
4950
*/
@@ -53,10 +54,13 @@ class Language
5354
* Stores filenames that have been
5455
* loaded so that we don't load them again.
5556
*
56-
* @var array
57+
* @var array<non-empty-string, list<non-empty-string>>
5758
*/
5859
protected $loadedFiles = [];
5960

61+
/**
62+
* @param non-empty-string $locale
63+
*/
6064
public function __construct(string $locale)
6165
{
6266
$this->locale = $locale;
@@ -69,6 +73,8 @@ public function __construct(string $locale)
6973
/**
7074
* Sets the current locale to use when performing string lookups.
7175
*
76+
* @param non-empty-string|null $locale
77+
*
7278
* @return $this
7379
*/
7480
public function setLocale(?string $locale = null)
@@ -89,80 +95,91 @@ public function getLocale(): string
8995
* Parses the language string for a file, loads the file, if necessary,
9096
* getting the line.
9197
*
98+
* @param array<array-key, float|int|string> $args
99+
*
92100
* @return list<string>|string
93101
*/
94102
public function getLine(string $line, array $args = [])
95103
{
96-
// if no file is given, just parse the line
104+
// 1. Format the line as-is if it does not have a file.
97105
if (! str_contains($line, '.')) {
98106
return $this->formatMessage($line, $args);
99107
}
100108

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.
103110
[$file, $parsedLine] = $this->parseLine($line, $this->locale);
104111

105112
$output = $this->getTranslationOutput($this->locale, $file, $parsedLine);
106113

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, '-')) {
108116
[$locale] = explode('-', $this->locale, 2);
109117

110118
[$file, $parsedLine] = $this->parseLine($line, $locale);
111119

112120
$output = $this->getTranslationOutput($locale, $file, $parsedLine);
113121
}
114122

115-
// if still not found, try English
123+
// 4. If still not found, try English.
116124
if ($output === null) {
117125
[$file, $parsedLine] = $this->parseLine($line, 'en');
118126

119127
$output = $this->getTranslationOutput('en', $file, $parsedLine);
120128
}
121129

130+
// 5. Fallback to the original line if no translation was found.
122131
$output ??= $line;
123132

124133
return $this->formatMessage($output, $args);
125134
}
126135

127136
/**
128-
* @return array|string|null
137+
* @return list<string>|string|null
129138
*/
130139
protected function getTranslationOutput(string $locale, string $file, string $parsedLine)
131140
{
132141
$output = $this->language[$locale][$file][$parsedLine] ?? null;
142+
133143
if ($output !== null) {
134144
return $output;
135145
}
136146

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+
}
141157

142-
$output = $current[$row] ?? null;
143-
if (is_array($output)) {
144-
$current = $output;
158+
if (is_array($output)) {
159+
$current = $output;
160+
}
145161
}
146-
}
147162

148-
if ($output !== null) {
149-
return $output;
163+
if ($output !== null && ! is_array($output)) {
164+
return $output;
165+
}
150166
}
151167

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) + ['', ''];
154170

155-
return $this->language[$locale][$file][$row][$key] ?? null;
171+
return $this->language[$locale][$file][$first][$rest] ?? null;
156172
}
157173

158174
/**
159175
* Parses the language string which should include the
160176
* filename as the first segment (separated by period).
177+
*
178+
* @return array{non-empty-string, non-empty-string}
161179
*/
162180
protected function parseLine(string $line, string $locale): array
163181
{
164-
$file = substr($line, 0, strpos($line, '.'));
165-
$line = substr($line, strlen($file) + 1);
182+
[$file, $line] = explode('.', $line, 2);
166183

167184
if (! isset($this->language[$locale][$file]) || ! array_key_exists($line, $this->language[$locale][$file])) {
168185
$this->load($file, $locale);
@@ -174,10 +191,10 @@ protected function parseLine(string $line, string $locale): array
174191
/**
175192
* Advanced message formatting.
176193
*
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
179196
*
180-
* @return array|string
197+
* @return ($message is list<string> ? list<string> : string)
181198
*/
182199
protected function formatMessage($message, array $args = [])
183200
{
@@ -194,32 +211,27 @@ protected function formatMessage($message, array $args = [])
194211
}
195212

196213
$formatted = MessageFormatter::formatMessage($this->locale, $message, $args);
214+
197215
if ($formatted === false) {
198216
// Format again to get the error message.
199217
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());
203221
} catch (IntlException $e) {
204-
$fmtError = '"' . $e->getMessage() . '" (' . $e->getCode() . ')';
222+
$fmtError = sprintf('"%s" (%d)', $e->getMessage(), $e->getCode());
205223
}
206224

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+
));
223235

224236
return $message . "\n【Warning】Also, invalid string(s) was passed to the Language class. See log file for details.";
225237
}
@@ -232,7 +244,7 @@ protected function formatMessage($message, array $args = [])
232244
* will return the file's contents, otherwise will merge with
233245
* the existing language lines.
234246
*
235-
* @return list<mixed>|null
247+
* @return ($return is true ? LoadedStrings : null)
236248
*/
237249
protected function load(string $file, string $locale, bool $return = false)
238250
{
@@ -270,29 +282,35 @@ protected function load(string $file, string $locale, bool $return = false)
270282
}
271283

272284
/**
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
275288
*/
276289
protected function requireFile(string $path): array
277290
{
278291
$files = service('locator')->search($path, 'php', false);
279292
$strings = [];
280293

281294
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.
286295
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+
}
288304
}
289305
}
290306

291-
if (isset($strings[1])) {
292-
$string = array_shift($strings);
307+
$count = count($strings);
308+
309+
if ($count > 1) {
310+
$base = array_shift($strings);
293311

294-
$strings = array_replace_recursive($string, ...$strings);
295-
} elseif (isset($strings[0])) {
312+
$strings = array_replace_recursive($base, ...$strings);
313+
} elseif ($count === 1) {
296314
$strings = $strings[0];
297315
}
298316

system/Language/en/Language.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313

1414
// "Language" language settings
1515
return [
16+
// @deprecated v4.6.3 - never used
1617
'invalidMessageFormat' => 'Invalid message format: "{0}", args: "{1}"',
1718
];

system/Test/Mock/MockLanguage.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515

1616
use CodeIgniter\Language\Language;
1717

18+
/**
19+
* @phpstan-import-type LoadedStrings from Language
20+
*/
1821
class MockLanguage extends Language
1922
{
2023
/**
2124
* Stores the data that should be
2225
* returned by the 'requireFile()' method.
2326
*
24-
* @var mixed
27+
* @var LoadedStrings|null
2528
*/
2629
protected $data;
2730

@@ -30,18 +33,24 @@ class MockLanguage extends Language
3033
* 'requireFile()' method to allow easy overrides
3134
* during testing.
3235
*
36+
* @param LoadedStrings $data
37+
*
3338
* @return $this
3439
*/
3540
public function setData(string $file, array $data, ?string $locale = null)
3641
{
3742
$this->language[$locale ?? $this->locale][$file] = $data;
3843

44+
$this->data = $data;
45+
3946
return $this;
4047
}
4148

4249
/**
4350
* Provides an override that allows us to set custom
4451
* data to be returned easily during testing.
52+
*
53+
* @return LoadedStrings
4554
*/
4655
protected function requireFile(string $path): array
4756
{

tests/_support/Language/SecondMockLanguage.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,27 @@
1515

1616
use CodeIgniter\Language\Language;
1717

18+
/**
19+
* @phpstan-import-type LoadedStrings from Language
20+
*/
1821
class SecondMockLanguage extends Language
1922
{
2023
/**
2124
* Expose the protected *load* method
25+
*
26+
* @return ($return is true ? LoadedStrings : null)
2227
*/
23-
public function loadem(string $file, string $locale = 'en', bool $return = false)
28+
public function loadem(string $file, string $locale = 'en', bool $return = false): ?array
2429
{
2530
return $this->load($file, $locale, $return);
2631
}
2732

2833
/**
2934
* Expose the loaded language files
35+
*
36+
* @return list<non-empty-string>
3037
*/
31-
public function loaded(string $locale = 'en')
38+
public function loaded(string $locale = 'en'): array
3239
{
3340
return $this->loadedFiles[$locale];
3441
}

0 commit comments

Comments
 (0)