feat: add language flag emoji functions and enhance language selector component for improved localization support and user experience

This commit is contained in:
Ümit Tunç
2026-05-21 10:13:16 +03:00
parent 62540c4a98
commit 55efbc6614
4 changed files with 230 additions and 23 deletions
+89 -1
View File
@@ -222,6 +222,93 @@ if (!function_exists('get_translation_status_color')) {
} }
} }
if (!function_exists('flag_emoji_from_country_code')) {
/**
* Build a flag emoji from a two-letter country/region code (e.g. TR 🇹🇷).
*/
function flag_emoji_from_country_code(string $countryCode): string
{
$countryCode = strtoupper(substr($countryCode, 0, 2));
$flag = '';
for ($i = 0; $i < strlen($countryCode); $i++) {
$flag .= mb_chr(127397 + ord($countryCode[$i]));
}
return $flag;
}
}
if (!function_exists('country_code_from_flag_emoji')) {
/**
* Extract a two-letter code from a flag emoji (e.g. 🇬🇧 GB).
*/
function country_code_from_flag_emoji(?string $flag): ?string
{
if (empty($flag)) {
return null;
}
$code = '';
foreach (mb_str_split($flag) as $char) {
$ord = mb_ord($char);
if ($ord >= 0x1F1E6 && $ord <= 0x1F1FF) {
$code .= chr(ord('A') + ($ord - 0x1F1E6));
}
}
return strlen($code) === 2 ? $code : null;
}
}
if (!function_exists('resolve_language_flag')) {
/**
* Resolve flag emoji for a language (stored flag or generated from code).
*/
function resolve_language_flag(Language|string $language): string
{
if (is_string($language)) {
$language = Language::findByCode($language);
}
if (! $language) {
return '🌐';
}
if (! empty($language->flag) && $language->flag !== '🌐') {
return $language->flag;
}
return flag_emoji_from_country_code($language->code);
}
}
if (!function_exists('get_language_short_code')) {
/**
* Short display code for language (from flag region, e.g. GB, TR, SE).
*/
function get_language_short_code(Language|string $language): string
{
if (is_string($language)) {
$model = Language::findByCode($language);
return $model ? get_language_short_code($model) : strtoupper($language);
}
$fromFlag = country_code_from_flag_emoji($language->flag);
if ($fromFlag) {
return $fromFlag;
}
$fromResolved = country_code_from_flag_emoji(resolve_language_flag($language));
return $fromResolved ?: strtoupper($language->code);
}
}
if (!function_exists('get_language_flag')) { if (!function_exists('get_language_flag')) {
/** /**
* Get language flag emoji or icon * Get language flag emoji or icon
@@ -229,7 +316,8 @@ if (!function_exists('get_language_flag')) {
function get_language_flag(string $languageCode): string function get_language_flag(string $languageCode): string
{ {
$language = Language::findByCode($languageCode); $language = Language::findByCode($languageCode);
return $language ? ($language->flag ?? '🌐') : '🌐';
return $language ? resolve_language_flag($language) : '🌐';
} }
} }
@@ -8,42 +8,48 @@
$currentLanguage = $languages->firstWhere('code', $currentLang); $currentLanguage = $languages->firstWhere('code', $currentLang);
$languageCollapseId = 'offcanvas-language-collapse'; $languageCollapseId = 'offcanvas-language-collapse';
$languageLabel = static function ($language): string {
return $language->native_name ?: $language->name ?: strtoupper($language->code);
};
$languageFlag = static function ($language): string {
return resolve_language_flag($language);
};
$wrapperClass = $variant === 'offcanvas' $wrapperClass = $variant === 'offcanvas'
? 'language-select-offcanvas w-full' ? 'language-select-offcanvas w-full'
: 'navbar-other !ml-5 header-language-desktop !hidden lg:!flex xl:!flex shrink-0'; : 'navbar-other !ml-5 header-language-desktop !hidden lg:!flex xl:!flex shrink-0';
$linkClass = 'nav-link dropdown-item dropdown-toggle xl:!text-[.85rem] lg:!text-[.85rem] max-lg:!text-[1.05rem]'; $linkClass = 'nav-link dropdown-item dropdown-toggle language-select__trigger xl:!text-[.85rem] lg:!text-[.85rem] max-lg:!text-[1.05rem] !normal-case';
@endphp @endphp
@if($variant === 'offcanvas') @if($variant === 'offcanvas')
<div {{ $attributes->merge(['class' => $wrapperClass]) }}> <div {{ $attributes->merge(['class' => $wrapperClass]) }}>
<button <button
type="button" type="button"
class="language-select-offcanvas__toggle w-full flex items-center justify-between uppercase !text-[1.05rem] !text-white hover:!text-white focus:!text-white bg-transparent border-0 p-0" class="language-select-offcanvas__toggle w-full flex items-center justify-between !normal-case !text-[1.05rem] !text-white hover:!text-white focus:!text-white bg-transparent border-0 p-0"
data-bs-toggle="collapse" data-bs-toggle="collapse"
data-bs-target="#{{ $languageCollapseId }}" data-bs-target="#{{ $languageCollapseId }}"
aria-expanded="false" aria-expanded="false"
aria-controls="{{ $languageCollapseId }}" aria-controls="{{ $languageCollapseId }}"
> >
<span> <span class="inline-flex items-center gap-2">
@if($currentLanguage) <span class="material-symbols-outlined language-select__icon">translate</span>
{{ strtoupper($currentLanguage->code) }} <span class="sr-only">{{ $currentLanguage ? $languageLabel($currentLanguage) : strtoupper($currentLang) }}</span>
@else
{{ strtoupper($currentLang) }}
@endif
</span> </span>
<i class="uil uil-angle-down language-select-offcanvas__icon text-[1.1rem] !text-[#e31e24] transition-transform duration-200"></i> <i class="uil uil-angle-down language-select-offcanvas__icon language-select__caret shrink-0 before:content-['\e92d'] text-[0.85rem] transition-transform duration-200" aria-hidden="true"></i>
</button> </button>
<div id="{{ $languageCollapseId }}" class="collapse language-select-offcanvas__panel"> <div id="{{ $languageCollapseId }}" class="collapse language-select-offcanvas__panel">
<ul class="navbar-nav flex-col !ps-0 !mt-2 !mb-0 uppercase"> <ul class="navbar-nav flex-col !ps-0 !mt-2 !mb-0 !normal-case">
@foreach($languages as $language) @foreach($languages as $language)
<li class="nav-item"> <li class="nav-item">
<a <a
class="nav-link !py-[0.35rem] !px-0 !text-[0.95rem] {{ $language->code === $currentLang ? 'active font-bold !text-[#e31e24]' : '!text-white/85 hover:!text-white' }}" class="nav-link !py-[0.35rem] !px-0 !text-[0.95rem] !normal-case inline-flex items-center gap-2 {{ $language->code === $currentLang ? 'active font-bold !text-[#e31e24]' : '!text-white/85 hover:!text-white' }}"
href="{{ route('language.switch', $language->code) }}" href="{{ route('language.switch', $language->code) }}"
> >
{{ strtoupper($language->code) }} <span class="language-select__flag shrink-0 text-[1.1rem] leading-none font-bold" aria-hidden="true">{{ $languageFlag($language) }}</span>
<span>{{ $languageLabel($language) }}</span>
</a> </a>
</li> </li>
@endforeach @endforeach
@@ -53,20 +59,17 @@
@else @else
<div {{ $attributes->merge(['class' => $wrapperClass]) }}> <div {{ $attributes->merge(['class' => $wrapperClass]) }}>
<ul class="navbar-nav !flex-row !items-center"> <ul class="navbar-nav !flex-row !items-center">
<li class="nav-item dropdown language-select uppercase group"> <li class="nav-item dropdown language-select group">
<a class="{{ $linkClass }}" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="{{ $linkClass }} inline-flex items-center gap-0.5 !pe-1" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="{{ $currentLanguage ? $languageLabel($currentLanguage) : strtoupper($currentLang) }}">
@if($currentLanguage) <span class="material-symbols-outlined language-select__icon">translate</span>
{{ strtoupper($currentLanguage->code) }}
@else
{{ strtoupper($currentLang) }}
@endif
</a> </a>
<ul class="dropdown-menu group-hover:shadow-[0_0.25rem_0.75rem_rgba(30,34,40,0.15)]"> <ul class="dropdown-menu group-hover:shadow-[0_0.25rem_0.75rem_rgba(30,34,40,0.15)] !min-w-[11rem]">
@foreach($languages as $language) @foreach($languages as $language)
<li class="nav-item"> <li class="nav-item">
<a class="dropdown-item hover:!text-[#747ed1] {{ $language->code === $currentLang ? 'active font-bold !text-[#e31e24]' : '' }}" <a class="dropdown-item hover:!text-[#747ed1] !normal-case inline-flex items-center gap-2 {{ $language->code === $currentLang ? 'active font-bold !text-[#e31e24]' : '' }}"
href="{{ route('language.switch', $language->code) }}"> href="{{ route('language.switch', $language->code) }}">
{{ strtoupper($language->code) }} <span class="language-select__flag shrink-0 text-[1.05rem] leading-none font-bold" aria-hidden="true">{{ $languageFlag($language) }}</span>
<span>{{ $languageLabel($language) }}</span>
</a> </a>
</li> </li>
@endforeach @endforeach
@@ -75,3 +78,83 @@
</ul> </ul>
</div> </div>
@endif @endif
@once
@push('styles')
<style>
.language-select .dropdown-menu {
min-width: 11rem;
}
.language-select__flag {
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
}
.language-select .dropdown-item,
.language-select-offcanvas .nav-link {
white-space: nowrap;
}
.language-select .material-symbols-outlined,
.language-select-offcanvas .material-symbols-outlined {
font-family: 'Material Symbols Outlined' !important;
font-weight: normal !important;
font-style: normal !important;
font-size: 1.25rem;
line-height: 1 !important;
letter-spacing: normal !important;
text-transform: none !important;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.language-select .language-select__trigger {
display: inline-flex !important;
flex-direction: row !important;
flex-wrap: nowrap !important;
align-items: center !important;
gap: 0.15rem;
}
.language-select .language-select__trigger.dropdown-toggle:after,
.language-select .language-select__trigger.dropdown-toggle::after {
position: static !important;
top: auto !important;
right: auto !important;
display: inline-block !important;
vertical-align: middle !important;
margin-left: 0 !important;
margin-right: 0 !important;
line-height: 1;
}
.language-select .language-select__trigger .language-select__icon,
.language-select-offcanvas__toggle .language-select__icon,
.language-select-offcanvas__toggle .language-select__caret {
color: #e31e24;
}
.navbar-dark .language-select .language-select__trigger .language-select__icon {
color: #ffffff;
}
.navbar-dark .language-select .language-select__trigger.dropdown-toggle:after,
.navbar-dark .language-select .language-select__trigger.dropdown-toggle::after {
color: #ffffff !important;
}
.navbar-light .language-select .language-select__trigger .language-select__icon {
color: #e31e24;
}
.navbar-light .language-select .language-select__trigger.dropdown-toggle:after,
.navbar-light .language-select .language-select__trigger.dropdown-toggle::after {
color: #e31e24 !important;
}
</style>
@endpush
@endonce
+3
View File
@@ -12,6 +12,9 @@
<link rel="stylesheet" href="{{ asset('html/style.css') }}"> <link rel="stylesheet" href="{{ asset('html/style.css') }}">
<link rel="stylesheet" href="{{ asset('html/assets/css/colors/grape.css') }}"> <link rel="stylesheet" href="{{ asset('html/assets/css/colors/grape.css') }}">
<link rel="preload" href="{{ asset('html/assets/css/fonts/urbanist.css') }}" as="style" onload="this.rel='stylesheet'"> <link rel="preload" href="{{ asset('html/assets/css/fonts/urbanist.css') }}" as="style" onload="this.rel='stylesheet'">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=translate">
@stack('styles') @stack('styles')
<style> <style>
.navbar.navbar-light.fixed .btn:not(.btn-expand):not(.btn-gradient) { .navbar.navbar-light.fixed .btn:not(.btn-expand):not(.btn-gradient) {
+33
View File
@@ -60,6 +60,9 @@ $favicon_path = setting('site_favicon');
<link rel="stylesheet" href="{{ asset('assets/css/colors/grape.css') }}"> <link rel="stylesheet" href="{{ asset('assets/css/colors/grape.css') }}">
<link rel="preload" href="{{ asset('assets/css/fonts/urbanist.css') }}" as="style" onload="this.rel='stylesheet'"> <link rel="preload" href="{{ asset('assets/css/fonts/urbanist.css') }}" as="style" onload="this.rel='stylesheet'">
<link rel="preload" href="{{ asset('assets/css/fonts/space.css') }}" as="style" onload="this.rel='stylesheet'"> <link rel="preload" href="{{ asset('assets/css/fonts/space.css') }}" as="style" onload="this.rel='stylesheet'">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=translate">
<!-- Site Verification --> <!-- Site Verification -->
@if(setting('seo_google_search_console')) @if(setting('seo_google_search_console'))
@@ -125,6 +128,36 @@ $favicon_path = setting('site_favicon');
} }
} }
.language-select .language-select__trigger {
display: inline-flex !important;
flex-direction: row !important;
flex-wrap: nowrap !important;
align-items: center !important;
gap: 0.15rem;
}
.language-select .language-select__trigger.dropdown-toggle:after,
.language-select .language-select__trigger.dropdown-toggle::after {
position: static !important;
top: auto !important;
right: auto !important;
display: inline-block !important;
vertical-align: middle !important;
margin-left: 0 !important;
margin-right: 0 !important;
line-height: 1;
}
.navbar-dark .language-select .language-select__trigger.dropdown-toggle:after,
.navbar-dark .language-select .language-select__trigger.dropdown-toggle::after {
color: #ffffff !important;
}
.navbar-light .language-select .language-select__trigger.dropdown-toggle:after,
.navbar-light .language-select .language-select__trigger.dropdown-toggle::after {
color: #e31e24 !important;
}
<?php echo setting('custom_css') ?> <?php echo setting('custom_css') ?>
</style> </style>