From 217deb0bf9d3dd06d4cb8f1345141c184bcc4eac Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski <5760249+gdarko@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:43:41 +0200 Subject: [PATCH] feat: localize country names in PDFs via symfony/intl (#681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports #639 to 3.x — the original targets the now feature-frozen 2.x line. Address::country_name now resolves the localized country name for the current app locale via Symfony\Component\Intl\Countries, falling back to the stored name on lookup failure. Adds symfony/intl + a unit test. Co-authored-by: Lukas Selch --- app/Models/Address.php | 14 ++- composer.json | 1 + composer.lock | 92 ++++++++++++++++++- tests/Unit/AddressCountryLocalizationTest.php | 57 ++++++++++++ 4 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/AddressCountryLocalizationTest.php diff --git a/app/Models/Address.php b/app/Models/Address.php index 080466c4..bb469941 100644 --- a/app/Models/Address.php +++ b/app/Models/Address.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Symfony\Component\Intl\Countries; class Address extends Model { @@ -18,9 +19,18 @@ class Address extends Model public function getCountryNameAttribute(): ?string { - $name = $this->country ? $this->country->name : null; + if (! $this->country) { + return null; + } - return $name; + try { + return Countries::getName( + $this->country->code, + app()->getLocale() + ); + } catch (\Exception $e) { + return $this->country->name; + } } public function user(): BelongsTo diff --git a/composer.json b/composer.json index bd7a38fd..efee30fb 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "spatie/laravel-backup": "^10.0", "spatie/laravel-medialibrary": "^11.11", "symfony/http-client": "^7.3", + "symfony/intl": "^7.3", "symfony/mailer": "^7.3", "symfony/mailgun-mailer": "^7.3", "symfony/postmark-mailer": "^7.3" diff --git a/composer.lock b/composer.lock index 8e6091c9..9b0515de 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bc1914fedd271f614d922d716ab0e3be", + "content-hash": "c1dac5a39ce45f742d64a45877782655", "packages": [ { "name": "aws/aws-crt-php", @@ -6751,6 +6751,96 @@ ], "time": "2026-05-29T08:46:08+00:00" }, + { + "name": "symfony/intl", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/intl.git", + "reference": "7cfb7792d580dea833647420afd5f2f98df8457b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/intl/zipball/7cfb7792d580dea833647420afd5f2f98df8457b", + "reference": "7cfb7792d580dea833647420afd5f2f98df8457b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/string": "<7.1" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Intl\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Eriksen Costa", + "email": "eriksen.costa@infranology.com.br" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides access to the localization data of the ICU library", + "homepage": "https://symfony.com", + "keywords": [ + "i18n", + "icu", + "internationalization", + "intl", + "l10n", + "localization" + ], + "support": { + "source": "https://github.com/symfony/intl/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T12:55:43+00:00" + }, { "name": "symfony/mailer", "version": "v7.4.12", diff --git a/tests/Unit/AddressCountryLocalizationTest.php b/tests/Unit/AddressCountryLocalizationTest.php new file mode 100644 index 00000000..eebcdb05 --- /dev/null +++ b/tests/Unit/AddressCountryLocalizationTest.php @@ -0,0 +1,57 @@ + 'DatabaseSeeder', '--force' => true]); + Artisan::call('db:seed', ['--class' => 'DemoSeeder', '--force' => true]); +}); + +it('returns localized country name for German locale', function () { + App::setLocale('de'); + + $address = Address::factory()->create([ + 'country_id' => Country::where('code', 'DE')->first()->id, + ]); + + expect($address->country_name)->toBe('Deutschland'); +}); + +it('returns localized country name for French locale', function () { + App::setLocale('fr'); + + $address = Address::factory()->create([ + 'country_id' => Country::where('code', 'DE')->first()->id, + ]); + + expect($address->country_name)->toBe('Allemagne'); +}); + +it('returns null when address has no country', function () { + $address = Address::factory()->create([ + 'country_id' => null, + ]); + + expect($address->country_name)->toBeNull(); +}); + +it('falls back to database name when intl lookup fails', function () { + App::setLocale('de'); + + // Create a country with a code that won't be found in ICU data + $country = new Country; + $country->timestamps = false; + $country->code = 'XX'; + $country->name = 'Unknown Land'; + $country->phonecode = 0; + $country->save(); + + $address = Address::factory()->create([ + 'country_id' => $country->id, + ]); + + expect($address->country_name)->toBe('Unknown Land'); +});