Skip to content

Commit 26a7f14

Browse files
committed
feat: add support for localized strings
1 parent bacbbda commit 26a7f14

12 files changed

Lines changed: 225 additions & 39 deletions

File tree

src/Helper/InflectsString.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Ahc\Cli\Helper;
1313

14+
use Ahc\Cli\Translations\Translator;
15+
1416
use function lcfirst;
1517
use function mb_strwidth;
1618
use function mb_substr;
@@ -30,6 +32,8 @@
3032
*/
3133
trait InflectsString
3234
{
35+
private static ?Translator $translator = null;
36+
3337
/**
3438
* Convert a string to camel case.
3539
*/
@@ -75,4 +79,16 @@ public function substr(string $string, int $start, ?int $length = null): string
7579

7680
return substr($string, $start, $length);
7781
}
82+
83+
/**
84+
* Translates a message using the translator.
85+
*/
86+
public static function translate(string $key, array $args = []): string
87+
{
88+
if (self::$translator === null) {
89+
self::$translator = new Translator();
90+
}
91+
92+
return self::$translator->getMessage($key, $args);
93+
}
7894
}

src/Helper/OutputHelper.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
*/
5959
class OutputHelper
6060
{
61+
use InflectsString;
62+
6163
protected Writer $writer;
6264

6365
/** @var int Max width of command name */
@@ -77,7 +79,7 @@ public function printTrace(Throwable $e): void
7779

7880
$this->writer->colors(
7981
"{$eClass} <red>{$e->getMessage()}</end><eol/>" .
80-
"(thrown in <yellow>{$e->getFile()}</end><white>:{$e->getLine()})</end>"
82+
"({$this->translate('thrownIn')} <yellow>{$e->getFile()}</end><white>:{$e->getLine()})</end>"
8183
);
8284

8385
// @codeCoverageIgnoreStart
@@ -87,7 +89,7 @@ public function printTrace(Throwable $e): void
8789
}
8890
// @codeCoverageIgnoreEnd
8991

90-
$traceStr = '<eol/><eol/><bold>Stack Trace:</end><eol/><eol/>';
92+
$traceStr = "<eol/><eol/><bold>{$this->translate('stackTrace')}:</end><eol/><eol/>";
9193

9294
foreach ($e->getTrace() as $i => $trace) {
9395
$trace += ['class' => '', 'type' => '', 'function' => '', 'file' => '', 'line' => '', 'args' => []];
@@ -185,7 +187,7 @@ protected function showHelp(string $for, array $items, string $header = '', stri
185187
$this->writer->help_header($header, true);
186188
}
187189

188-
$this->writer->eol()->help_category($for . ':', true);
190+
$this->writer->eol()->help_category($this->translate(strtolower($for)) . ':', true);
189191

190192
if (empty($items)) {
191193
$this->writer->help_text(' (n/a)', true);
@@ -229,7 +231,7 @@ public function showUsage(string $usage): self
229231
$usage = str_replace('$0', $_SERVER['argv'][0] ?? '[cmd]', $usage);
230232

231233
if (!str_contains($usage, ' ## ')) {
232-
$this->writer->eol()->help_category('Usage Examples:', true)->colors($usage)->eol();
234+
$this->writer->eol()->help_category($this->translate('usageExamples') . ':', true)->colors($usage)->eol();
233235

234236
return $this;
235237
}
@@ -246,7 +248,7 @@ public function showUsage(string $usage): self
246248
return str_pad('# ', $maxlen - array_shift($lines), ' ', STR_PAD_LEFT);
247249
}, $usage);
248250

249-
$this->writer->eol()->help_category('Usage Examples:', true)->colors($usage)->eol();
251+
$this->writer->eol()->help_category($this->translate('usageExamples') . ':', true)->colors($usage)->eol();
250252

251253
return $this;
252254
}
@@ -261,11 +263,11 @@ public function showCommandNotFound(string $attempted, array $available): self
261263
}
262264
}
263265

264-
$this->writer->error("Command $attempted not found", true);
266+
$this->writer->error($this->translate('commandNotFound', [$attempted]), true);
265267
if ($closest) {
266268
asort($closest);
267269
$closest = key($closest);
268-
$this->writer->bgRed("Did you mean $closest?", true);
270+
$this->writer->bgRed($this->translate('commandSuggestion', [$closest]), true);
269271
}
270272

271273
return $this;

src/Helper/Shell.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
*/
3838
class Shell
3939
{
40+
use InflectsString;
41+
4042
const STDIN_DESCRIPTOR_KEY = 0;
4143
const STDOUT_DESCRIPTOR_KEY = 1;
4244
const STDERR_DESCRIPTOR_KEY = 2;
@@ -99,7 +101,7 @@ public function __construct(protected string $command, protected ?string $input
99101
{
100102
// @codeCoverageIgnoreStart
101103
if (!function_exists('proc_open')) {
102-
throw new RuntimeException('Required proc_open could not be found in your PHP setup.');
104+
throw new RuntimeException($this->translate('procOpenMissing'));
103105
}
104106
// @codeCoverageIgnoreEnd
105107

@@ -181,7 +183,7 @@ protected function checkTimeout(): void
181183
if ($executionDuration > $this->processTimeout) {
182184
$this->kill();
183185

184-
throw new RuntimeException('Timeout occurred, process terminated.');
186+
throw new RuntimeException($this->translate('timeoutOccured'));
185187
}
186188
// @codeCoverageIgnoreStart
187189
}
@@ -216,7 +218,7 @@ public function setOptions(
216218
public function execute(bool $async = false, ?array $stdin = null, ?array $stdout = null, ?array $stderr = null): self
217219
{
218220
if ($this->isRunning()) {
219-
throw new RuntimeException('Process is already running.');
221+
throw new RuntimeException($this->translate('processAlreadyRun'));
220222
}
221223

222224
$this->descriptors = $this->prepareDescriptors($stdin, $stdout, $stderr);
@@ -234,7 +236,7 @@ public function execute(bool $async = false, ?array $stdin = null, ?array $stdou
234236

235237
// @codeCoverageIgnoreStart
236238
if (!is_resource($this->process)) {
237-
throw new RuntimeException('Bad program could not be started.');
239+
throw new RuntimeException($this->translate('badProgram'));
238240
}
239241
// @codeCoverageIgnoreEnd
240242

src/IO/Interactor.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Ahc\Cli\IO;
1313

14+
use Ahc\Cli\Helper\InflectsString;
1415
use Ahc\Cli\Input\Reader;
1516
use Ahc\Cli\Output\Writer;
1617
use Throwable;
@@ -195,6 +196,8 @@
195196
*/
196197
class Interactor
197198
{
199+
use InflectsString;
200+
198201
protected Reader $reader;
199202
protected Writer $writer;
200203

@@ -312,7 +315,7 @@ public function choices(string $text, array $choices, $default = null, bool $cas
312315
*/
313316
public function prompt(string $text, $default = null, ?callable $fn = null, int $retry = 3): mixed
314317
{
315-
$error = 'Invalid value. Please try again!';
318+
$error = $this->translate('promptInvalidValue');
316319
$hidden = func_get_args()[4] ?? false;
317320
$readFn = ['read', 'readHidden'][(int) $hidden];
318321

@@ -370,7 +373,7 @@ protected function listOptions(array $choices, $default = null, bool $multi = fa
370373
$this->writer->eol()->choice(str_pad(" [$choice]", $maxLen + 6))->answer($desc);
371374
}
372375

373-
$label = $multi ? 'Choices (comma separated)' : 'Choice';
376+
$label = $this->translate($multi ? 'choices' : 'choice');
374377

375378
$this->writer->eol()->question($label);
376379

src/Input/Command.php

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ public function __construct(
8383
*/
8484
protected function defaults(): self
8585
{
86-
$this->option('-h, --help', 'Show help')->on([$this, 'showHelp']);
87-
$this->option('-V, --version', 'Show version')->on([$this, 'showVersion']);
88-
$this->option('-v, --verbosity', 'Verbosity level', null, 0)->on(
86+
$this->option('-h, --help', $this->translate('showHelp'))->on([$this, 'showHelp']);
87+
$this->option('-V, --version', $this->translate('showVersion'))->on([$this, 'showVersion']);
88+
$this->option('-v, --verbosity', $this->translate('verbosityLevel'), null, 0)->on(
8989
fn () => $this->set('verbosity', ($this->verbosity ?? 0) + 1) && false
9090
);
9191

@@ -196,7 +196,7 @@ public function argument(string $raw, string $desc = '', $default = null): self
196196
$argument = new Argument($raw, $desc, $default);
197197

198198
if ($this->_argVariadic) {
199-
throw new InvalidParameterException('Only last argument can be variadic');
199+
throw new InvalidParameterException($this->translate('argumentVariadic'));
200200
}
201201

202202
if ($argument->variadic()) {
@@ -303,9 +303,7 @@ protected function handleUnknown(string $arg, ?string $value = null): mixed
303303

304304
// Has some value, error!
305305
if ($values) {
306-
throw new RuntimeException(
307-
sprintf('Option "%s" not registered', $arg)
308-
);
306+
throw new RuntimeException($this->translate('optionNotRegistered', [$arg]));
309307
}
310308

311309
// Has no value, show help!
@@ -358,13 +356,13 @@ public function showDefaultHelp(): mixed
358356
$io->logo($logo, true);
359357
}
360358

361-
$io->help_header("Command {$this->_name}, version {$this->_version}", true)->eol();
359+
$io->help_header("{$this->translate('command')} {$this->_name}, {$this->translate('version')} {$this->_version}", true)->eol();
362360
$io->help_summary($this->_desc, true)->eol();
363-
$io->help_text('Usage: ')->help_example("{$this->_name} [OPTIONS...] [ARGUMENTS...]", true);
361+
$io->help_text("{$this->translate('usage')}: ")->help_example("{$this->_name} {$this->translate('helpExample')}", true);
364362

365363
$helper
366364
->showArgumentsHelp($this->allArguments())
367-
->showOptionsHelp($this->allOptions(), '', 'Legend: <required> [optional] variadic...');
365+
->showOptionsHelp($this->allOptions(), '', $this->translate('optionHelp'));
368366

369367
if ($this->_usage) {
370368
$helper->showUsage($this->_usage);

src/Input/Parameter.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use function json_encode;
1717
use function ltrim;
1818
use function strpos;
19-
use function sprintf;
2019

2120
/**
2221
* Cli Parameter.
@@ -84,7 +83,7 @@ public function desc(bool $withDefault = false): string
8483
return $this->desc;
8584
}
8685

87-
return ltrim(sprintf('%s [default: %s]', $this->desc, json_encode($this->default)));
86+
return ltrim($this->translate('descWithDefault', [$this->desc, json_encode($this->default)]));
8887
}
8988

9089
/**

src/Input/Parser.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Ahc\Cli\Exception\InvalidParameterException;
1515
use Ahc\Cli\Exception\RuntimeException;
16+
use Ahc\Cli\Helper\InflectsString;
1617
use Ahc\Cli\Helper\Normalizer;
1718
use InvalidArgumentException;
1819

@@ -37,6 +38,8 @@
3738
*/
3839
abstract class Parser
3940
{
41+
use InflectsString;
42+
4043
/** @var string|null The last seen variadic option name */
4144
protected ?string $_lastVariadic = null;
4245

@@ -264,9 +267,9 @@ public function unset(string $name): self
264267
protected function ifAlreadyRegistered(Parameter $param): void
265268
{
266269
if ($this->registered($param->attributeName())) {
267-
throw new InvalidParameterException(sprintf(
268-
'The parameter "%s" is already registered',
269-
$param instanceof Option ? $param->long() : $param->name()
270+
throw new InvalidParameterException($this->translate(
271+
'parameterAlreadyRegistered',
272+
[$param instanceof Option ? $param->long() : $param->name()]
270273
));
271274
}
272275
}

src/Output/Color.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212
namespace Ahc\Cli\Output;
1313

1414
use Ahc\Cli\Exception\InvalidArgumentException;
15+
use Ahc\Cli\Helper\InflectsString;
1516

1617
use function array_intersect_key;
1718
use function constant;
1819
use function defined;
1920
use function lcfirst;
2021
use function method_exists;
2122
use function preg_match_all;
22-
use function sprintf;
2323
use function str_ireplace;
2424
use function str_replace;
2525
use function stripos;
@@ -39,6 +39,8 @@
3939
*/
4040
class Color
4141
{
42+
use InflectsString;
43+
4244
const BLACK = 30;
4345
const RED = 31;
4446
const GREEN = 32;
@@ -192,12 +194,12 @@ public static function style(string $name, array $style): void
192194
$style = array_intersect_key($style, $allow);
193195

194196
if (empty($style)) {
195-
throw new InvalidArgumentException('Trying to set empty or invalid style');
197+
throw new InvalidArgumentException(self::translate('usingInvalidStyle'));
196198
}
197199

198200
$invisible = (isset($style['bg']) && isset($style['fg']) && $style['bg'] === $style['fg']);
199201
if ($invisible && method_exists(static::class, $name)) {
200-
throw new InvalidArgumentException('Built-in styles cannot be invisible');
202+
throw new InvalidArgumentException(self::translate('styleInvisible'));
201203
}
202204

203205
static::$styles[$name] = $style;
@@ -214,7 +216,7 @@ public static function style(string $name, array $style): void
214216
public function __call(string $name, array $arguments): string
215217
{
216218
if (!isset($arguments[0])) {
217-
throw new InvalidArgumentException('Text required');
219+
throw new InvalidArgumentException($this->translate('textRequired'));
218220
}
219221

220222
[$name, $text, $style] = $this->parseCall($name, $arguments);
@@ -229,7 +231,7 @@ public function __call(string $name, array $arguments): string
229231
}
230232

231233
if (!method_exists($this, $name)) {
232-
throw new InvalidArgumentException(sprintf('Style "%s" not defined', $name));
234+
throw new InvalidArgumentException($this->translate('undefinedStyle', [$name]));
233235
}
234236

235237
return $this->{$name}($text, $style);

src/Output/ProgressBar.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Ahc\Cli\Output;
1313

14+
use Ahc\Cli\Helper\InflectsString;
1415
use Ahc\Cli\Helper\Terminal;
1516
use UnexpectedValueException;
1617

@@ -31,6 +32,8 @@
3132
*/
3233
class ProgressBar
3334
{
35+
use InflectsString;
36+
3437
/**
3538
* The total number of items involved.
3639
*/
@@ -133,7 +136,7 @@ public function option(string|array $key, ?string $value = null): self
133136
{
134137
if (is_string($key)) {
135138
if (empty($value)) {
136-
throw new UnexpectedValueException('configuration option value is required');
139+
throw new UnexpectedValueException($this->translate('configOptionMissing'));
137140
}
138141

139142
$key = [$key => $value];
@@ -163,11 +166,11 @@ public function current(int $current, string $label = '')
163166
{
164167
if ($this->total == 0) {
165168
// Avoid dividing by 0
166-
throw new UnexpectedValueException('The progress total must be greater than zero.');
169+
throw new UnexpectedValueException($this->translate('progressbarTotalMin'));
167170
}
168-
171+
169172
if ($current > $this->total) {
170-
throw new UnexpectedValueException(sprintf('The current (%d) is greater than the total (%d).', $current, $this->total));
173+
throw new UnexpectedValueException($this->translate('progressbarCurrentMax', [$current, $this->total]));
171174
}
172175

173176
$this->drawProgressBar($current, $label);

src/Output/Table.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
use function is_array;
2525
use function max;
2626
use function reset;
27-
use function sprintf;
2827
use function str_repeat;
2928
use function trim;
3029

@@ -107,7 +106,7 @@ protected function normalize(array $rows): array
107106

108107
if (!is_array($head)) {
109108
throw new InvalidArgumentException(
110-
sprintf('Rows must be array of assoc arrays, %s given', gettype($head))
109+
$this->translate('invalidTableRowsType', [gettype($head)])
111110
);
112111
}
113112

0 commit comments

Comments
 (0)