From 82931953953fc94614b90bf2c35ff8285c7c0b00 Mon Sep 17 00:00:00 2001 From: Konstantinos Arvanitakis Date: Wed, 30 Sep 2026 12:07:26 +0300 Subject: [PATCH] Feat: Adding Translations Pull Command --- src/Command/PullTranslationsCommand.php | 159 ++++++++++++++++++ .../Seeders/StorefrontTranslationsSeeder.php | 8 + src/Providers/LocalizationServiceProvider.php | 5 + 3 files changed, 172 insertions(+) create mode 100644 src/Command/PullTranslationsCommand.php diff --git a/src/Command/PullTranslationsCommand.php b/src/Command/PullTranslationsCommand.php new file mode 100644 index 0000000..af17de4 --- /dev/null +++ b/src/Command/PullTranslationsCommand.php @@ -0,0 +1,159 @@ +> */ + private const SEEDERS = [ + 'storefront' => StorefrontTranslationsSeeder::class, + 'checkout' => CheckoutTranslationsSeeder::class, + 'validation' => ValidationTranslationsSeeder::class, + ]; + + public function handle(): int + { + $dryRun = (bool) $this->option('dry-run'); + $total = 0; + + foreach (self::SEEDERS as $group => $seederClass) { + $file = realpath((new ReflectionClass($seederClass))->getFileName()); + + if (! $dryRun && str_contains($file, '/vendor/')) { + $this->error("{$file} is an installed copy, not your ../boboko-core checkout."); + $this->line('Switch to local mode first (bin/core-mode local), or use --dry-run.'); + + return self::FAILURE; + } + + $known = (new ReflectionMethod($seederClass, 'lines'))->invoke(new $seederClass); + + $missing = LanguageLine::query() + ->where('group', $group) + ->whereNotIn('key', array_keys($known)) + ->orderBy('key') + ->get(); + + if ($missing->isEmpty()) { + $this->line("{$group}: nothing missing"); + + continue; + } + + $entries = ''; + + foreach ($missing as $line) { + $en = $line->text['en'] ?? ''; + $el = $line->text['el'] ?? ''; + + if ($en === '' || $el === '') { + $this->warn(" {$group}.{$line->key} has no ".($en === '' ? 'English' : 'Greek').' text — added empty, fill it in'); + } + + $entries .= $this->entry($line->key, $en, $el); + $this->info(" + {$group}.{$line->key}"); + } + + $total += $missing->count(); + + if (! $dryRun && ! $this->append($file, $entries)) { + return self::FAILURE; + } + } + + $this->newLine(); + $this->line($dryRun + ? "{$total} line(s) would be added." + : "{$total} line(s) added — move them into the right section and commit core."); + + return self::SUCCESS; + } + + /** + * One lines() entry in the seeders' own style: single line when short, + * split over several lines when long. + */ + private function entry(string $key, string $en, string $el): string + { + [$key, $en, $el] = array_map(fn (string $value) => var_export($value, true), [$key, $en, $el]); + + $single = " {$key} => [{$en}, {$el}],\n"; + + if (mb_strlen($single) <= 120) { + return $single; + } + + return " {$key} => [\n {$en},\n {$el},\n ],\n"; + } + + /** + * Inserts the entries right before the closing `];` of lines(), then + * lints the file and restores the original if the result doesn't parse. + */ + private function append(string $file, string $entries): bool + { + $original = file_get_contents($file); + $method = strpos($original, 'function lines(): array'); + $close = $method === false ? false : strpos($original, "\n ];\n }", $method); + + if ($close === false) { + $this->error("Couldn't find the end of lines() in {$file} — add these by hand."); + + return false; + } + + $before = rtrim(substr($original, 0, $close)); + + // The last existing entry doesn't always have a trailing comma. + if (! str_ends_with($before, ',') && ! str_ends_with($before, '[')) { + $before .= ','; + } + + $updated = $before + ."\n\n // ── Pulled from the database (boboko:translations:pull) — move into the right section ──\n" + .$entries + .substr($original, $close + 1); + + file_put_contents($file, $updated); + + exec(PHP_BINARY.' -l '.escapeshellarg($file).' 2>&1', $output, $exitCode); + + if ($exitCode !== 0) { + file_put_contents($file, $original); + $this->error("Writing {$file} produced invalid PHP — restored the original:"); + $this->line(implode("\n", $output)); + + return false; + } + + return true; + } +} diff --git a/src/Localization/Database/Seeders/StorefrontTranslationsSeeder.php b/src/Localization/Database/Seeders/StorefrontTranslationsSeeder.php index ae2b61b..cdf1b05 100644 --- a/src/Localization/Database/Seeders/StorefrontTranslationsSeeder.php +++ b/src/Localization/Database/Seeders/StorefrontTranslationsSeeder.php @@ -98,6 +98,14 @@ class StorefrontTranslationsSeeder extends Seeder 'pagination.previous' => ['Previous page', 'Προηγούμενη σελίδα'], 'pagination.page' => ['Page :page', 'Σελίδα :page'], + // ── Error pages ───────────────────────────────────────────── + 'errors.404_title' => ['Page not found', 'Η σελίδα δεν βρέθηκε'], + 'errors.404_text' => [ + 'The page you are looking for does not exist or has been moved.', + 'Η σελίδα που αναζητάς δεν υπάρχει ή έχει μετακινηθεί.', + ], + 'errors.back_home' => ['Back to home', 'Επιστροφή στην αρχική'], + // ── Reviews ───────────────────────────────────────────────── 'review.rating' => ['Rating', 'Βαθμολογία'], 'review.write_label' => ['Write a review', 'Γράψε μια αξιολόγηση'], diff --git a/src/Providers/LocalizationServiceProvider.php b/src/Providers/LocalizationServiceProvider.php index 112d481..7a6906b 100644 --- a/src/Providers/LocalizationServiceProvider.php +++ b/src/Providers/LocalizationServiceProvider.php @@ -5,6 +5,7 @@ namespace Modules\Core\Providers; use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; use Lunar\Models\Language; +use Modules\Core\Command\PullTranslationsCommand; use Modules\Core\Localization\Events\LanguageCreated; use Modules\Core\Localization\Events\LanguageDeleted; use Modules\Core\Localization\Events\LanguageUpdated; @@ -46,5 +47,9 @@ class LocalizationServiceProvider extends ServiceProvider } Event::listen(LanguageUpdated::class, MigrateTranslationsForRenamedLanguage::class); + + if ($this->app->runningInConsole()) { + $this->commands([PullTranslationsCommand::class]); + } } }