Repository: knuckleswtf/scribe (php)

Issue:
Nested translations for validation rules rely on non-universal translation feature
### Scribe version

4.33.0

### PHP version

8.1

### Framework

Laravel

### Framework version

10.*

### Scribe config

```ruby
not relevant
```


### What happened?

This code is in ParsesValidationRules:

```php
$description = trans("validation.{$rule}");
        // For rules that can apply to multiple types (eg 'max' rule), Laravel returns an array of possible messages
        // 'numeric' => 'The :attribute must not be greater than :max'
        // 'file' => 'The :attribute must have a size less than :max kilobytes'
        if (is_array($description)) {
            $description = $description[$baseType];
        }
```

It expects trans to return an array for the max rule. However, our application uses a different translation engine (specifically `joedixon/laravel-translation` with translations in the database) that does not support returning arrays.

In any case, I have fixed the issue by changing it to:

```php
$translationString = "validation.{$rule}";
$description = trans($translationString);

if ($translationString === $description) {
    $translationString = "validation.{$rule}.{$baseType}";
    $translated = trans($translationString);
    if ($translated !== $translationString) {
        $description = $translated;
    }
}
```

I have no idea how widespread this issue is, and I haven't found any official docs on creating translation engines. Maybe it's the engine's fault. Unfortunately, it was a bit of a pain to fix, because I had to extend 5 strategies that use the ParsesValidationRules trait and add a test that checks the original `getDescription` for code changes. Would you consider adding this change?

### Docs

- [X] I've checked [the docs](https://scribe.knuckles.wtf/laravel), the [troubleshooting guide](https://scribe.knuckles.wtf/laravel/troubleshooting), and [existing issues](https://github.com/knuckleswtf/scribe/issues?q=is%3Aissue+), but I didn't find a solution

Pull request description:
Fix translating rules with translation engines that don't return arrays.
Fixes #824 

Adds support for translating rules like `'validation.max.string'` for translation engines that don't support returning an array for `'validation.max'`.

Patch:
diff --git a/resources/js/tryitout.js b/resources/js/tryitout.js
index a3feb1bd..01c584f2 100644
--- a/resources/js/tryitout.js
+++ b/resources/js/tryitout.js
@@ -194,6 +194,11 @@ async function executeTryOut(endpointId, form) {
     const bodyParameters = form.querySelectorAll('input[data-component=body]');
     bodyParameters.forEach(el => {
         let value = el.value;
+
+        if (el.type === 'number' && typeof value === 'string') {
+            value = parseFloat(value);
+        }
+
         if (el.type === 'file' && el.files[0]) {
             setter(el.name, el.files[0]);
             return;
diff --git a/src/Extracting/ParsesValidationRules.php b/src/Extracting/ParsesValidationRules.php
index bec00aa1..2204481d 100644
--- a/src/Extracting/ParsesValidationRules.php
+++ b/src/Extracting/ParsesValidationRules.php
@@ -778,11 +778,21 @@ trait ParsesValidationRules
             return "Must match the regex {$arguments[':regex']}.";
         }
 
-        $description = trans("validation.{$rule}");
-        // For rules that can apply to multiple types (eg 'max' rule), Laravel returns an array of possible messages
+        $translationString = "validation.{$rule}";
+        $description = trans($translationString);
+
+        // For rules that can apply to multiple types (eg 'max' rule), There is an array of possible messages
         // 'numeric' => 'The :attribute must not be greater than :max'
         // 'file' => 'The :attribute must have a size less than :max kilobytes'
-        if (is_array($description)) {
+        // Depending on the translation engine, trans may return the array, or it will fail to translate the string
+        // and will need to be called with the baseType appended.
+        if ($description === $translationString) {
+            $translationString = "{$translationString}.{$baseType}";
+            $translated = trans($translationString);
+            if ($translated !== $translationString) {
+                $description = $translated;
+            }
+        } elseif (is_array($description)) {
             $description = $description[$baseType];
         }
 
diff --git a/src/Tools/Utils.php b/src/Tools/Utils.php
index e7db3e15..114de014 100644
--- a/src/Tools/Utils.php
+++ b/src/Tools/Utils.php
@@ -9,6 +9,7 @@ use FastRoute\RouteParser\Std;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Illuminate\Routing\Route;
+use Illuminate\Support\Facades\File;
 use Illuminate\Support\Str;
 use Knuckles\Scribe\Exceptions\CouldntFindFactory;
 use Knuckles\Scribe\Exceptions\CouldntGetRouteDetails;
@@ -191,6 +192,11 @@ class Utils
         }
     }
 
+    public static function makeDirectoryRecursive(string $dir): void
+    {
+        File::isDirectory($dir) || File::makeDirectory($dir, 0777, true, true);
+    }
+
     public static function deleteFilesMatching(string $dir, callable $condition): void
     {
         if (class_exists(LocalFilesystemAdapter::class)) {
diff --git a/src/Writing/Writer.php b/src/Writing/Writer.php
index d9fa1602..69db61cc 100644
--- a/src/Writing/Writer.php
+++ b/src/Writing/Writer.php
@@ -3,7 +3,6 @@
 namespace Knuckles\Scribe\Writing;
 
 use Illuminate\Support\Facades\Storage;
-use Illuminate\Support\Str;
 use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
 use Knuckles\Scribe\Tools\DocumentationConfig;
 use Knuckles\Scribe\Tools\Globals;
@@ -96,6 +95,7 @@ class Writer
 
             $spec = $this->generateOpenAPISpec($parsedRoutes);
             if ($this->isStatic) {
+                Utils::makeDirectoryRecursive($this->staticTypeOutputPath);
                 $specPath = "{$this->staticTypeOutputPath}/openapi.yaml";
                 file_put_contents($specPath, $spec);
             } else {


Test patch:
diff --git a/tests/Unit/UtilsTest.php b/tests/Unit/UtilsTest.php
new file mode 100644
index 00000000..a5eab141
--- /dev/null
+++ b/tests/Unit/UtilsTest.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Knuckles\Scribe\Tests\Unit;
+
+use Knuckles\Scribe\Tests\BaseLaravelTest;
+use Knuckles\Scribe\Tools\Utils;
+
+class UtilsTest extends BaseLaravelTest
+{
+    /** @test */
+    public function make_directory_recursive()
+    {
+        $dir = __DIR__ . '/test_dir';
+        Utils::makeDirectoryRecursive($dir);
+        $this->assertDirectoryExists($dir); // Directory exists
+
+        if (rmdir($dir)) { // Remove the directory
+            dump("Directory deleted successfully: $dir");
+        } else { // If deletion fails, you can handle the error as needed
+            dump("Failed to delete directory: $dir");
+        }
+    }
+}
diff --git a/tests/Unit/ValidationRuleParsingTest.php b/tests/Unit/ValidationRuleParsingTest.php
index 95765404..03aa4e9a 100644
--- a/tests/Unit/ValidationRuleParsingTest.php
+++ b/tests/Unit/ValidationRuleParsingTest.php
@@ -4,6 +4,7 @@ namespace Knuckles\Scribe\Tests\Unit;
 
 use Illuminate\Foundation\Application;
 use Illuminate\Support\Facades\Validator;
+use Illuminate\Translation\Translator;
 use Illuminate\Validation\Rule;
 use Illuminate\Validation\ValidationException;
 use Knuckles\Scribe\Extracting\ParsesValidationRules;
@@ -597,6 +598,32 @@ class ValidationRuleParsingTest extends BaseLaravelTest
             array_map(fn ($case) => $case->value, Fixtures\TestStringBackedEnum::cases())
         ));
     }
+
+    /** @test */
+    public function can_translate_validation_rules_with_types_with_translator_without_array_support()
+    {
+        // Single line DocComment
+        $ruleset = [
+            'nested' => [
+                'string', 'max:20',
+            ],
+        ];
+
+        $results = $this->strategy->parse($ruleset);
+
+        $this->assertEquals('Must not be greater than 20 characters.', $results['nested']['description']);
+
+        $this->app->extend('translator', function ($command, $app) {
+            $loader = $app['translation.loader'];
+            $locale = $app['config']['app.locale'];
+            return new DummyTranslator($loader, $locale);
+        });
+
+        $results = $this->strategy->parse($ruleset);
+
+        $this->assertEquals('successfully translated by concatenated string.', $results['nested']['description']);
+
+    }
 }
 
 class DummyValidationRule implements \Illuminate\Contracts\Validation\Rule
@@ -673,3 +700,15 @@ if ($laravel10Rules) {
         }
     }
 }
+
+class DummyTranslator extends Translator
+{
+    public function get($key, array $replace = [], $locale = null, $fallback = true)
+    {
+        if ($key === 'validation.max.string') {
+            return 'successfully translated by concatenated string';
+        }
+
+        return $key;
+    }
+}