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')) {
|
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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user