feat: add language flag emoji functions and enhance language selector component for improved localization support and user experience
This commit is contained in:
@@ -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')) {
|
||||
/**
|
||||
* Get language flag emoji or icon
|
||||
@@ -229,7 +316,8 @@ if (!function_exists('get_language_flag')) {
|
||||
function get_language_flag(string $languageCode): string
|
||||
{
|
||||
$language = Language::findByCode($languageCode);
|
||||
return $language ? ($language->flag ?? '🌐') : '🌐';
|
||||
|
||||
return $language ? resolve_language_flag($language) : '🌐';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,42 +8,48 @@
|
||||
$currentLanguage = $languages->firstWhere('code', $currentLang);
|
||||
$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'
|
||||
? 'language-select-offcanvas w-full'
|
||||
: '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
|
||||
|
||||
@if($variant === 'offcanvas')
|
||||
<div {{ $attributes->merge(['class' => $wrapperClass]) }}>
|
||||
<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-target="#{{ $languageCollapseId }}"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{ $languageCollapseId }}"
|
||||
>
|
||||
<span>
|
||||
@if($currentLanguage)
|
||||
{{ strtoupper($currentLanguage->code) }}
|
||||
@else
|
||||
{{ strtoupper($currentLang) }}
|
||||
@endif
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span class="material-symbols-outlined language-select__icon">translate</span>
|
||||
<span class="sr-only">{{ $currentLanguage ? $languageLabel($currentLanguage) : strtoupper($currentLang) }}</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>
|
||||
|
||||
<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)
|
||||
<li class="nav-item">
|
||||
<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) }}"
|
||||
>
|
||||
{{ 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>
|
||||
</li>
|
||||
@endforeach
|
||||
@@ -53,20 +59,17 @@
|
||||
@else
|
||||
<div {{ $attributes->merge(['class' => $wrapperClass]) }}>
|
||||
<ul class="navbar-nav !flex-row !items-center">
|
||||
<li class="nav-item dropdown language-select uppercase group">
|
||||
<a class="{{ $linkClass }}" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@if($currentLanguage)
|
||||
{{ strtoupper($currentLanguage->code) }}
|
||||
@else
|
||||
{{ strtoupper($currentLang) }}
|
||||
@endif
|
||||
<li class="nav-item dropdown language-select group">
|
||||
<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) }}">
|
||||
<span class="material-symbols-outlined language-select__icon">translate</span>
|
||||
</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)
|
||||
<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) }}">
|
||||
{{ 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>
|
||||
</li>
|
||||
@endforeach
|
||||
@@ -75,3 +78,83 @@
|
||||
</ul>
|
||||
</div>
|
||||
@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
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<link rel="stylesheet" href="{{ asset('html/style.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="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')
|
||||
<style>
|
||||
.navbar.navbar-light.fixed .btn:not(.btn-expand):not(.btn-gradient) {
|
||||
|
||||
@@ -60,6 +60,9 @@ $favicon_path = setting('site_favicon');
|
||||
<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/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 -->
|
||||
@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') ?>
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user