Add multi-language audio tracks and self-hosted flag-icons
Introduce per-video language support and multiple audio tracks (VideoAudioTrack model + migrations for language, description, title), a reusable language-select component, and a track-editor form. Bundle the self-hosted flag-icons v7.2.3 library and a NAS auto-sync command. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
f8d13457fa
commit
66fd78c10f
314 files changed
+16507
-3172
No files matched your search
@@ -10,10 +10,8 @@ class Countries
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
// Generate flag emoji from ISO2 (two regional indicator letters)
|
||||
$f = fn(string $c): string =>
|
||||
mb_chr(0x1F1E6 + ord($c[0]) - 65) .
|
||||
mb_chr(0x1F1E6 + ord($c[1]) - 65);
|
||||
// Return lowercase ISO2 code — used as the fi fi-{code} CSS class (flag-icons library)
|
||||
$f = fn(string $c): string => strtolower($c);
|
||||
|
||||
return [
|
||||
|
||||
@@ -305,7 +303,7 @@ class Countries
|
||||
$location = $dtze->getLocation();
|
||||
$countryCode = strtoupper($location['country_code'] ?? '');
|
||||
$countryData = $countries[$countryCode] ?? null;
|
||||
$flag = $countryData ? $countryData['flag'] : '🌐';
|
||||
$flag = $countryData ? $countryData['flag'] : '';
|
||||
$countryName = $countryData ? $countryData['name'] : '';
|
||||
|
||||
$parts = explode('/', $tz);
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Data;
|
||||
|
||||
class Languages
|
||||
{
|
||||
/**
|
||||
* All supported languages keyed by ISO 639-1 code.
|
||||
* Fields: name (English), native (native script), code (uppercase ISO), flag (ISO 3166-1 alpha-2 lowercase for flag-icons)
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
return [
|
||||
// ── HIGH-PRIORITY (Middle East / platform focus) ────────────────────
|
||||
'ar' => ['name' => 'Arabic', 'native' => 'العربية', 'code' => 'AR', 'flag' => 'sa'],
|
||||
'en' => ['name' => 'English', 'native' => 'English', 'code' => 'EN', 'flag' => 'gb'],
|
||||
'fa' => ['name' => 'Persian', 'native' => 'فارسی', 'code' => 'FA', 'flag' => 'ir'],
|
||||
'ur' => ['name' => 'Urdu', 'native' => 'اردو', 'code' => 'UR', 'flag' => 'pk'],
|
||||
'tr' => ['name' => 'Turkish', 'native' => 'Türkçe', 'code' => 'TR', 'flag' => 'tr'],
|
||||
'ku' => ['name' => 'Kurdish', 'native' => 'Kurdî', 'code' => 'KU', 'flag' => 'iq'],
|
||||
|
||||
// ── SOUTH ASIA ───────────────────────────────────────────────────────
|
||||
'hi' => ['name' => 'Hindi', 'native' => 'हिन्दी', 'code' => 'HI', 'flag' => 'in'],
|
||||
'bn' => ['name' => 'Bengali', 'native' => 'বাংলা', 'code' => 'BN', 'flag' => 'bd'],
|
||||
'ta' => ['name' => 'Tamil', 'native' => 'தமிழ்', 'code' => 'TA', 'flag' => 'lk'],
|
||||
'te' => ['name' => 'Telugu', 'native' => 'తెలుగు', 'code' => 'TE', 'flag' => 'in'],
|
||||
'ml' => ['name' => 'Malayalam', 'native' => 'മലയാളം', 'code' => 'ML', 'flag' => 'in'],
|
||||
'si' => ['name' => 'Sinhala', 'native' => 'සිංහල', 'code' => 'SI', 'flag' => 'lk'],
|
||||
'ne' => ['name' => 'Nepali', 'native' => 'नेपाली', 'code' => 'NE', 'flag' => 'np'],
|
||||
|
||||
// ── EAST ASIA ────────────────────────────────────────────────────────
|
||||
'zh' => ['name' => 'Chinese', 'native' => '中文', 'code' => 'ZH', 'flag' => 'cn'],
|
||||
'ja' => ['name' => 'Japanese', 'native' => '日本語', 'code' => 'JA', 'flag' => 'jp'],
|
||||
'ko' => ['name' => 'Korean', 'native' => '한국어', 'code' => 'KO', 'flag' => 'kr'],
|
||||
|
||||
// ── SOUTHEAST ASIA ───────────────────────────────────────────────────
|
||||
'id' => ['name' => 'Indonesian', 'native' => 'Bahasa Indonesia', 'code' => 'ID', 'flag' => 'id'],
|
||||
'ms' => ['name' => 'Malay', 'native' => 'Bahasa Melayu', 'code' => 'MS', 'flag' => 'my'],
|
||||
'tl' => ['name' => 'Tagalog', 'native' => 'Filipino', 'code' => 'TL', 'flag' => 'ph'],
|
||||
'th' => ['name' => 'Thai', 'native' => 'ภาษาไทย', 'code' => 'TH', 'flag' => 'th'],
|
||||
'vi' => ['name' => 'Vietnamese', 'native' => 'Tiếng Việt', 'code' => 'VI', 'flag' => 'vn'],
|
||||
'my' => ['name' => 'Burmese', 'native' => 'မြန်မာဘာသာ', 'code' => 'MY', 'flag' => 'mm'],
|
||||
'km' => ['name' => 'Khmer', 'native' => 'ភាសាខ្មែរ', 'code' => 'KM', 'flag' => 'kh'],
|
||||
|
||||
// ── CENTRAL ASIA ─────────────────────────────────────────────────────
|
||||
'az' => ['name' => 'Azerbaijani', 'native' => 'Azərbaycan', 'code' => 'AZ', 'flag' => 'az'],
|
||||
'kk' => ['name' => 'Kazakh', 'native' => 'Қазақша', 'code' => 'KK', 'flag' => 'kz'],
|
||||
'uz' => ['name' => 'Uzbek', 'native' => "O'zbek", 'code' => 'UZ', 'flag' => 'uz'],
|
||||
'tg' => ['name' => 'Tajik', 'native' => 'Тоҷикӣ', 'code' => 'TG', 'flag' => 'tj'],
|
||||
'tk' => ['name' => 'Turkmen', 'native' => 'Türkmençe', 'code' => 'TK', 'flag' => 'tm'],
|
||||
'ky' => ['name' => 'Kyrgyz', 'native' => 'Кыргызча', 'code' => 'KY', 'flag' => 'kg'],
|
||||
|
||||
// ── EUROPE (ROMANCE) ─────────────────────────────────────────────────
|
||||
'fr' => ['name' => 'French', 'native' => 'Français', 'code' => 'FR', 'flag' => 'fr'],
|
||||
'es' => ['name' => 'Spanish', 'native' => 'Español', 'code' => 'ES', 'flag' => 'es'],
|
||||
'pt' => ['name' => 'Portuguese', 'native' => 'Português', 'code' => 'PT', 'flag' => 'pt'],
|
||||
'it' => ['name' => 'Italian', 'native' => 'Italiano', 'code' => 'IT', 'flag' => 'it'],
|
||||
'ro' => ['name' => 'Romanian', 'native' => 'Română', 'code' => 'RO', 'flag' => 'ro'],
|
||||
'ca' => ['name' => 'Catalan', 'native' => 'Català', 'code' => 'CA', 'flag' => 'es'],
|
||||
|
||||
// ── EUROPE (GERMANIC) ────────────────────────────────────────────────
|
||||
'de' => ['name' => 'German', 'native' => 'Deutsch', 'code' => 'DE', 'flag' => 'de'],
|
||||
'nl' => ['name' => 'Dutch', 'native' => 'Nederlands', 'code' => 'NL', 'flag' => 'nl'],
|
||||
'sv' => ['name' => 'Swedish', 'native' => 'Svenska', 'code' => 'SV', 'flag' => 'se'],
|
||||
'no' => ['name' => 'Norwegian', 'native' => 'Norsk', 'code' => 'NO', 'flag' => 'no'],
|
||||
'da' => ['name' => 'Danish', 'native' => 'Dansk', 'code' => 'DA', 'flag' => 'dk'],
|
||||
'fi' => ['name' => 'Finnish', 'native' => 'Suomi', 'code' => 'FI', 'flag' => 'fi'],
|
||||
'is' => ['name' => 'Icelandic', 'native' => 'Íslenska', 'code' => 'IS', 'flag' => 'is'],
|
||||
|
||||
// ── EUROPE (SLAVIC) ──────────────────────────────────────────────────
|
||||
'ru' => ['name' => 'Russian', 'native' => 'Русский', 'code' => 'RU', 'flag' => 'ru'],
|
||||
'uk' => ['name' => 'Ukrainian', 'native' => 'Українська', 'code' => 'UK', 'flag' => 'ua'],
|
||||
'pl' => ['name' => 'Polish', 'native' => 'Polski', 'code' => 'PL', 'flag' => 'pl'],
|
||||
'cs' => ['name' => 'Czech', 'native' => 'Čeština', 'code' => 'CS', 'flag' => 'cz'],
|
||||
'sk' => ['name' => 'Slovak', 'native' => 'Slovenčina', 'code' => 'SK', 'flag' => 'sk'],
|
||||
'hr' => ['name' => 'Croatian', 'native' => 'Hrvatski', 'code' => 'HR', 'flag' => 'hr'],
|
||||
'sr' => ['name' => 'Serbian', 'native' => 'Srpski', 'code' => 'SR', 'flag' => 'rs'],
|
||||
'bg' => ['name' => 'Bulgarian', 'native' => 'Български', 'code' => 'BG', 'flag' => 'bg'],
|
||||
'sl' => ['name' => 'Slovenian', 'native' => 'Slovenščina', 'code' => 'SL', 'flag' => 'si'],
|
||||
'mk' => ['name' => 'Macedonian', 'native' => 'Македонски', 'code' => 'MK', 'flag' => 'mk'],
|
||||
|
||||
// ── EUROPE (OTHER) ───────────────────────────────────────────────────
|
||||
'el' => ['name' => 'Greek', 'native' => 'Ελληνικά', 'code' => 'EL', 'flag' => 'gr'],
|
||||
'hu' => ['name' => 'Hungarian', 'native' => 'Magyar', 'code' => 'HU', 'flag' => 'hu'],
|
||||
'he' => ['name' => 'Hebrew', 'native' => 'עברית', 'code' => 'HE', 'flag' => 'il'],
|
||||
'lt' => ['name' => 'Lithuanian', 'native' => 'Lietuvių', 'code' => 'LT', 'flag' => 'lt'],
|
||||
'lv' => ['name' => 'Latvian', 'native' => 'Latviešu', 'code' => 'LV', 'flag' => 'lv'],
|
||||
'et' => ['name' => 'Estonian', 'native' => 'Eesti', 'code' => 'ET', 'flag' => 'ee'],
|
||||
'sq' => ['name' => 'Albanian', 'native' => 'Shqip', 'code' => 'SQ', 'flag' => 'al'],
|
||||
|
||||
// ── AFRICA ───────────────────────────────────────────────────────────
|
||||
'sw' => ['name' => 'Swahili', 'native' => 'Kiswahili', 'code' => 'SW', 'flag' => 'tz'],
|
||||
'am' => ['name' => 'Amharic', 'native' => 'አማርኛ', 'code' => 'AM', 'flag' => 'et'],
|
||||
'so' => ['name' => 'Somali', 'native' => 'Soomaali', 'code' => 'SO', 'flag' => 'so'],
|
||||
'ha' => ['name' => 'Hausa', 'native' => 'Hausa', 'code' => 'HA', 'flag' => 'ng'],
|
||||
'yo' => ['name' => 'Yoruba', 'native' => 'Yorùbá', 'code' => 'YO', 'flag' => 'ng'],
|
||||
'ig' => ['name' => 'Igbo', 'native' => 'Igbo', 'code' => 'IG', 'flag' => 'ng'],
|
||||
'zu' => ['name' => 'Zulu', 'native' => 'isiZulu', 'code' => 'ZU', 'flag' => 'za'],
|
||||
'af' => ['name' => 'Afrikaans', 'native' => 'Afrikaans', 'code' => 'AF', 'flag' => 'za'],
|
||||
|
||||
// ── AMERICAS ─────────────────────────────────────────────────────────
|
||||
'qu' => ['name' => 'Quechua', 'native' => 'Runa Simi', 'code' => 'QU', 'flag' => 'pe'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the flag icon code (lowercase ISO 3166-1 alpha-2) for a given ISO 639-1 language code.
|
||||
* Returns null when the code is unknown or empty.
|
||||
*/
|
||||
public static function flag(?string $iso639): ?string
|
||||
{
|
||||
if (!$iso639) return null;
|
||||
return self::all()[strtolower($iso639)]['flag'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options list for the language-select component.
|
||||
* value = ISO 639-1 code (e.g. "ar", "en").
|
||||
* Arabic and English are pinned to the top; everything else is sorted alphabetically by English name.
|
||||
*/
|
||||
public static function forLanguage(): array
|
||||
{
|
||||
$pinned = ['ar', 'en'];
|
||||
$all = self::all();
|
||||
$list = [];
|
||||
|
||||
foreach ($all as $iso => $lang) {
|
||||
$list[] = [
|
||||
'value' => $iso,
|
||||
'code' => $lang['code'],
|
||||
'flag' => $lang['flag'],
|
||||
'label' => $lang['name'],
|
||||
'native' => $lang['native'],
|
||||
'search' => strtolower($iso . ' ' . $lang['name'] . ' ' . $lang['native']),
|
||||
'pinned' => in_array($iso, $pinned, true),
|
||||
];
|
||||
}
|
||||
|
||||
usort($list, function ($a, $b) use ($pinned) {
|
||||
$aPin = array_search($a['value'], $pinned, true);
|
||||
$bPin = array_search($b['value'], $pinned, true);
|
||||
|
||||
if ($aPin !== false && $bPin !== false) return $aPin <=> $bPin;
|
||||
if ($aPin !== false) return -1;
|
||||
if ($bPin !== false) return 1;
|
||||
|
||||
return strcmp($a['label'], $b['label']);
|
||||
});
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user