Skip to content

feat: Support for localized strings #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,29 @@ Whenever an exception is caught by `Application::handle()`, it will show a beaut

![Exception Preview](https://user-images.githubusercontent.com/2908547/44401057-8b350880-a577-11e8-8ca6-20508d593d98.png "Exception trace")

### Autocompletion
## I18n Support

**adhocore/cli** also supports internationalisation. This is particularly useful if you are not very comfortable with English or if you are creating a framework or CLI application that could be used by people from a variety of backgrounds.

By default, all the texts generated by our system are in English. But you can easily modify them by defining your translations as follows

```php
\Ahc\Application::addLocale('fr', [
'Only last argument can be variadic' => 'Seul le dernier argument peut être variadique',
], true);
```

You can also change the default English text to make the description more explicit if you wish.

```php
\Ahc\Application::addLocale('en', [
'Show help' => 'Shows helpful information about a command',
]);
```

vous pouvez trouver toutes les clés de traduction supportées par le paquet dans cette gist : https://gist.github.com/dimtrovich/1597c16d5c74334e68eef15a4e7ba3fd

## Autocompletion

Any console applications that are built on top of **adhocore/cli** can entertain autocomplete of commands and options in zsh shell with oh-my-zsh.

Expand Down
38 changes: 33 additions & 5 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Ahc\Cli;

use Ahc\Cli\Exception\InvalidArgumentException;
use Ahc\Cli\Helper\InflectsString;
use Ahc\Cli\Helper\OutputHelper;
use Ahc\Cli\Input\Command;
use Ahc\Cli\IO\Interactor;
Expand All @@ -28,7 +29,6 @@
use function is_array;
use function is_int;
use function method_exists;
use function sprintf;

/**
* A cli application.
Expand All @@ -40,6 +40,25 @@
*/
class Application
{
use InflectsString;

/**
* Locale of CLI.
*/
public static $locale = 'en';

/**
* list of translations for each supported locale
*
* @var array<string, array>
*
* @example
* ```php
* ['locale' => ['key1' => 'value1', 'key2' => 'value2']]
* ```
*/
public static $locales = [];

/** @var Command[] */
protected array $commands = [];

Expand Down Expand Up @@ -130,6 +149,15 @@ public function logo(?string $logo = null)
return $this;
}

public static function addLocale(string $locale, array $texts, bool $default = false)
{
if ($default) {
self::$locale = $locale;
}

self::$locales[$locale] = $texts;
}

/**
* Add a command by its name desc alias etc and return command.
*/
Expand Down Expand Up @@ -161,7 +189,7 @@ public function add(Command $command, string $alias = '', bool $default = false)
$this->aliases[$alias] ??
null
) {
throw new InvalidArgumentException(sprintf('Command "%s" already added', $name));
throw new InvalidArgumentException($this->translate('Command "%s" already added', [$name]));
}

if ($alias) {
Expand Down Expand Up @@ -190,7 +218,7 @@ public function add(Command $command, string $alias = '', bool $default = false)
public function defaultCommand(string $commandName): self
{
if (!isset($this->commands[$commandName])) {
throw new InvalidArgumentException(sprintf('Command "%s" does not exist', $commandName));
throw new InvalidArgumentException($this->translate('Command "%s" does not exist', [$commandName]));
}

$this->default = $commandName;
Expand Down Expand Up @@ -386,8 +414,8 @@ public function showHelp(): mixed
public function showDefaultHelp(): mixed
{
$writer = $this->io()->writer();
$header = "{$this->name}, version {$this->version}";
$footer = 'Run `<command> --help` for specific help';
$header = "{$this->name}, {$this->translate('version')} {$this->version}";
$footer = $this->translate('Run `<command> --help` for specific help');

if ($this->logo) {
$writer->logo($this->logo, true);
Expand Down
13 changes: 13 additions & 0 deletions src/Helper/InflectsString.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Ahc\Cli\Helper;

use Ahc\Cli\Application;

use function lcfirst;
use function mb_strwidth;
use function mb_substr;
Expand Down Expand Up @@ -75,4 +77,15 @@ public function substr(string $string, int $start, ?int $length = null): string

return substr($string, $start, $length);
}

/**
* Translates a message using the translator.
*/
public static function translate(string $text, array $args = []): string
Copy link
Owner

@adhocore adhocore Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could be a simple function t() or __() under namespace Ahc\cli (not method) :)

then can be imported as use function Ahc\Cli\t

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh ok but i had striked this one and left another note 😀

Copy link
Contributor Author

@dimtrovich dimtrovich Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand. Can you please explain?
the function t() simple or a method in the InflectsString trait. because now I don't understand.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, now I understand better. I hadn't paid attention to the link you provided. I'm going to manage the argument positioning

So, in the case of the t() function, should we leave it or go back to the previous translate method?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its ok as it is, we just need to consider the sprintf for wrapping up

Copy link
Contributor Author

@dimtrovich dimtrovich Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, shouldn't positioning only be managed for texts with more than one argument?

For example, this is relevant for The current (%d) is greater than the total (%d). but much less so for Command %s not found.

I would like to leave Command %s not found unchanged but change The current (%d) is greater than the total (%d). to The current (%1$d) is greater than the total (%2$d)..

or we'll make everything the same

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya thats fine

Copy link
Contributor Author

@dimtrovich dimtrovich Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

gist sample was also updated

{
$translations = Application::$locales[Application::$locale] ?? [];
$text = $translations[$text] ?? $text;

return sprintf($text, ...$args);
}
}
18 changes: 10 additions & 8 deletions src/Helper/OutputHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
*/
class OutputHelper
{
use InflectsString;

protected Writer $writer;

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

$this->writer->colors(
"{$eClass} <red>{$e->getMessage()}</end><eol/>" .
"(thrown in <yellow>{$e->getFile()}</end><white>:{$e->getLine()})</end>"
"({$this->translate('thrown in')} <yellow>{$e->getFile()}</end><white>:{$e->getLine()})</end>"
);

// @codeCoverageIgnoreStart
Expand All @@ -87,7 +89,7 @@ public function printTrace(Throwable $e): void
}
// @codeCoverageIgnoreEnd

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

foreach ($e->getTrace() as $i => $trace) {
$trace += ['class' => '', 'type' => '', 'function' => '', 'file' => '', 'line' => '', 'args' => []];
Expand All @@ -97,7 +99,7 @@ public function printTrace(Throwable $e): void
$traceStr .= " <comment>$i)</end> <red>$symbol</end><comment>($args)</end>";
if ('' !== $trace['file']) {
$file = realpath($trace['file']);
$traceStr .= "<eol/> <yellow>at $file</end><white>:{$trace['line']}</end><eol/>";
$traceStr .= "<eol/> <yellow>{$this->translate('at')} $file</end><white>:{$trace['line']}</end><eol/>";
}
}

Expand Down Expand Up @@ -185,7 +187,7 @@ protected function showHelp(string $for, array $items, string $header = '', stri
$this->writer->help_header($header, true);
}

$this->writer->eol()->help_category($for . ':', true);
$this->writer->eol()->help_category($this->translate($for) . ':', true);

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

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

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

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

return $this;
}
Expand All @@ -261,11 +263,11 @@ public function showCommandNotFound(string $attempted, array $available): self
}
}

$this->writer->error("Command $attempted not found", true);
$this->writer->error($this->translate('Command %s not found', [$attempted]), true);
if ($closest) {
asort($closest);
$closest = key($closest);
$this->writer->bgRed("Did you mean $closest?", true);
$this->writer->bgRed($this->translate('Did you mean %s?', [$closest]), true);
}

return $this;
Expand Down
10 changes: 6 additions & 4 deletions src/Helper/Shell.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
*/
class Shell
{
use InflectsString;

const STDIN_DESCRIPTOR_KEY = 0;
const STDOUT_DESCRIPTOR_KEY = 1;
const STDERR_DESCRIPTOR_KEY = 2;
Expand Down Expand Up @@ -99,7 +101,7 @@ public function __construct(protected string $command, protected ?string $input
{
// @codeCoverageIgnoreStart
if (!function_exists('proc_open')) {
throw new RuntimeException('Required proc_open could not be found in your PHP setup.');
throw new RuntimeException($this->translate('Required proc_open could not be found in your PHP setup.'));
}
// @codeCoverageIgnoreEnd

Expand Down Expand Up @@ -181,7 +183,7 @@ protected function checkTimeout(): void
if ($executionDuration > $this->processTimeout) {
$this->kill();

throw new RuntimeException('Timeout occurred, process terminated.');
throw new RuntimeException($this->translate('Timeout occurred, process terminated.'));
}
// @codeCoverageIgnoreStart
}
Expand Down Expand Up @@ -216,7 +218,7 @@ public function setOptions(
public function execute(bool $async = false, ?array $stdin = null, ?array $stdout = null, ?array $stderr = null): self
{
if ($this->isRunning()) {
throw new RuntimeException('Process is already running.');
throw new RuntimeException($this->translate('Process is already running.'));
}

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

// @codeCoverageIgnoreStart
if (!is_resource($this->process)) {
throw new RuntimeException('Bad program could not be started.');
throw new RuntimeException($this->translate('Bad program could not be started.'));
}
// @codeCoverageIgnoreEnd

Expand Down
7 changes: 5 additions & 2 deletions src/IO/Interactor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Ahc\Cli\IO;

use Ahc\Cli\Helper\InflectsString;
use Ahc\Cli\Input\Reader;
use Ahc\Cli\Output\Writer;
use Throwable;
Expand Down Expand Up @@ -195,6 +196,8 @@
*/
class Interactor
{
use InflectsString;

protected Reader $reader;
protected Writer $writer;

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

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

$label = $multi ? 'Choices (comma separated)' : 'Choice';
$label = $this->translate($multi ? 'Choices (comma separated)' : 'Choice');

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

Expand Down
18 changes: 8 additions & 10 deletions src/Input/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ public function __construct(
*/
protected function defaults(): self
{
$this->option('-h, --help', 'Show help')->on([$this, 'showHelp']);
$this->option('-V, --version', 'Show version')->on([$this, 'showVersion']);
$this->option('-v, --verbosity', 'Verbosity level', null, 0)->on(
$this->option('-h, --help', $this->translate('Show help'))->on([$this, 'showHelp']);
$this->option('-V, --version', $this->translate('Show version'))->on([$this, 'showVersion']);
$this->option('-v, --verbosity', $this->translate('Verbosity level'), null, 0)->on(
fn () => $this->set('verbosity', ($this->verbosity ?? 0) + 1) && false
);

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

if ($this->_argVariadic) {
throw new InvalidParameterException('Only last argument can be variadic');
throw new InvalidParameterException($this->translate('Only last argument can be variadic'));
}

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

// Has some value, error!
if ($values) {
throw new RuntimeException(
sprintf('Option "%s" not registered', $arg)
);
throw new RuntimeException($this->translate('Option "%s" not registered', [$arg]));
}

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

$io->help_header("Command {$this->_name}, version {$this->_version}", true)->eol();
$io->help_header("{$this->translate('Command')} {$this->_name}, {$this->translate('version')} {$this->_version}", true)->eol();
$io->help_summary($this->_desc, true)->eol();
$io->help_text('Usage: ')->help_example("{$this->_name} [OPTIONS...] [ARGUMENTS...]", true);
$io->help_text("{$this->translate('Usage')}: ")->help_example("{$this->_name} {$this->translate('[OPTIONS...] [ARGUMENTS...]')}", true);

$helper
->showArgumentsHelp($this->allArguments())
->showOptionsHelp($this->allOptions(), '', 'Legend: <required> [optional] variadic...');
->showOptionsHelp($this->allOptions(), '', $this->translate('Legend: <required> [optional] variadic...'));

if ($this->_usage) {
$helper->showUsage($this->_usage);
Expand Down
3 changes: 1 addition & 2 deletions src/Input/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use function json_encode;
use function ltrim;
use function strpos;
use function sprintf;

/**
* Cli Parameter.
Expand Down Expand Up @@ -84,7 +83,7 @@ public function desc(bool $withDefault = false): string
return $this->desc;
}

return ltrim(sprintf('%s [default: %s]', $this->desc, json_encode($this->default)));
return ltrim($this->translate('%s [default: %s]', [$this->desc, json_encode($this->default)]));
}

/**
Expand Down
Loading