Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Deprecated command aliases.** A command lists old names in `$deprecatedCommandAliases`;
they stay registered, and invoking the command by one prints a single line naming the
canonical command before anything runs. The canonical name and `$commandAliases` entries
print nothing. The base `Command` composes the new `WarnsOnDeprecatedAlias` trait, and a
command on any other base can `use` it directly. This is the one sanctioned way for a bare
name to stay registered beside a `laranail::<slug>.<command>` one. See
`docs/tools/commands.md`.
- **`laranail/console::` is the canonical translation namespace**, the composer package name,
and `vendor:publish --tag=laranail::console-lang` now publishes into
`lang/vendor/laranail/console/`.

### Changed

- The package's own strings are looked up through `laranail/console::`. Overrides made
against `laranail-console::` (files under the old `vendor/laranail-console/` directory, or
`addLines()` on that namespace) still apply whenever the canonical namespace has no override
for the same key.
- `composer.json` declares `illuminate/http`, which `Tools\Events\CommandEvents` imports, and
excludes `laranail/*` from Packagist so the family resolves only through its VCS
repositories.

### Deprecated

- **The `laranail-console::` translation namespace.** It is still registered over the same
files, so existing `__('laranail-console::…')` calls resolve; use `laranail/console::`.
Removal no earlier than the next minor after 0.1.

## [0.1.4] - 2026-09-27

### Added
Expand Down
11 changes: 11 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"ext-mbstring": "*",
"illuminate/console": "^13.0",
"illuminate/contracts": "^13.0",
"illuminate/http": "^13.0",
"illuminate/support": "^13.0",
"laravel/prompts": "^0.3.18|^1.0",
"symfony/console": "^8.0"
Expand Down Expand Up @@ -148,6 +149,16 @@
{
"type": "vcs",
"url": "https://github.com/laranail/package-tools"
},
{
"type": "composer",
"url": "https://repo.packagist.org",
"exclude": [
"laranail/*"
]
},
{
"packagist.org": false
}
]
}
30 changes: 22 additions & 8 deletions docs/i18n.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Internationalization

Every user-facing string in the package is a translation key under the
`laranail-console::` namespace, with English defaults in `resources/lang/en/`:
`laranail/console::` namespace, with English defaults in `resources/lang/en/`:

- `console.php` — Tools strings (status labels, interaction prompts, errors)
- `prompter.php` — Prompter exception/flow strings
Expand All @@ -13,34 +13,48 @@ Every user-facing string in the package is a translation key under the
php artisan vendor:publish --tag=laranail::console-lang
```

This copies the language files into `lang/vendor/laranail-console/`, which is
where the `laranail-console::` namespace reads overrides from. Edit them there, or
add new locales (e.g. `lang/vendor/laranail-console/fr/console.php`). A file placed
This copies the language files into `lang/vendor/laranail/console/`, which is
where the `laranail/console::` namespace reads overrides from. Edit them there, or
add new locales (e.g. `lang/vendor/laranail/console/fr/console.php`). A file placed
in the `lang/` root is never read for a namespaced key.

## Adding a locale

Create the matching files under `lang/vendor/laranail-console/<locale>/` and set
Create the matching files under `lang/vendor/laranail/console/<locale>/` and set
the locale:

```php
// config/laranail/console.php
'locale' => 'fr', // or null to follow the application locale
```

Strings are resolved with `__('laranail-console::validators.email')` and friends, with
Strings are resolved with `__('laranail/console::validators.email')` and friends, with
`:placeholder` replacements. Exceptions resolve their messages the same way via
`ConsoleException::fromKey()`, falling back to a humanised key if a translation
is missing — so a missing key never produces an empty error message.

## Widget strings

User-facing widget text (the `Summary`, `Header`, `Menu`, `TaskProgress` and
`Callout` labels) lives under `laranail-console::console.widgets.*` and is resolved via
`Callout` labels) lives under `laranail/console::console.widgets.*` and is resolved via
`Support\Lang`, which honours `config('laranail.console.locale')` **without** mutating the
host application's global locale. Each string keeps its English default as a
fallback, so widgets render correctly even outside a booted application. Override
them by publishing the lang files and editing `console.php`'s `widgets` array (or
adding a `lang/vendor/laranail-console/<locale>/console.php`).
adding a `lang/vendor/laranail/console/<locale>/console.php`).

## The deprecated `laranail-console::` namespace

Before 0.1.5 the package registered its strings as `laranail-console::`. That namespace is
now a deprecated alias, registered over the same files, so `__('laranail-console::…')` in
your code still resolves; it will be removed no earlier than the next minor after 0.1.
Move calls to `laranail/console::`.

Overrides made against the old namespace keep working too. Files published under the old
`vendor/laranail-console/` directory of your `lang` path, and lines added at runtime with
`addLines(…, 'laranail-console')`, still apply to the package's own strings: the package
uses an override of the old namespace whenever the canonical one has not been overridden for
the same key. Move them to `vendor/laranail/console/` at your convenience; when both exist for
a key, the canonical one wins.

[← Docs index](../README.md#documentation)
80 changes: 77 additions & 3 deletions docs/tools/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ base — one obvious access path, no Middle-Man indirection.
## Use the trait (when you can't extend the base)

The base is just `extends Illuminate\Console\Command` + the
`Tools\Commands\Concerns\InteractsWithConsoleServices` and `InteractsWithConsoleWriter`
traits. It does **not** compose `SupportsNamespacedNames`; see
`Tools\Commands\Concerns\InteractsWithConsoleServices`, `InteractsWithConsoleWriter` and
`WarnsOnDeprecatedAlias` traits. It does **not** compose `SupportsNamespacedNames`; see
[Namespaced command names](#namespaced-command-names). If your command must
extend a different base (a vendor command, Laravel's `GeneratorCommand`, …), `use`
the trait directly to get the **same** full support — `$this->services`, the managed
Expand Down Expand Up @@ -235,7 +235,8 @@ the trait alone never reads it.

An alias must itself be vendor-scoped. A bare `make:crud` beside
`laranail::your-package.make-crud` hands back exactly the flat-registry collision the
namespaced name exists to prevent.
namespaced name exists to prevent. The one exception is an old name a command must keep
answering to, and that goes in `$deprecatedCommandAliases` (below), never here.

```php
use Simtabi\Laranail\Console\Tools\Commands\Command;
Expand All @@ -257,6 +258,79 @@ final class MakeCrudCommand extends Command
}
```

### Deprecated aliases (`$deprecatedCommandAliases`)

When a command is renamed to the `laranail::<package-slug>.<command>` shape, its old name
stays registered as a deprecated alias so existing scripts, scheduler entries and
`Artisan::call()` keep working. Invoking the command by that alias prints one line naming
the canonical command, before any prompt and before `handle()` runs:

```text
Deprecated: [make:crud] is a deprecated alias and will be removed in the next minor after 0.1. Use [laranail::your-package.make-crud] instead.
```

The canonical name and the aliases in `$commandAliases` print nothing. On a real terminal
the line goes to stderr, so piped output is unchanged.

This is the only way a bare generic name may stay registered beside a vendor-scoped one: the
warning names the replacement, which makes the alias a migration path rather than a second
name.

```php
use Simtabi\Laranail\Console\Tools\Commands\Command;
use Simtabi\Laranail\Console\Tools\Commands\Concerns\SupportsNamespacedNames;

final class MakeCrudCommand extends Command
{
use SupportsNamespacedNames;

protected $signature = 'laranail::your-package.make-crud';

/** @var list<string> */
protected array $deprecatedCommandAliases = ['make:crud'];

public function handle(): int
{
return self::SUCCESS;
}
}
```

The base `Command` composes the `Tools\Commands\Concerns\WarnsOnDeprecatedAlias` trait.
A command on any other base gets the same behaviour by using the trait itself, and the
trait registers the deprecated aliases on its own, so no `$commandAliases` support is needed:

```php
use Illuminate\Console\Command;
use Simtabi\Laranail\Console\Tools\Commands\Concerns\WarnsOnDeprecatedAlias;
use Simtabi\Laranail\Console\Tools\Commands\Concerns\SupportsNamespacedNames;

final class SyncCommand extends Command
{
use SupportsNamespacedNames;
use WarnsOnDeprecatedAlias;

protected $signature = 'laranail::your-package.sync';

/** @var list<string> */
protected array $deprecatedCommandAliases = ['your-package:sync'];

public function handle(): int
{
return self::SUCCESS;
}
}
```

| Member | What it does |
|---|---|
| `getAliases()` | every alias, the deprecated ones included, so the application registers them all |
| `deprecatedCommandAliases()` | the declared deprecated aliases, without the command's own name |
| `initialize()` | prints the warning when the command was invoked by a deprecated alias |

A command that overrides `initialize()` must call `parent::initialize()`, or the warning
never prints. The behaviour is pinned by `tests/Tools/Unit/Commands/DeprecatedAliasTest.php`.

## `laranail::console.check`

Validates the `console.*` config and reports any problems, exiting non-zero on a bad
Expand Down
6 changes: 3 additions & 3 deletions docs/tools/prompter.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ gets an options-aware default validator unless you set your own.

Each implements `ValidatorInterface::validate(mixed): ?string` (null = valid,
otherwise the error message). All are **total** — non-string input returns the
error rather than throwing. Messages default to `laranail-console::validators.*` (see
error rather than throwing. Messages default to `laranail/console::validators.*` (see
[i18n](../i18n.md)). Length checks count **characters** (`mb_strlen`), not bytes.

**Constructor convention (2.0).** Constructors take **only validator-specific
Expand All @@ -92,7 +92,7 @@ new RadioFieldValidator(['a', 'b'])->errorMessage('Pick one');
```

`->errorMessage()` sets a fixed string. Without it, the default
`laranail-console::validators.*` message is used; `->replace([...])` substitutes placeholders
`laranail/console::validators.*` message is used; `->replace([...])` substitutes placeholders
into that translated default and `->locale(...)` resolves it in a specific locale (see
[i18n](../i18n.md)). All three resolve at validate-time, so the active locale is honoured:

Expand Down Expand Up @@ -167,7 +167,7 @@ new V\UuidOrIntegerOrSlugValidator('uuid4');
new V\PathFieldValidator()->replace(['attribute' => 'config path'])->locale('fr');
```

The default messages live under `laranail-console::validators.*` (see [i18n](../i18n.md)):
The default messages live under `laranail/console::validators.*` (see [i18n](../i18n.md)):
`->errorMessage()` replaces the message outright, while `->replace([...])` /
`->locale(...)` feed the translated default. Resolution happens at validate-time.

Expand Down
4 changes: 2 additions & 2 deletions docs/tools/support.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ vocabulary. It has nine cases: `Success`, `Failed`, `Warning`, `Pending`,
| `symbol(?Capabilities)` | the [Symbols](#symbols) glyph (`✓` / `[OK]`); `●`/`[on]` and `○`/`[off]` for `Active`/`Inactive`; `''` for `Unknown` |
| `color()` | the formatter colour used in markup (`green`, `red`, …) |
| `role()` | the palette role (`success`, `danger`, `warning`, `info`, `muted`) for background-painting widgets such as `Badge` |
| `label()` | the translated label, from `laranail-console::console.status.<case>` |
| `label()` | the translated label, from `laranail/console::console.status.<case>` |

`Status::fromBool($ok)` maps a boolean to `Success` or `Failed`.
`TaskStatus::toStatus()` maps a task state onto the same vocabulary. Widgets
Expand Down Expand Up @@ -348,7 +348,7 @@ Validate the `console.*` config with [`Console::validateConfig()`](../configurat

## Lang

Resolves widget strings from the `laranail-console::console.*` translations, honouring
Resolves widget strings from the `laranail/console::console.*` translations, honouring
`console.locale` **without** mutating the host app's global locale; falls back to the
given default (with `:placeholder` interpolation) when no translation exists.

Expand Down
17 changes: 7 additions & 10 deletions src/Exceptions/ConsoleException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@

use Throwable;
use RuntimeException;
use Simtabi\Laranail\Console\Tools\Support\Translations;

/**
* Base exception for the laranail/console package.
*
* All Tools and Prompter exceptions extend this, so consumers can catch the
* whole package with a single type. Messages are resolved from the `laranail-console::`
* whole package with a single type. Messages are resolved from the `laranail/console::`
* translation namespace with a safe fallback, so a missing key never produces
* an empty-message exception.
*
Expand All @@ -35,16 +36,12 @@ public static function fromKey(string $key, array $replace = [], ?Throwable $pre
*/
protected static function resolveMessage(string $key, array $replace = []): string
{
$namespaced = "laranail-console::{$key}";
// Translations::get() only uses the translator when a container has
// bound it, so the exception is still usable outside a booted application.
$message = Translations::get($key, $replace);

// Only use the translator when a container has actually bound it, so
// the exception is still usable outside a booted Laravel application.
if (function_exists('app') && app()->bound('translator')) {
$message = trans($namespaced, $replace);

if (is_string($message) && $message !== $namespaced) {
return $message;
}
if ($message !== Translations::key($key)) {
return $message;
}

// Fallback: humanise the leaf key and interpolate replacements.
Expand Down
2 changes: 1 addition & 1 deletion src/Prompter/Exceptions/PrompterException.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Simtabi\Laranail\Console\Exceptions\ConsoleException;

/**
* Prompter-domain exception. Messages resolve from `laranail-console::prompter.*` with a
* Prompter-domain exception. Messages resolve from `laranail/console::prompter.*` with a
* safe fallback (via {@see ConsoleException::fromKey()}), so a missing key never
* produces an empty-message exception.
*/
Expand Down
3 changes: 2 additions & 1 deletion src/Prompter/Services/FormBuilder/FormBuilderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Closure;
use Laravel\Prompts\FormBuilder as PromptsFormBuilder;
use Simtabi\Laranail\Console\Prompter\Enums\FieldType;
use Simtabi\Laranail\Console\Tools\Support\Translations;
use Simtabi\Laranail\Console\Prompter\Contracts\ValidatorInterface;
use Simtabi\Laranail\Console\Prompter\Exceptions\PrompterException;
use Simtabi\Laranail\Console\Prompter\Validators\RadioFieldValidator;
Expand Down Expand Up @@ -132,7 +133,7 @@ protected function makeValidator(FormFieldService $formField): Closure
$isEmpty = in_array($value, [null, '', []], true);

if ($formField->required && $isEmpty) {
return $formField->customErrorMessage ?? __('laranail-console::prompter.field_required');
return $formField->customErrorMessage ?? Translations::get('prompter.field_required');
}

if ($isEmpty) {
Expand Down
3 changes: 2 additions & 1 deletion src/Prompter/Validators/AbstractValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Simtabi\Laranail\Console\Prompter\Validators;

use Simtabi\Laranail\Console\Tools\Support\Translations;
use Simtabi\Laranail\Console\Prompter\Contracts\ValidatorInterface;

/**
Expand Down Expand Up @@ -90,7 +91,7 @@ protected static function configuredLocale(): ?string
protected function resolvedMessage(): string
{
return $this->customMessage
?? __('laranail-console::validators.' . $this->messageKey, $this->replace, $this->locale ?? self::configuredLocale());
?? Translations::get('validators.' . $this->messageKey, $this->replace, $this->locale ?? self::configuredLocale());
}

/**
Expand Down
16 changes: 11 additions & 5 deletions src/Providers/ConsoleServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Override;
use Illuminate\Support\ServiceProvider;
use Simtabi\Laranail\Console\ConsoleManager;
use Simtabi\Laranail\Console\Tools\Support\Translations;
use Simtabi\Laranail\Console\Exceptions\ConsoleException;
use Simtabi\Laranail\Console\Tools\Support\ConfigValidator;
use Simtabi\Laranail\Console\Tools\Providers\ToolsServiceProvider;
Expand Down Expand Up @@ -40,18 +41,23 @@ public function register(): void

public function boot(): void
{
$this->loadTranslationsFrom(self::LANG_PATH, 'laranail-console');
// The canonical namespace is the composer package name, so overrides land in
// lang/vendor/laranail/console. The hyphen form is the pre-0.1.5 namespace,
// kept over the same files so `__('laranail-console::…')` still resolves;
// Translations::get() also honours overrides a host made against it.
$this->loadTranslationsFrom(self::LANG_PATH, Translations::NAMESPACE);
$this->loadTranslationsFrom(self::LANG_PATH, Translations::LEGACY_NAMESPACE);

if ($this->app->runningInConsole()) {
$this->publishes([
self::CONFIG_PATH => $this->app->configPath('laranail/console.php'),
], 'laranail::console-config');

// vendor/laranail-console, matching the namespace registered above.
// Publishing to the lang root put the files where the namespaced
// loader never looks, so every published override was ignored.
// vendor/laranail/console, matching the canonical namespace above
// (Laravel reads overrides from lang/vendor/{namespace}). Publishing to
// the lang root put the files where the namespaced loader never looks.
$this->publishes([
self::LANG_PATH => $this->app->langPath('vendor/laranail-console'),
self::LANG_PATH => $this->app->langPath('vendor/' . Translations::NAMESPACE),
], 'laranail::console-lang');

// Opt-in fail-fast: validate console.* config at boot (console only, so
Expand Down
Loading
Loading