diff --git a/README.md b/README.md index 86944eb..477dd49 100644 --- a/README.md +++ b/README.md @@ -363,7 +363,23 @@ $shortUrl->update([ ]); ``` -#### 3. A/B Split Rotation +#### 3. Browser Language-Based Redirects (new in v2.0.5) +Route visitors to language-specific URLs based on their browser's language preferences (detected from the `Accept-Language` header). Fully supports matching exact regional locales (e.g., `en-US`, `en-GB`) with automatic fallback to base language codes (e.g., `en`, `pl`). + +```php +$shortUrl->update([ + 'targeting_rules' => [ + 'type' => 'language', + 'language' => [ + ['language_code' => 'pl', 'url' => 'https://pl.example.com'], + ['language_code' => 'en-US', 'url' => 'https://us.example.com'], + ['language_code' => 'de', 'url' => 'https://de.example.com'], + ], + ], +]); +``` + +#### 4. A/B Split Rotation Distribute traffic across multiple URLs using weighted random selection. Weights are proportional — they do not need to sum to 100. ```php diff --git a/resources/lang/en/default.php b/resources/lang/en/default.php index 9698c34..9d02857 100644 --- a/resources/lang/en/default.php +++ b/resources/lang/en/default.php @@ -299,13 +299,16 @@ return [ 'targeting_type_device' => 'Device-Based Redirects', 'targeting_type_country' => 'Country-Based (Geo-IP) Redirects', 'targeting_type_rotation' => 'A/B Split Rotation', + 'targeting_type_language' => 'Browser Language-Based Redirects', 'device_targeting_rules' => 'Device Rules', 'country_targeting_rules' => 'Country Rules', + 'language_targeting_rules' => 'Language Rules', 'rotation_targeting_rules' => 'Rotation Targets', 'device_mobile' => 'Mobile Destination URL', 'device_tablet' => 'Tablet Destination URL', 'device_desktop' => 'Desktop Destination URL', 'country_code' => 'Country Code', + 'language_code' => 'Language Code', 'rotation_url' => 'Destination URL', 'rotation_weight' => 'Traffic Weight', 'rotation_weight_helper' => 'Percentage of traffic to route to this URL (e.g. 50 for 50%). Weights are balanced proportionally.', diff --git a/resources/lang/en/languages.php b/resources/lang/en/languages.php new file mode 100644 index 0000000..a887a3f --- /dev/null +++ b/resources/lang/en/languages.php @@ -0,0 +1,44 @@ + 'Polish', + 'en' => 'English', + 'de' => 'German', + 'fr' => 'French', + 'es' => 'Spanish', + 'it' => 'Italian', + 'pt' => 'Portuguese', + 'ru' => 'Russian', + 'zh' => 'Chinese', + 'ja' => 'Japanese', + 'ar' => 'Arabic', + 'nl' => 'Dutch', + 'cs' => 'Czech', + 'sk' => 'Slovak', + 'hu' => 'Hungarian', + 'ro' => 'Romanian', + 'bg' => 'Bulgarian', + 'hr' => 'Croatian', + 'sr' => 'Serbian', + 'sl' => 'Slovenian', + 'uk' => 'Ukrainian', + 'tr' => 'Turkish', + 'da' => 'Danish', + 'sv' => 'Swedish', + 'no' => 'Norwegian', + 'fi' => 'Finnish', + 'el' => 'Greek', + 'he' => 'Hebrew', + 'hi' => 'Hindi', + 'th' => 'Thai', + 'vi' => 'Vietnamese', + 'ko' => 'Korean', + 'id' => 'Indonesian', + 'ms' => 'Malay', + 'et' => 'Estonian', + 'lv' => 'Latvian', + 'lt' => 'Lithuanian', + 'is' => 'Icelandic', + 'ga' => 'Irish', + 'fa' => 'Persian', +]; diff --git a/resources/lang/pl/default.php b/resources/lang/pl/default.php index fb41692..1a7465b 100644 --- a/resources/lang/pl/default.php +++ b/resources/lang/pl/default.php @@ -300,13 +300,16 @@ return [ 'targeting_type_device' => 'Przekierowania zależne od urządzenia', 'targeting_type_country' => 'Przekierowania zależne od kraju (Geo-IP)', 'targeting_type_rotation' => 'Podział ruchu A/B (Rotacja)', + 'targeting_type_language' => 'Przekierowania zależne od języka przeglądarki', 'device_targeting_rules' => 'Reguły urządzeń', 'country_targeting_rules' => 'Reguły krajów', + 'language_targeting_rules' => 'Reguły języków', 'rotation_targeting_rules' => 'Cele rotacji', 'device_mobile' => 'Docelowy URL dla urządzeń mobilnych', 'device_tablet' => 'Docelowy URL dla tabletów', 'device_desktop' => 'Docelowy URL dla komputerów stacjonarnych', 'country_code' => 'Kod kraju', + 'language_code' => 'Kod języka', 'rotation_url' => 'Docelowy URL', 'rotation_weight' => 'Waga ruchu', 'rotation_weight_helper' => 'Procent ruchu kierowany na ten URL (np. 50 dla 50%). Wagi są bilansowane proporcjonalnie.', diff --git a/resources/lang/pl/languages.php b/resources/lang/pl/languages.php new file mode 100644 index 0000000..daa67c3 --- /dev/null +++ b/resources/lang/pl/languages.php @@ -0,0 +1,44 @@ + 'Polski', + 'en' => 'Angielski', + 'de' => 'Niemiecki', + 'fr' => 'Francuski', + 'es' => 'Hiszpański', + 'it' => 'Włoski', + 'pt' => 'Portugalski', + 'ru' => 'Rosyjski', + 'zh' => 'Chiński', + 'ja' => 'Japoński', + 'ar' => 'Arabski', + 'nl' => 'Holenderski', + 'cs' => 'Czeski', + 'sk' => 'Słowacki', + 'hu' => 'Węgierski', + 'ro' => 'Rumuński', + 'bg' => 'Bułgarski', + 'hr' => 'Chorwacki', + 'sr' => 'Serbski', + 'sl' => 'Słoweński', + 'uk' => 'Ukraiński', + 'tr' => 'Turecki', + 'da' => 'Duński', + 'sv' => 'Szwedzki', + 'no' => 'Norweski', + 'fi' => 'Fiński', + 'el' => 'Grecki', + 'he' => 'Hebrajski', + 'hi' => 'Hindi', + 'th' => 'Tajski', + 'vi' => 'Wietnamski', + 'ko' => 'Koreański', + 'id' => 'Indonezyjski', + 'ms' => 'Malajski', + 'et' => 'Estoński', + 'lv' => 'Łotewski', + 'lt' => 'Litewski', + 'is' => 'Islandzki', + 'ga' => 'Irlandzki', + 'fa' => 'Perski', +]; diff --git a/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php b/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php index 78fa557..a4c2d7e 100644 --- a/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php +++ b/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php @@ -432,6 +432,7 @@ class ShortUrlForm 'none' => __('filament-short-url::default.targeting_type_none'), 'device' => __('filament-short-url::default.targeting_type_device'), 'geo' => __('filament-short-url::default.targeting_type_country'), + 'language' => __('filament-short-url::default.targeting_type_language'), 'rotation' => __('filament-short-url::default.targeting_type_rotation'), ]) ->default('none') @@ -487,6 +488,33 @@ class ShortUrlForm ->columns(2) ->visible(fn (Get $get): bool => $get('targeting_rules.type') === 'geo'), + Repeater::make('targeting_rules.language') + ->label(__('filament-short-url::default.language_targeting_rules')) + ->schema([ + Select::make('language_code') + ->label(__('filament-short-url::default.language_code')) + ->options(function (): array { + $languages = __('filament-short-url::languages'); + if (is_array($languages)) { + asort($languages, SORT_LOCALE_STRING); + + return $languages; + } + + return []; + }) + ->searchable() + ->disableOptionsWhenSelectedInSiblingRepeaterItems() + ->required(), + TextInput::make('url') + ->label(__('filament-short-url::default.destination_url')) + ->url() + ->required() + ->maxLength(2048), + ]) + ->columns(2) + ->visible(fn (Get $get): bool => $get('targeting_rules.type') === 'language'), + Repeater::make('targeting_rules.rotation') ->label(__('filament-short-url::default.rotation_targeting_rules')) ->schema([ diff --git a/src/Filament/Resources/ShortUrlResource/Widgets/ShortUrlVisitsRightBreakdown.php b/src/Filament/Resources/ShortUrlResource/Widgets/ShortUrlVisitsRightBreakdown.php index d775a26..88f583e 100644 --- a/src/Filament/Resources/ShortUrlResource/Widgets/ShortUrlVisitsRightBreakdown.php +++ b/src/Filament/Resources/ShortUrlResource/Widgets/ShortUrlVisitsRightBreakdown.php @@ -27,37 +27,10 @@ class ShortUrlVisitsRightBreakdown extends Widget */ protected function getViewData(): array { - $languageNames = [ - 'en' => 'English', - 'pl' => 'Polish', - 'de' => 'German', - 'es' => 'Spanish', - 'fr' => 'French', - 'it' => 'Italian', - 'ru' => 'Russian', - 'zh' => 'Chinese', - 'ja' => 'Japanese', - 'pt' => 'Portuguese', - 'nl' => 'Dutch', - 'tr' => 'Turkish', - 'uk' => 'Ukrainian', - 'cs' => 'Czech', - 'sv' => 'Swedish', - 'no' => 'Norwegian', - 'da' => 'Danish', - 'fi' => 'Finnish', - 'ro' => 'Romanian', - 'hu' => 'Hungarian', - 'sk' => 'Slovak', - 'bg' => 'Bulgarian', - 'hr' => 'Croatian', - 'sr' => 'Serbian', - 'sl' => 'Slovenian', - 'et' => 'Estonian', - 'lv' => 'Latvian', - 'lt' => 'Lithuanian', - 'el' => 'Greek', - ]; + $languageNames = __('filament-short-url::languages'); + if (! is_array($languageNames)) { + $languageNames = []; + } if (! $this->record) { return [ @@ -84,6 +57,14 @@ class ShortUrlVisitsRightBreakdown extends Widget public static function getLanguageTranslation(string $langCode): string { $langCode = strtolower(trim($langCode)); + + // 1. Try loading from languages translation file (languages.php) + $translatedLanguages = __('filament-short-url::languages'); + if (is_array($translatedLanguages) && isset($translatedLanguages[$langCode])) { + return $translatedLanguages[$langCode]; + } + + // 2. Try PHP Locale extension if (class_exists(\Locale::class)) { try { $name = \Locale::getDisplayLanguage($langCode, app()->getLocale()); @@ -95,39 +76,8 @@ class ShortUrlVisitsRightBreakdown extends Widget } } - $fallback = [ - 'en' => 'English', - 'pl' => 'Polish', - 'de' => 'German', - 'es' => 'Spanish', - 'fr' => 'French', - 'it' => 'Italian', - 'ru' => 'Russian', - 'zh' => 'Chinese', - 'ja' => 'Japanese', - 'pt' => 'Portuguese', - 'nl' => 'Dutch', - 'tr' => 'Turkish', - 'uk' => 'Ukrainian', - 'cs' => 'Czech', - 'sv' => 'Swedish', - 'no' => 'Norwegian', - 'da' => 'Danish', - 'fi' => 'Finnish', - 'ro' => 'Romanian', - 'hu' => 'Hungarian', - 'sk' => 'Slovak', - 'bg' => 'Bulgarian', - 'hr' => 'Croatian', - 'sr' => 'Serbian', - 'sl' => 'Slovenian', - 'et' => 'Estonian', - 'lv' => 'Latvian', - 'lt' => 'Lithuanian', - 'el' => 'Greek', - ]; - - return $fallback[$langCode] ?? strtoupper($langCode); + // 3. Simple fallback + return strtoupper($langCode); } /** diff --git a/src/Models/ShortUrl.php b/src/Models/ShortUrl.php index 4c92207..3b6bc9d 100644 --- a/src/Models/ShortUrl.php +++ b/src/Models/ShortUrl.php @@ -440,6 +440,43 @@ class ShortUrl extends Model } } + if ($type === 'language') { + $acceptedLanguages = $request->getLanguages(); + + // Pass 1: Exact match (e.g. "en-us" matches "en-us" rule, or "pl" matches "pl" rule) + foreach ($acceptedLanguages as $acceptedLanguage) { + $acceptedLanguage = strtolower(trim(str_replace('_', '-', $acceptedLanguage))); + if (empty($acceptedLanguage)) { + continue; + } + + foreach ($rules['language'] ?? [] as $rule) { + $ruleLang = strtolower(trim(str_replace('_', '-', $rule['language_code'] ?? ''))); + if ($ruleLang === $acceptedLanguage) { + return $rule['url'] ?? $this->destination_url; + } + } + } + + // Pass 2: Primary language fallback match (e.g. "en-us" matches general "en" rule) + foreach ($acceptedLanguages as $acceptedLanguage) { + $acceptedLanguage = strtolower(trim(str_replace('_', '-', $acceptedLanguage))); + if (empty($acceptedLanguage)) { + continue; + } + + $parts = explode('-', $acceptedLanguage); + $primaryLang = strtolower(trim($parts[0])); + + foreach ($rules['language'] ?? [] as $rule) { + $ruleLang = strtolower(trim(str_replace('_', '-', $rule['language_code'] ?? ''))); + if ($ruleLang === $primaryLang) { + return $rule['url'] ?? $this->destination_url; + } + } + } + } + if ($type === 'rotation') { $items = $rules['rotation'] ?? []; if (! empty($items)) {