Repository: Vicimus/Support
Pull request #409: dev(shared): merge php74 into master 
Author: login: Quintile
Created: 2025-04-22T18:58:51Z | Merged: 2025-04-24T13:52:11Z

Whenever we develop new features for Marketingsuite, we develop these new features, interfaces, services, etc in the php74 branch. These changes and new developments are never merged into master, but if we eventually upgrade Marketingsuite to php8, we will be switching to use the master branch of support, and we need all these changes to be present or a bunch of work and fixes will just disappear and things will be broken.



Diff:
diff --git a/.github/workflows/standard.yml b/.github/workflows/standard.yml
index 505b8c5e..f8072dbc 100644
--- a/.github/workflows/standard.yml
+++ b/.github/workflows/standard.yml
@@ -3,6 +3,7 @@ on:
   pull_request:
     branches:
       - master
+      - php74
 jobs:
   phpcs:
     runs-on: ubuntu-latest
diff --git a/resources/testing/lang/en/i18n.php b/resources/testing/lang/en/i18n.php
new file mode 100644
index 00000000..587f5aa7
--- /dev/null
+++ b/resources/testing/lang/en/i18n.php
@@ -0,0 +1,7 @@
+<?php
+
+declare(strict_types=1);
+
+return [
+    'hello' => 'Hello',
+];
diff --git a/resources/testing/lang/en/i18n/fruit.php b/resources/testing/lang/en/i18n/fruit.php
new file mode 100644
index 00000000..0cc36578
--- /dev/null
+++ b/resources/testing/lang/en/i18n/fruit.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+return [
+    'banana' => 'Banana',
+    'strawberry' => [
+        'Strawberry',
+        'context',
+        'location',
+    ],
+];
diff --git a/resources/testing/lang/fr-CA/i18n.php b/resources/testing/lang/fr-CA/i18n.php
new file mode 100644
index 00000000..faada82d
--- /dev/null
+++ b/resources/testing/lang/fr-CA/i18n.php
@@ -0,0 +1,7 @@
+<?php
+
+declare(strict_types=1);
+
+return [
+    'hello' => 'Bonjour',
+];
diff --git a/resources/testing/lang/fr-CA/i18n/fruit.php b/resources/testing/lang/fr-CA/i18n/fruit.php
new file mode 100644
index 00000000..26dca839
--- /dev/null
+++ b/resources/testing/lang/fr-CA/i18n/fruit.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+return [
+    'banana' => 'French for banana',
+    'strawberry' => [
+        'Strawberry',
+        'context',
+        'location',
+        true,
+    ],
+];
diff --git a/src/Interfaces/ADF/ADFLead.php b/src/Interfaces/ADF/ADFLead.php
index d7a4e57d..63bfc2f3 100644
--- a/src/Interfaces/ADF/ADFLead.php
+++ b/src/Interfaces/ADF/ADFLead.php
@@ -69,7 +69,8 @@ public function preferredDate(): ?DateTime;
     public function vehicle(): ?ADFVehicle;
 
     /**
-     * Attach a token
      */
+    public function vehicleOfInterest(): ?ADFVehicle;
+
     public function withToken(string $token): mixed;
 }
diff --git a/src/Interfaces/Incentives/Incentive.php b/src/Interfaces/Incentives/Incentive.php
index 2d4a57a3..9f19ce7e 100644
--- a/src/Interfaces/Incentives/Incentive.php
+++ b/src/Interfaces/Incentives/Incentive.php
@@ -6,5 +6,43 @@
 
 interface Incentive
 {
-    //
+    /**
+     * Does this incentive have a lease rate
+     */
+    public function hasLeaseRate(): bool;
+
+    /**
+     * Does this incentive have a finance rate
+     */
+    public function hasRate(): bool;
+
+    /**
+     * Get the highest rate in the program
+     */
+    public function highestRate(): Term;
+
+    /**
+     * Get the highest term in the program
+     */
+    public function highestTerm(): Term;
+
+    /**
+     * Get the institution
+     */
+    public function institution(): Institution;
+
+    /**
+     * Is this residuals
+     */
+    public function isResiduals(): bool;
+
+    /**
+     * Get the lowest rate in the program
+     */
+    public function lowestRate(): Term;
+
+    /**
+     * Get the lowest term in the program
+     */
+    public function lowestTerm(): Term;
 }
diff --git a/src/Interfaces/Incentives/Institution.php b/src/Interfaces/Incentives/Institution.php
new file mode 100644
index 00000000..5c3407d3
--- /dev/null
+++ b/src/Interfaces/Incentives/Institution.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Vicimus\Support\Interfaces\Incentives;
+
+/**
+ * Represents an institution
+ */
+interface Institution
+{
+    //
+}
diff --git a/src/Interfaces/Leads/Distribution.php b/src/Interfaces/Leads/Distribution.php
new file mode 100644
index 00000000..0c251358
--- /dev/null
+++ b/src/Interfaces/Leads/Distribution.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Vicimus\Support\Interfaces\Leads;
+
+/**
+ * Represent a lead distribution with configuration information
+ */
+interface Distribution
+{
+    /**
+     */
+    public function debug(): string;
+
+    /**
+     */
+    public function getHeaders(): string;
+
+    /**
+     */
+    public function getHttpAdfFormat(): string;
+
+    /**
+     */
+    public function getHttpProviderId(): string;
+
+    /**
+     */
+    public function getHttpVendorId(): string;
+
+    /**
+     */
+    public function getStoreId(): int;
+
+    /**
+     */
+    public function http(): ?string;
+}
diff --git a/src/Interfaces/Leads/HttpCallbackHandler.php b/src/Interfaces/Leads/HttpCallbackHandler.php
new file mode 100644
index 00000000..2e5f9fd2
--- /dev/null
+++ b/src/Interfaces/Leads/HttpCallbackHandler.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Vicimus\Support\Interfaces\Leads;
+
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * An interface for Http callback Handlers related to leads, lead distributions and http adf content
+ */
+interface HttpCallbackHandler
+{
+    /**
+     * Handle the response from the http post
+     *
+     * @param Distribution      $distribution The distribution that made the POST
+     * @param ResponseInterface $result       The response from the server
+     *
+     */
+    public function handle(Distribution $distribution, ResponseInterface $result): void;
+}
diff --git a/src/Interfaces/MarketingSuite/Assets/Creatable.php b/src/Interfaces/MarketingSuite/Assets/Creatable.php
index 1aebab05..d5d96969 100644
--- a/src/Interfaces/MarketingSuite/Assets/Creatable.php
+++ b/src/Interfaces/MarketingSuite/Assets/Creatable.php
@@ -53,6 +53,14 @@ public function path(): ?string;
      */
     public function property(string $name, mixed $default = null): mixed;
 
+    /**
+     * Get a property value
+     *
+     * @param string $name The name of the property to get
+     *
+     */
+    public function propertyWithoutFallback(string $name): bool|int|string|null;
+
     /**
      * Record a property value
      */
diff --git a/src/Interfaces/MarketingSuite/SimpleProperties.php b/src/Interfaces/MarketingSuite/SimpleProperties.php
index 10bbdb37..be685878 100644
--- a/src/Interfaces/MarketingSuite/SimpleProperties.php
+++ b/src/Interfaces/MarketingSuite/SimpleProperties.php
@@ -20,14 +20,5 @@ interface SimpleProperties
      */
     public function property(string $name, mixed $default = null): mixed;
 
-    /**
-     * Check if there is a property value
-     *
-     * @phpcsSuppress SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint
-     *
-     * @param string $property The property to set
-     * @param mixed  $value    If set, will record a property rather than get
-     * @param bool   $hidden   The hidden state of a property
-     */
     public function record(string $property, mixed $value = null, bool $hidden = false): void;
 }
diff --git a/src/Interfaces/MarketingSuite/Validatable.php b/src/Interfaces/MarketingSuite/Validatable.php
index 008d6291..19c663a9 100644
--- a/src/Interfaces/MarketingSuite/Validatable.php
+++ b/src/Interfaces/MarketingSuite/Validatable.php
@@ -8,6 +8,7 @@
  * Interface Validatable
  *
  * @property string $title
+ * @property string|int $id
  */
 interface Validatable
 {
diff --git a/src/Interfaces/Store.php b/src/Interfaces/Store.php
index ece25a6e..6390d29c 100644
--- a/src/Interfaces/Store.php
+++ b/src/Interfaces/Store.php
@@ -12,6 +12,14 @@
  */
 interface Store
 {
+    /**
+     * Check if the store has a property or not
+     *
+     * @param string $property The property to check
+     *
+     */
+    public function hasProperty(string $property): bool;
+
     /**
      * Get the primary identifier for the store
      */
diff --git a/src/Locale/Compiler.php b/src/Locale/Compiler.php
index e95ef5e6..d8b609d4 100644
--- a/src/Locale/Compiler.php
+++ b/src/Locale/Compiler.php
@@ -5,24 +5,19 @@
 namespace Vicimus\Support\Locale;
 
 use Exception;
-use Illuminate\Contracts\Cache\Repository;
+use RuntimeException;
+use Symfony\Component\Finder\Finder;
 use Vicimus\Support\Interfaces\MarketingSuite\Exceptions\LocaleException;
 
 class Compiler
 {
-    private Repository $cache;
-
-    private string $env;
-
     private string $file;
 
     private string $pathToLang;
 
-    public function __construct(Repository $cache, string $env, string $pathToLang, string $file = 'i18n.php')
+    public function __construct(string $pathToLang, string $file = 'i18n.php')
     {
-        $this->cache = $cache;
         $this->file = $file;
-        $this->env = $env;
         $this->pathToLang = $pathToLang;
     }
 
@@ -35,12 +30,39 @@ public function __construct(Repository $cache, string $env, string $pathToLang,
      */
     public function get(string $locale): array
     {
-        if ($this->env !== 'production') {
-            return $this->getLocale($locale);
+        return $this->getLocale($locale);
+    }
+
+    /**
+     * @param string[]|string[][] $translations The translations
+     * @param string              $prefix       The prefix
+     *
+     * @return Translation[]|Translation[][]
+     *
+     * @throws RuntimeException
+     */
+    private function castToTranslations(array $translations, string $prefix = ''): array
+    {
+        $converted = [];
+        foreach ($translations as $key => $value) {
+            if (is_string($value)) {
+                $converted[$key] = new Translation($key, $value, $prefix ? sprintf('%s.%s', $prefix, $key) : $key);
+                continue;
+            }
+
+            if (!is_array($value)) {
+                throw new RuntimeException('Expected string or array but got something else');
+            }
+
+            if (!$this->isComplexTranslation($value)) {
+                $converted[$key] = $this->castToTranslations($value, $key);
+                continue;
+            }
+
+            $converted[$key] = new Translation($key, $value, $prefix ? sprintf('%s.%s', $prefix, $key) : $key);
         }
 
-        $key = sprintf('i18n-%s', $locale);
-        return $this->cache->remember($key, 30, fn () => $this->getLocale($locale));
+        return $converted;
     }
 
     /**
@@ -63,6 +85,86 @@ private function getLocale(string $locale): array
             throw new $ex(sprintf('Locale [%s] is not supported at this time', $locale));
         }
 
-        return include $path;
+        return $this->read($path);
+    }
+
+    /**
+     * @param string[] $value The value to check
+     *
+     * @return bool
+     */
+    private function isComplexTranslation(array $value): bool
+    {
+        return array_keys($value) === range(0, count($value) - 1);
+    }
+
+    /**
+     * Dynamically read all the i18n files
+     *
+     * @param string $path            The path to the base i18n file
+     * @param bool   $dynamicallyLoad Should we dynamically load other files
+     *
+     * @return string[]|string[][]
+     *
+     * @throws RuntimeException
+     */
+    private function read(string $path, bool $dynamicallyLoad = true): array
+    {
+        $uncategorized = require $path;
+        if (!$dynamicallyLoad) {
+            return $this->transform($this->castToTranslations($uncategorized));
+        }
+
+        $main = $uncategorized;
+
+        $filename = pathinfo($path)['filename'];
+        $searchPath = sprintf('%s/%s', pathinfo($path)['dirname'], $filename);
+
+        if (file_exists($searchPath) && is_dir($searchPath)) {
+            $finder = new Finder();
+            foreach ($finder->files()->ignoreDotFiles(true)->in($searchPath) as $file) {
+                $key = pathinfo($file->getFileInfo()->getFilename())['filename'];
+                if (array_key_exists($key, $main)) {
+                    throw new RuntimeException(sprintf(
+                        'Key for file [%s] already exists when building i18n master array',
+                        $file->getFileInfo()->getFilename(),
+                    ));
+                }
+
+                $category = require $file->getPathname();
+                $main[$key] = $category;
+            }
+        }
+
+        return $this->transform($this->castToTranslations($main));
+    }
+
+    /**
+     * Convert the Translation instances to their values
+     *
+     * @param Translation[]|Translation[][] $translations The translations
+     *
+     * @return string[]|string[][]
+     *
+     * @throws RuntimeException
+     */
+    private function transform(array $translations): array
+    {
+        $result = [];
+
+        foreach ($translations as $key => $value) {
+            if (is_array($value)) {
+                $result[$key] = $this->transform($value);
+                continue;
+            }
+
+            if (!$value instanceof Translation) {
+                throw new RuntimeException(sprintf('Translation [%s] must be an instance of Translation', $key));
+            }
+
+            $result[$key] = $value->value;
+        }
+
+        return $result;
     }
 }
diff --git a/src/Locale/Translation.php b/src/Locale/Translation.php
new file mode 100644
index 00000000..19dcd4b1
--- /dev/null
+++ b/src/Locale/Translation.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Vicimus\Support\Locale;
+
+use Vicimus\Support\Classes\ImmutableObject;
+
+/**
+ * @property string $key
+ * @property string[]|string $value
+ * @property string $path
+ * @property string $context
+ * @property string[][] $locations
+ * @property bool $translated
+ */
+class Translation extends ImmutableObject
+{
+    /**
+     * @param string                     $key         The key (banana))
+     * @param string|string[]|string[][] $translation The translation data
+     * @param string|null                $path        The path (inventory-ads.banana))
+     */
+    public function __construct(string $key, string|array $translation, ?string $path = null)
+    {
+        $path ??= $key;
+        $value = $translation;
+        $context = '';
+        $locations = [];
+        $translated = false;
+
+        $locationIndex = 1;
+        $contextIndex = 2;
+        $translatedIndex = 3;
+
+        if (!is_string($value)) {
+            $value = $value[0];
+            if (is_array($value)) {
+                $locationIndex--;
+                $contextIndex--;
+                $value = $key;
+            }
+
+            $context = $translation[$contextIndex] ?? '';
+            $locations = $translation[$locationIndex] ?? [];
+            $translated = $translation[$translatedIndex] ?? false;
+        }
+
+        parent::__construct([
+            'path' => $path,
+            'key' => $key,
+            'value' => $value,
+            'context' => $context,
+            'locations' => $locations,
+            'translated' => $translated,
+        ]);
+    }
+}
diff --git a/src/Services/MaturityCalculator.php b/src/Services/MaturityCalculator.php
new file mode 100644
index 00000000..24b15982
--- /dev/null
+++ b/src/Services/MaturityCalculator.php
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Vicimus\Support\Services;
+
+use Carbon\Carbon;
+use DateTimeInterface;
+
+/**
+ *
+ */
+class MaturityCalculator
+{
+    /**
+     * @param Carbon   $purchaseDate     The purchase date of the vehicle
+     * @param int      $term             The term in unknown units
+     * @param int|null $paymentFrequency The payment frequency we figured out
+     *
+     */
+    public function calculate(Carbon $purchaseDate, int $term, ?int &$paymentFrequency = null): ?DateTimeInterface
+    {
+        // Cash deal or invalid term
+        if ($term <= 1 || $term > 208) {
+            return null;
+        }
+
+        $biWeekly = $term % 26 === 0;
+
+        // Normal bi-weekly term
+        if ($biWeekly) {
+            $paymentFrequency = 26;
+            return $purchaseDate->copy()->addYears($term / 26);
+        }
+
+        // Messed up bi-weekly term
+        if ($term > 96) {
+            $paymentFrequency = 26;
+            $daysToMaturity = round($term / 26 * 365);
+            return $purchaseDate->copy()->addDays((int) $daysToMaturity);
+        }
+
+        $paymentFrequency = 12;
+        return $purchaseDate->copy()->addMonths($term);
+    }
+}
diff --git a/tests/Unit/Locale/CompilerTest.php b/tests/Unit/Locale/CompilerTest.php
new file mode 100644
index 00000000..2732eec1
--- /dev/null
+++ b/tests/Unit/Locale/CompilerTest.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Vicimus\Support\Tests\Unit\Locale;
+
+use Vicimus\Support\Locale\Compiler;
+use Vicimus\Support\Testing\TestCase;
+
+class CompilerTest extends TestCase
+{
+    public function testCompile(): void
+    {
+        $compiler = new Compiler(__DIR__ . '/../../../resources/testing/lang');
+        $translations = $compiler->get('en');
+
+        $this->assertArrayHasKey('hello', $translations);
+        $this->assertEquals('Hello', $translations['hello']);
+
+        $this->assertArrayHasKey('fruit', $translations);
+        $this->assertIsArray($translations['fruit']);
+
+        $this->assertArrayHasKey('banana', $translations['fruit']);
+        $this->assertEquals('Banana', $translations['fruit']['banana']);
+
+        $this->assertArrayHasKey('strawberry', $translations['fruit']);
+        $this->assertEquals('Strawberry', $translations['fruit']['strawberry']);
+
+        $french = $compiler->get('fr-CA');
+        $this->assertArrayHasKey('hello', $french);
+        $this->assertEquals('Bonjour', $french['hello']);
+        $this->assertArrayHasKey('fruit', $french);
+        $this->assertIsArray($french['fruit']);
+
+        $this->assertArrayHasKey('banana', $french['fruit']);
+        $this->assertEquals('French for banana', $french['fruit']['banana']);
+    }
+}
diff --git a/tests/Unit/Services/MaturityCalculatorTest.php b/tests/Unit/Services/MaturityCalculatorTest.php
new file mode 100644
index 00000000..d0f562ab
--- /dev/null
+++ b/tests/Unit/Services/MaturityCalculatorTest.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Vicimus\Support\Tests\Unit\Services;
+
+use Carbon\Carbon;
+use Vicimus\Support\Services\MaturityCalculator;
+use Vicimus\Support\Testing\TestCase;
+
+/**
+ *
+ */
+class MaturityCalculatorTest extends TestCase
+{
+    /**
+     */
+    public function testCalculate(): void
+    {
+        $calculator = new MaturityCalculator();
+
+        $purchaseDate = new Carbon('2025-04-03 12:00:00');
+
+        // Term of 1 is always a cash deal and not a valid finance or lease deal
+        $this->assertEquals(null, $calculator->calculate($purchaseDate, 0));
+        $this->assertEquals(null, $calculator->calculate($purchaseDate, 1));
+
+        // Between 2 and 12 are always monthly
+        $this->assertEquals('2025-06-03', $calculator->calculate($purchaseDate, 2)->format('Y-m-d'));
+        $this->assertEquals('2025-07-03', $calculator->calculate($purchaseDate, 3)->format('Y-m-d'));
+        $this->assertEquals('2025-08-03', $calculator->calculate($purchaseDate, 4)->format('Y-m-d'));
+        $this->assertEquals('2025-09-03', $calculator->calculate($purchaseDate, 5)->format('Y-m-d'));
+        $this->assertEquals('2025-10-03', $calculator->calculate($purchaseDate, 6)->format('Y-m-d'));
+        $this->assertEquals('2025-11-03', $calculator->calculate($purchaseDate, 7)->format('Y-m-d'));
+        $this->assertEquals('2025-12-03', $calculator->calculate($purchaseDate, 8)->format('Y-m-d'));
+        $this->assertEquals('2026-01-03', $calculator->calculate($purchaseDate, 9)->format('Y-m-d'));
+        $this->assertEquals('2026-02-03', $calculator->calculate($purchaseDate, 10)->format('Y-m-d'));
+        $this->assertEquals('2026-03-03', $calculator->calculate($purchaseDate, 11)->format('Y-m-d'));
+        $this->assertEquals('2026-04-03', $calculator->calculate($purchaseDate, 12)->format('Y-m-d'));
+
+        // Obviously monthly
+        $this->assertEquals('2027-04-03', $calculator->calculate($purchaseDate, 24)->format('Y-m-d'));
+        $this->assertEquals('2028-04-03', $calculator->calculate($purchaseDate, 36)->format('Y-m-d'));
+        $this->assertEquals('2029-04-03', $calculator->calculate($purchaseDate, 48)->format('Y-m-d'));
+        $this->assertEquals('2030-04-03', $calculator->calculate($purchaseDate, 60)->format('Y-m-d'));
+        $this->assertEquals('2031-04-03', $calculator->calculate($purchaseDate, 72)->format('Y-m-d'));
+        $this->assertEquals('2032-04-03', $calculator->calculate($purchaseDate, 84)->format('Y-m-d'));
+        $this->assertEquals('2033-04-03', $calculator->calculate($purchaseDate, 96)->format('Y-m-d'));
+
+        // Obviously bi-weekly
+        $this->assertEquals('2026-04-03', $calculator->calculate($purchaseDate, 26)->format('Y-m-d'));
+        $this->assertEquals('2027-04-03', $calculator->calculate($purchaseDate, 52)->format('Y-m-d'));
+        $this->assertEquals('2028-04-03', $calculator->calculate($purchaseDate, 78)->format('Y-m-d'));
+        $this->assertEquals('2029-04-03', $calculator->calculate($purchaseDate, 104)->format('Y-m-d'));
+        $this->assertEquals('2030-04-03', $calculator->calculate($purchaseDate, 130)->format('Y-m-d'));
+        $this->assertEquals('2031-04-03', $calculator->calculate($purchaseDate, 156)->format('Y-m-d'));
+        $this->assertEquals('2032-04-03', $calculator->calculate($purchaseDate, 182)->format('Y-m-d'));
+        $this->assertEquals('2033-04-03', $calculator->calculate($purchaseDate, 208)->format('Y-m-d'));
+
+        // Probably monthly
+        $this->assertEquals('2031-07-03', $calculator->calculate($purchaseDate, 75)->format('Y-m-d'));
+        $this->assertEquals('2033-03-03', $calculator->calculate($purchaseDate, 95)->format('Y-m-d'));
+
+        // Probably bi-weekly
+        $this->assertEquals('2029-01-22', $calculator->calculate($purchaseDate, 99)->format('Y-m-d'));
+        $this->assertEquals('2031-03-19', $calculator->calculate($purchaseDate, 155)->format('Y-m-d'));
+        $this->assertEquals('2033-03-18', $calculator->calculate($purchaseDate, 207)->format('Y-m-d'));
+
+        // Probably an error or lazy dealership employee
+        $this->assertEquals(null, $calculator->calculate($purchaseDate, 209));
+    }
+}


Reviews:
user: login: qlty-cloud-legacy[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-22T19:00:29Z
user: login: qlty-cloud-legacy[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-23T14:21:41Z
user: login: rob-houston
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T13:51:38Z

Review comments:
diff_hunk: @@ -35,12 +29,39 @@ public function __construct(Repository $cache, string $env, string $pathToLang,
      */
     public function get(string $locale): array
     {
-        if ($this->env !== 'production') {
-            return $this->getLocale($locale);
+        return $this->getLocale($locale);
+    }
+
+    /**
+     * @param string[]|string[][] $translations The translations
+     * @param string              $prefix       The prefix
+     *
+     * @return Translation[]|Translation[][]
+     *
+     * @throws RuntimeException
+     */
+    private function castToTranslations(array $translations, string $prefix = ''): array
path: src/Locale/Compiler.php
user: login: qlty-cloud-legacy[bot]
body: Function `castToTranslations` has a Cognitive Complexity of 9 (exceeds 5 allowed). Consider refactoring.
created_at: 2025-04-22T19:00:29Z
author_association: NONE
diff_hunk: @@ -63,6 +84,83 @@
             throw new $ex(sprintf('Locale [%s] is not supported at this time', $locale));
         }
 
-        return include $path;
+        return $this->read($path);
+    }
+
+    /**
+     * @param string[] $value The value to check
+     *
+     * @return bool
+     */
+    private function isComplexTranslation(array $value): bool
+    {
+        return array_keys($value) === range(0, count($value) - 1);
+    }
+
+    /**
+     * Dynamically read all the i18n files
+     *
+     * @param string $path            The path to the base i18n file
+     * @param bool   $dynamicallyLoad Should we dynamically load other files
+     *
+     * @return string[]|string[][]
+     *
+     * @throws RuntimeException
+     */
+    private function read(string $path, bool $dynamicallyLoad = true): array
+    {
+        $uncategorized = require $path;
+        if (!$dynamicallyLoad) {
+            return $this->transform($this->castToTranslations($uncategorized));
+        }
+
+        $main = $uncategorized;
+
+        $filename = pathinfo($path)['filename'];
+        $searchPath = sprintf('%s/%s', pathinfo($path)['dirname'], $filename);
+        $finder = new Finder();
+        foreach ($finder->files()->ignoreDotFiles(true)->in($searchPath) as $file) {
+            $key = pathinfo($file->getFileInfo()->getFilename())['filename'];
+            if (array_key_exists($key, $main)) {
+                throw new RuntimeException(sprintf(
+                    'Key for file [%s] already exists when building i18n master array',
+                    $file->getFileInfo()->getFilename(),
+                ));
+            }
+
+            $category = require $file->getPathname();
+            $main[$key] = $category;
+        }
+
+        return $this->transform($this->castToTranslations($main));
+    }
+
+    /**
+     * Convert the Translation instances to their values
+     *
+     * @param Translation[]|Translation[][] $translations The translations
+     *
+     * @return string[]|string[][]
+     *
+     * @throws RuntimeException
+     */
+    private function transform(array $translations): array
path: src/Locale/Compiler.php
user: login: qlty-cloud-legacy[bot]
body: Function `transform` has a Cognitive Complexity of 6 (exceeds 5 allowed). Consider refactoring.
created_at: 2025-04-22T19:00:29Z
author_association: NONE
diff_hunk: @@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Vicimus\Support\Locale;
+
+use Vicimus\Support\Classes\ImmutableObject;
+
+/**
+ * @property string $key
+ * @property string[]|string $value
+ * @property string $path
+ * @property string $context
+ * @property string[][] $locations
+ * @property bool $translated
+ */
+class Translation extends ImmutableObject
+{
+    /**
+     * @param string                     $key         The key (banana))
+     * @param string|string[]|string[][] $translation The translation data
+     * @param string|null                $path        The path (inventory-ads.banana))
+     */
+    public function __construct(string $key, string|array $translation, ?string $path = null)
path: src/Locale/Translation.php
user: login: qlty-cloud-legacy[bot]
body: Method `__construct` has 27 lines of code (exceeds 25 allowed). Consider refactoring.
created_at: 2025-04-22T19:00:29Z
author_association: NONE
diff_hunk: @@ -63,6 +85,86 @@ private function getLocale(string $locale): array
             throw new $ex(sprintf('Locale [%s] is not supported at this time', $locale));
         }
 
-        return include $path;
+        return $this->read($path);
+    }
+
+    /**
+     * @param string[] $value The value to check
+     *
+     * @return bool
+     */
+    private function isComplexTranslation(array $value): bool
+    {
+        return array_keys($value) === range(0, count($value) - 1);
+    }
+
+    /**
+     * Dynamically read all the i18n files
+     *
+     * @param string $path            The path to the base i18n file
+     * @param bool   $dynamicallyLoad Should we dynamically load other files
+     *
+     * @return string[]|string[][]
+     *
+     * @throws RuntimeException
+     */
+    private function read(string $path, bool $dynamicallyLoad = true): array
path: src/Locale/Compiler.php
user: login: qlty-cloud-legacy[bot]
body: Function `read` has a Cognitive Complexity of 8 (exceeds 5 allowed). Consider refactoring.
created_at: 2025-04-23T14:21:41Z
author_association: NONE

PR comments:
user: login: qlty-cloud-legacy[bot]
created_at: 2025-04-23T14:25:39Z
body: Code Climate has analyzed commit e35f44cb and detected **4 issues** on this pull request.

Here's the issue category breakdown:

| Category | Count |
|:---:|:---:|
| Complexity | 4 |


The test coverage on the diff in this pull request is **88.3%** (**75%** is the threshold).

This pull request will bring the total coverage in the repository to **73.8%**.

View more on [Code Climate](https://codeclimate.com/github/Vicimus/Support/pull/409).

author_association: NONE