diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 292899014..44f879a2d 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -100,7 +100,11 @@ jobs: librsvg2-dev \ patchelf \ gstreamer1.0-plugins-base \ - gstreamer1.0-plugins-good + gstreamer1.0-plugins-good \ + gstreamer1.0-plugins-bad \ + gstreamer1.0-plugins-ugly \ + gstreamer1.0-libav \ + gstreamer1.0-gl - name: Install frontend dependencies run: npm ci diff --git a/api/youtube/live.js b/api/youtube/live.js index 10593d341..97902421d 100644 --- a/api/youtube/live.js +++ b/api/youtube/live.js @@ -15,6 +15,28 @@ export default async function handler(request) { } const url = new URL(request.url); const channel = url.searchParams.get('channel'); + const videoIdParam = url.searchParams.get('videoId'); + + // Video ID lookup: resolve author name via oembed + if (videoIdParam && /^[A-Za-z0-9_-]{11}$/.test(videoIdParam)) { + try { + const oembedRes = await fetch( + `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoIdParam}&format=json`, + { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }, + ); + if (oembedRes.ok) { + const data = await oembedRes.json(); + return new Response(JSON.stringify({ channelName: data.author_name || null, title: data.title || null, videoId: videoIdParam }), { + status: 200, + headers: { ...cors, 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600, s-maxage=3600' }, + }); + } + } catch {} + return new Response(JSON.stringify({ channelName: null, title: null, videoId: videoIdParam }), { + status: 200, + headers: { ...cors, 'Content-Type': 'application/json' }, + }); + } if (!channel) { return new Response(JSON.stringify({ error: 'Missing channel parameter' }), { @@ -47,6 +69,16 @@ export default async function handler(request) { // Channel exists if the page contains canonical channel metadata const channelExists = html.includes('"channelId"') || html.includes('og:url'); + // Extract channel name from page metadata (prefer channel name over video title) + let channelName = null; + const ownerMatch = html.match(/"ownerChannelName"\s*:\s*"([^"]+)"/); + if (ownerMatch) { + channelName = ownerMatch[1]; + } else { + const authorMatch = html.match(/"author"\s*:\s*"([^"]+)"/); + if (authorMatch) channelName = authorMatch[1]; + } + // Scope both fields to the same videoDetails block so we don't // combine a videoId from one object with isLive from another. let videoId = null; @@ -60,7 +92,7 @@ export default async function handler(request) { } } - return new Response(JSON.stringify({ videoId, isLive: videoId !== null, channelExists }), { + return new Response(JSON.stringify({ videoId, isLive: videoId !== null, channelExists, channelName }), { status: 200, headers: { 'Content-Type': 'application/json', diff --git a/src/components/LiveNewsPanel.ts b/src/components/LiveNewsPanel.ts index 49b981368..e90c5e4a4 100644 --- a/src/components/LiveNewsPanel.ts +++ b/src/components/LiveNewsPanel.ts @@ -72,7 +72,7 @@ const TECH_LIVE_CHANNELS: LiveChannel[] = [ { id: 'bloomberg', name: 'Bloomberg', handle: '@Bloomberg', fallbackVideoId: 'iEpJwprxDdk' }, { id: 'yahoo', name: 'Yahoo Finance', handle: '@YahooFinance', fallbackVideoId: 'KQp-e_XQnDE' }, { id: 'cnbc', name: 'CNBC', handle: '@CNBC', fallbackVideoId: '9NyxcX3rhQs' }, - { id: 'nasa', name: 'NASA TV', handle: '@NASA', fallbackVideoId: 'fO9e9jnhYK8', useFallbackOnly: true }, + { id: 'nasa', name: 'Sen Space Live', handle: '@NASA', fallbackVideoId: 'fO9e9jnhYK8', useFallbackOnly: true }, ]; // Optional channels users can add from the "Available Channels" tab UI @@ -85,6 +85,8 @@ export const OPTIONAL_LIVE_CHANNELS: LiveChannel[] = [ { id: 'cbs-news', name: 'CBS News', handle: '@CBSNews' }, { id: 'nbc-news', name: 'NBC News', handle: '@NBCNews' }, // Europe + { id: 'bbc-news', name: 'BBC News', handle: '@BBCNews' }, + { id: 'france24-en', name: 'France 24 English', handle: '@FRANCE24English' }, { id: 'welt', name: 'WELT', handle: '@WELTNachrichtensender' }, { id: 'rtve', name: 'RTVE 24H', handle: '@RTVENoticias', fallbackVideoId: '7_srED6k0bE' }, { id: 'trt-haber', name: 'TRT Haber', handle: '@trthaber' }, @@ -111,6 +113,12 @@ export const OPTIONAL_LIVE_CHANNELS: LiveChannel[] = [ { id: 'vtc-now', name: 'VTC NOW', handle: '@VTCNOW' }, { id: 'cna-asia', name: 'CNA (NewsAsia)', handle: '@channelnewsasia' }, { id: 'nhk-world', name: 'NHK World Japan', handle: '@NHKWORLDJAPAN' }, + // Middle East + { id: 'al-hadath', name: 'Al Hadath', handle: '@AlHadath', fallbackVideoId: 'xWXpl7azI8k', useFallbackOnly: true }, + { id: 'sky-news-arabia', name: 'Sky News Arabia', handle: '@skynewsarabia' }, + { id: 'trt-world', name: 'TRT World', handle: '@taborrtworld' }, + { id: 'iran-intl', name: 'Iran International', handle: '@IranIntl' }, + { id: 'cgtn-arabic', name: 'CGTN Arabic', handle: '@CGTNArabic' }, // Africa { id: 'africanews', name: 'Africanews', handle: '@africanews' }, { id: 'channels-tv', name: 'Channels TV', handle: '@channelstv' }, @@ -121,9 +129,10 @@ export const OPTIONAL_LIVE_CHANNELS: LiveChannel[] = [ export const OPTIONAL_CHANNEL_REGIONS: { key: string; labelKey: string; channelIds: string[] }[] = [ { key: 'na', labelKey: 'components.liveNews.regionNorthAmerica', channelIds: ['livenow-fox', 'fox-news', 'newsmax', 'abc-news', 'cbs-news', 'nbc-news'] }, - { key: 'eu', labelKey: 'components.liveNews.regionEurope', channelIds: ['welt', 'rtve', 'trt-haber', 'ntv-turkey', 'cnn-turk', 'tv-rain'] }, + { key: 'eu', labelKey: 'components.liveNews.regionEurope', channelIds: ['bbc-news', 'france24-en', 'welt', 'rtve', 'trt-haber', 'ntv-turkey', 'cnn-turk', 'tv-rain'] }, { key: 'latam', labelKey: 'components.liveNews.regionLatinAmerica', channelIds: ['cnn-brasil', 'jovem-pan', 'record-news', 'band-jornalismo', 'tn-argentina', 'c5n', 'milenio', 'noticias-caracol', 'ntn24', 't13'] }, { key: 'asia', labelKey: 'components.liveNews.regionAsia', channelIds: ['tbs-news', 'ann-news', 'ntv-news', 'cti-news', 'wion', 'vtc-now', 'cna-asia', 'nhk-world'] }, + { key: 'me', labelKey: 'components.liveNews.regionMiddleEast', channelIds: ['al-hadath', 'sky-news-arabia', 'trt-world', 'iran-intl', 'cgtn-arabic'] }, { key: 'africa', labelKey: 'components.liveNews.regionAfrica', channelIds: ['africanews', 'channels-tv', 'ktn-news', 'enca', 'sabc-news'] }, ]; diff --git a/src/live-channels-window.ts b/src/live-channels-window.ts index a16b9b207..7c2cb8cfc 100644 --- a/src/live-channels-window.ts +++ b/src/live-channels-window.ts @@ -26,6 +26,35 @@ function customChannelIdFromHandle(handle: string): string { return 'custom-' + normalized; } +/** Parse YouTube URL into a handle or video ID. Returns null if not a YouTube URL. */ +function parseYouTubeInput(raw: string): { handle: string } | { videoId: string } | null { + let url: URL; + try { + url = new URL(raw); + } catch { + return null; + } + if (!url.hostname.match(/^(www\.)?(youtube\.com|youtu\.be)$/)) return null; + + // youtu.be/VIDEO_ID + if (url.hostname.includes('youtu.be')) { + const vid = url.pathname.slice(1); + if (/^[A-Za-z0-9_-]{11}$/.test(vid)) return { videoId: vid }; + return null; + } + // youtube.com/watch?v=VIDEO_ID + const v = url.searchParams.get('v'); + if (v && /^[A-Za-z0-9_-]{11}$/.test(v)) return { videoId: v }; + // youtube.com/@Handle + const handleMatch = url.pathname.match(/^\/@([\w.-]{3,30})$/); + if (handleMatch) return { handle: `@${handleMatch[1]}` }; + // youtube.com/c/ChannelName or /channel/ID + const channelMatch = url.pathname.match(/^\/(c|channel)\/([\w.-]+)$/); + if (channelMatch) return { handle: `@${channelMatch[2]}` }; + + return null; +} + // Persist active region tab across re-renders let activeRegionTab = OPTIONAL_CHANNEL_REGIONS[0]?.key ?? 'na'; @@ -341,8 +370,8 @@ export function initLiveChannelsWindow(containerEl?: HTMLElement): void { ${escapeHtml(t('components.liveNews.customChannel') ?? 'Custom channel')}
- - + +
@@ -379,7 +408,53 @@ export function initLiveChannelsWindow(containerEl?: HTMLElement): void { const nameInput = document.getElementById('liveChannelsName') as HTMLInputElement | null; const raw = handleInput?.value?.trim(); if (!raw) return; - const handle = raw.startsWith('@') ? raw : `@${raw}`; + if (handleInput) handleInput.classList.remove('invalid'); + + // Try parsing as a YouTube URL first + const parsed = parseYouTubeInput(raw); + + // Direct video URL (watch?v= or youtu.be/) + if (parsed && 'videoId' in parsed) { + const videoId = parsed.videoId; + const id = `custom-vid-${videoId}`; + if (channels.some((c) => c.id === id)) return; + + if (addBtn) { + addBtn.disabled = true; + addBtn.textContent = t('components.liveNews.verifying') ?? 'Verifying…'; + } + + // Try to resolve video/channel title via our proxy (YouTube oembed has no CORS) + let resolvedName = nameInput?.value?.trim() || ''; + if (!resolvedName) { + try { + const baseUrl = isDesktopRuntime() ? getRemoteApiBaseUrl() : ''; + const res = await fetch(`${baseUrl}/api/youtube/live?videoId=${encodeURIComponent(videoId)}`); + if (res.ok) { + const data = await res.json(); + resolvedName = data.channelName || data.title || ''; + } + } catch { /* use fallback */ } + } + if (!resolvedName) resolvedName = `Video ${videoId}`; + + if (addBtn) { + addBtn.disabled = false; + addBtn.textContent = t('components.liveNews.addChannel') ?? 'Add channel'; + } + + channels.push({ id, name: resolvedName, handle: `@video`, fallbackVideoId: videoId, useFallbackOnly: true }); + saveChannelsToStorage(channels); + renderList(listEl); + if (handleInput) handleInput.value = ''; + if (nameInput) nameInput.value = ''; + return; + } + + // Extract handle from URL, or treat raw input as handle + const handle = parsed && 'handle' in parsed + ? parsed.handle + : raw.startsWith('@') ? raw : `@${raw}`; // Validate YouTube handle format: @<3-30 alphanumeric/dot/hyphen/underscore chars> if (!/^@[\w.-]{3,30}$/i.test(handle)) { @@ -393,13 +468,13 @@ export function initLiveChannelsWindow(containerEl?: HTMLElement): void { const id = customChannelIdFromHandle(handle); if (channels.some((c) => c.id === id)) return; - // Validate channel exists on YouTube + // Validate channel exists on YouTube + resolve name if (addBtn) { addBtn.disabled = true; addBtn.textContent = t('components.liveNews.verifying') ?? 'Verifying…'; } - if (handleInput) handleInput.classList.remove('invalid'); + let resolvedName = ''; try { const baseUrl = isDesktopRuntime() ? getRemoteApiBaseUrl() : ''; const res = await fetch(`${baseUrl}/api/youtube/live?channel=${encodeURIComponent(handle)}`); @@ -412,6 +487,7 @@ export function initLiveChannelsWindow(containerEl?: HTMLElement): void { } return; } + resolvedName = data.channelName || ''; } // Non-OK status (429, 5xx) or ambiguous response — allow adding anyway } catch (e) { @@ -424,7 +500,7 @@ export function initLiveChannelsWindow(containerEl?: HTMLElement): void { } } - const name = nameInput?.value?.trim() || handle; + const name = nameInput?.value?.trim() || resolvedName || handle; channels.push({ id, name, handle }); saveChannelsToStorage(channels); renderList(listEl); diff --git a/src/locales/ar.json b/src/locales/ar.json index b60b10449..d80f4ccb8 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -1268,6 +1268,7 @@ "addChannel": "إضافة قناة", "remove": "إزالة", "youtubeHandle": "معرّف YouTube (مثال: @Channel)", + "youtubeHandleOrUrl": "رابط أو معرّف يوتيوب", "displayName": "اسم العرض (اختياري)", "openPanelSettings": "إعدادات عرض اللوحة", "channelSettings": "إعدادات القناة", @@ -1282,6 +1283,7 @@ "regionEurope": "أوروبا", "regionLatinAmerica": "أمريكا اللاتينية", "regionAsia": "آسيا", + "regionMiddleEast": "الشرق الأوسط", "regionAfrica": "أفريقيا" } }, diff --git a/src/locales/de.json b/src/locales/de.json index 92733867f..7c7eaee78 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1268,6 +1268,7 @@ "addChannel": "Kanal hinzufügen", "remove": "Entfernen", "youtubeHandle": "YouTube-Handle (z. B. @Channel)", + "youtubeHandleOrUrl": "YouTube-Handle oder URL", "displayName": "Anzeigename (optional)", "openPanelSettings": "Panelanzeige-Einstellungen", "channelSettings": "Kanaleinstellungen", @@ -1282,6 +1283,7 @@ "regionEurope": "Europa", "regionLatinAmerica": "Lateinamerika", "regionAsia": "Asien", + "regionMiddleEast": "Naher Osten", "regionAfrica": "Afrika" } }, diff --git a/src/locales/el.json b/src/locales/el.json index bd9243810..c1ed5a7db 100644 --- a/src/locales/el.json +++ b/src/locales/el.json @@ -1310,6 +1310,7 @@ "regionEurope": "Ευρώπη", "regionLatinAmerica": "Λατινική Αμερική", "regionAsia": "Ασία", + "regionMiddleEast": "Μέση Ανατολή", "regionAfrica": "Αφρική" } }, diff --git a/src/locales/en.json b/src/locales/en.json index b25a37581..67e6a22c4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1360,6 +1360,7 @@ "addChannel": "Add channel", "remove": "Remove", "youtubeHandle": "YouTube handle (e.g. @Channel)", + "youtubeHandleOrUrl": "YouTube handle or URL", "displayName": "Display name (optional)", "openPanelSettings": "Panel display settings", "channelSettings": "Channel Settings", @@ -1374,6 +1375,7 @@ "regionEurope": "Europe", "regionLatinAmerica": "Latin America", "regionAsia": "Asia", + "regionMiddleEast": "Middle East", "regionAfrica": "Africa", "invalidHandle": "Enter a valid YouTube handle (e.g. @ChannelName)", "channelNotFound": "YouTube channel not found", diff --git a/src/locales/es.json b/src/locales/es.json index da6d87f2c..11ca7ddd4 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1268,6 +1268,7 @@ "addChannel": "Añadir canal", "remove": "Eliminar", "youtubeHandle": "Handle de YouTube (ej. @Channel)", + "youtubeHandleOrUrl": "Identificador o URL de YouTube", "displayName": "Nombre para mostrar (opcional)", "openPanelSettings": "Configuración de visualización del panel", "channelSettings": "Configuración del canal", @@ -1282,6 +1283,7 @@ "regionEurope": "Europa", "regionLatinAmerica": "Latinoamérica", "regionAsia": "Asia", + "regionMiddleEast": "Oriente Medio", "regionAfrica": "África" } }, diff --git a/src/locales/fr.json b/src/locales/fr.json index 747ae3236..cd43b917f 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1268,6 +1268,7 @@ "addChannel": "Ajouter une chaîne", "remove": "Supprimer", "youtubeHandle": "Handle YouTube (ex. @Channel)", + "youtubeHandleOrUrl": "Identifiant ou URL YouTube", "displayName": "Nom d'affichage (optionnel)", "openPanelSettings": "Paramètres d'affichage du panneau", "channelSettings": "Paramètres de la chaîne", @@ -1282,6 +1283,7 @@ "regionEurope": "Europe", "regionLatinAmerica": "Amérique latine", "regionAsia": "Asie", + "regionMiddleEast": "Moyen-Orient", "regionAfrica": "Afrique" } }, diff --git a/src/locales/it.json b/src/locales/it.json index 5d76960fe..44ff9451a 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1268,6 +1268,7 @@ "addChannel": "Aggiungi canale", "remove": "Rimuovi", "youtubeHandle": "Handle YouTube (es. @Channel)", + "youtubeHandleOrUrl": "Handle o URL di YouTube", "displayName": "Nome visualizzato (opzionale)", "openPanelSettings": "Impostazioni visualizzazione pannello", "channelSettings": "Impostazioni canale", @@ -1282,6 +1283,7 @@ "regionEurope": "Europa", "regionLatinAmerica": "America Latina", "regionAsia": "Asia", + "regionMiddleEast": "Medio Oriente", "regionAfrica": "Africa" } }, diff --git a/src/locales/ja.json b/src/locales/ja.json index 2fa1140e7..006c10ff2 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1268,6 +1268,7 @@ "addChannel": "チャンネルを追加", "remove": "削除", "youtubeHandle": "YouTube ハンドル(例: @Channel)", + "youtubeHandleOrUrl": "YouTubeハンドルまたはURL", "displayName": "表示名(任意)", "openPanelSettings": "表示パネル設定", "channelSettings": "チャンネル設定", @@ -1282,6 +1283,7 @@ "regionEurope": "ヨーロッパ", "regionLatinAmerica": "中南米", "regionAsia": "アジア", + "regionMiddleEast": "中東", "regionAfrica": "アフリカ" } }, diff --git a/src/locales/nl.json b/src/locales/nl.json index 482be8a7e..ebb127368 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -1127,6 +1127,7 @@ "addChannel": "Kanaal toevoegen", "remove": "Verwijderen", "youtubeHandle": "YouTube-handle (bijv. @Channel)", + "youtubeHandleOrUrl": "YouTube-handle of URL", "displayName": "Weergavenaam (optioneel)", "openPanelSettings": "Paneelweergave-instellingen", "channelSettings": "Kanaalinstellingen", @@ -1141,6 +1142,7 @@ "regionEurope": "Europa", "regionLatinAmerica": "Latijns-Amerika", "regionAsia": "Azië", + "regionMiddleEast": "Midden-Oosten", "regionAfrica": "Afrika" } }, diff --git a/src/locales/pl.json b/src/locales/pl.json index 27fe279a5..b53f43681 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -1268,6 +1268,7 @@ "addChannel": "Dodaj kanał", "remove": "Usuń", "youtubeHandle": "Handle YouTube (np. @Channel)", + "youtubeHandleOrUrl": "Uchwyt lub URL YouTube", "displayName": "Nazwa wyświetlana (opcjonalnie)", "openPanelSettings": "Ustawienia wyświetlania panelu", "channelSettings": "Ustawienia kanału", @@ -1282,6 +1283,7 @@ "regionEurope": "Europa", "regionLatinAmerica": "Ameryka Łacińska", "regionAsia": "Azja", + "regionMiddleEast": "Bliski Wschód", "regionAfrica": "Afryka" } }, diff --git a/src/locales/pt.json b/src/locales/pt.json index cdb1b3580..85ffd3eb8 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -1127,6 +1127,7 @@ "addChannel": "Adicionar canal", "remove": "Remover", "youtubeHandle": "Handle do YouTube (ex.: @Channel)", + "youtubeHandleOrUrl": "Identificador ou URL do YouTube", "displayName": "Nome de exibição (opcional)", "openPanelSettings": "Configurações de exibição do painel", "channelSettings": "Configurações do canal", @@ -1141,6 +1142,7 @@ "regionEurope": "Europa", "regionLatinAmerica": "América Latina", "regionAsia": "Ásia", + "regionMiddleEast": "Oriente Médio", "regionAfrica": "África" } }, diff --git a/src/locales/ru.json b/src/locales/ru.json index 9492d608b..955bf5aa4 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1268,6 +1268,7 @@ "addChannel": "Добавить канал", "remove": "Удалить", "youtubeHandle": "YouTube- handle (напр. @Channel)", + "youtubeHandleOrUrl": "Имя канала или URL YouTube", "displayName": "Отображаемое имя (необяз.)", "openPanelSettings": "Настройки отображения панели", "channelSettings": "Настройки канала", @@ -1282,6 +1283,7 @@ "regionEurope": "Европа", "regionLatinAmerica": "Латинская Америка", "regionAsia": "Азия", + "regionMiddleEast": "Ближний Восток", "regionAfrica": "Африка" } }, diff --git a/src/locales/sv.json b/src/locales/sv.json index 51cdc9a4e..9a7df4661 100644 --- a/src/locales/sv.json +++ b/src/locales/sv.json @@ -1127,6 +1127,7 @@ "addChannel": "Lägg till kanal", "remove": "Ta bort", "youtubeHandle": "YouTube-handtag (t.ex. @Channel)", + "youtubeHandleOrUrl": "YouTube-handtag eller URL", "displayName": "Visningsnamn (valfritt)", "openPanelSettings": "Panelvisningsinställningar", "channelSettings": "Kanalinställningar", @@ -1141,6 +1142,7 @@ "regionEurope": "Europa", "regionLatinAmerica": "Latinamerika", "regionAsia": "Asien", + "regionMiddleEast": "Mellanöstern", "regionAfrica": "Afrika" } }, diff --git a/src/locales/th.json b/src/locales/th.json index 52a0121a7..b07ceba1f 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -1268,6 +1268,7 @@ "addChannel": "เพิ่มช่อง", "remove": "ลบ", "youtubeHandle": "YouTube handle (เช่น @Channel)", + "youtubeHandleOrUrl": "แฮนเดิล YouTube หรือ URL", "displayName": "ชื่อที่แสดง (ไม่บังคับ)", "openPanelSettings": "การตั้งค่าการแสดงผลแผง", "channelSettings": "การตั้งค่าช่อง", @@ -1282,6 +1283,7 @@ "regionEurope": "ยุโรป", "regionLatinAmerica": "ละตินอเมริกา", "regionAsia": "เอเชีย", + "regionMiddleEast": "ตะวันออกกลาง", "regionAfrica": "แอฟริกา" } }, diff --git a/src/locales/tr.json b/src/locales/tr.json index 4131790bb..d79be2396 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -1268,6 +1268,7 @@ "addChannel": "Kanal ekle", "remove": "Kaldır", "youtubeHandle": "YouTube tanıtıcı (örn. @Channel)", + "youtubeHandleOrUrl": "YouTube tanıtıcısı veya URL", "displayName": "Görünen ad (isteğe bağlı)", "openPanelSettings": "Panel görüntü ayarları", "channelSettings": "Kanal ayarları", @@ -1282,6 +1283,7 @@ "regionEurope": "Avrupa", "regionLatinAmerica": "Latin Amerika", "regionAsia": "Asya", + "regionMiddleEast": "Orta Doğu", "regionAfrica": "Afrika" } }, diff --git a/src/locales/vi.json b/src/locales/vi.json index a09458406..8b9ddc995 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1268,6 +1268,7 @@ "addChannel": "Thêm kênh", "remove": "Xóa", "youtubeHandle": "Handle YouTube (vd: @Channel)", + "youtubeHandleOrUrl": "Tên kênh hoặc URL YouTube", "displayName": "Tên hiển thị (tùy chọn)", "openPanelSettings": "Cài đặt hiển thị bảng", "channelSettings": "Cài đặt kênh", @@ -1282,6 +1283,7 @@ "regionEurope": "Châu Âu", "regionLatinAmerica": "Mỹ Latinh", "regionAsia": "Châu Á", + "regionMiddleEast": "Trung Đông", "regionAfrica": "Châu Phi" } }, diff --git a/src/locales/zh.json b/src/locales/zh.json index c8c243fa6..1a79e3781 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1268,6 +1268,7 @@ "addChannel": "添加频道", "remove": "删除", "youtubeHandle": "YouTube 句柄(如 @Channel)", + "youtubeHandleOrUrl": "YouTube 频道名称或链接", "displayName": "显示名称(可选)", "openPanelSettings": "面板显示设置", "channelSettings": "频道设置", @@ -1282,6 +1283,7 @@ "regionEurope": "欧洲", "regionLatinAmerica": "拉丁美洲", "regionAsia": "亚洲", + "regionMiddleEast": "中东", "regionAfrica": "非洲" } },