mirror of
https://github.com/movie-web/movie-web.git
synced 2025-01-23 23:21:11 +01:00
commit
89f97fe849
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "movie-web",
|
||||
"version": "4.2.2",
|
||||
"version": "4.2.3",
|
||||
"private": true,
|
||||
"homepage": "https://movie-web.app",
|
||||
"scripts": {
|
||||
@ -28,6 +28,7 @@
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.1",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@ladjs/country-language": "^1.0.3",
|
||||
"@movie-web/providers": "^2.0.2",
|
||||
"@noble/hashes": "^1.3.3",
|
||||
"@react-spring/web": "^9.7.3",
|
||||
@ -44,7 +45,6 @@
|
||||
"hls.js": "^1.4.14",
|
||||
"i18next": "^23.7.11",
|
||||
"immer": "^10.0.3",
|
||||
"iso-639-1": "^3.1.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"million": "^2.6.4",
|
||||
|
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@ -17,6 +17,9 @@ dependencies:
|
||||
'@headlessui/react':
|
||||
specifier: ^1.7.17
|
||||
version: 1.7.17(react-dom@18.2.0)(react@18.2.0)
|
||||
'@ladjs/country-language':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
'@movie-web/providers':
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.3
|
||||
@ -65,9 +68,6 @@ dependencies:
|
||||
immer:
|
||||
specifier: ^10.0.3
|
||||
version: 10.0.3
|
||||
iso-639-1:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
jwt-decode:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
@ -1912,6 +1912,11 @@ packages:
|
||||
'@jridgewell/resolve-uri': 3.1.1
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
|
||||
/@ladjs/country-language@1.0.3:
|
||||
resolution: {integrity: sha512-FJROu9/hh4eqVAGDyfL8vpv6Vb0qKHX1ozYLRZ+beUzD5xFf+3r0J+SVIWKviEa7W524Qvqou+ta1WrsRgzxGw==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
/@movie-web/providers@2.0.3:
|
||||
resolution: {integrity: sha512-6UNk5EebiNjGoFTuyHuu0eZZTreRYv0cdsn52CVYjm6CXG63w4dMbx8ybxcvMUrDF3o8bWlqnlovG142sdOmNw==}
|
||||
dependencies:
|
||||
|
21
src/@types/country-language.d.ts
vendored
Normal file
21
src/@types/country-language.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
declare module "@ladjs/country-language" {
|
||||
export interface LanguageObj {
|
||||
countries: Array<{
|
||||
code_2: string;
|
||||
code_3: string;
|
||||
numCode: string;
|
||||
}>;
|
||||
direction: "RTL" | "LTR";
|
||||
name: string[];
|
||||
nativeName: string[];
|
||||
iso639_1: string;
|
||||
}
|
||||
|
||||
type Callback<T> = (err: null | string, result: null | T) => void;
|
||||
|
||||
declare namespace lib {
|
||||
function getLanguage(locale: string, cb: Callback<LanguageObj>): void;
|
||||
}
|
||||
|
||||
export = lib;
|
||||
}
|
9
src/assets/README.md
Normal file
9
src/assets/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# About the languages
|
||||
|
||||
Locales are difficult, here is some guidance.
|
||||
|
||||
## Process on adding new languages
|
||||
1. Use weblate to add translations, see contributing guidelines.
|
||||
2. Add your language to `@/assets/languages.ts`. Must be in ISO format (ISO-639 for language and ISO-3166 for country/region). For joke languages, use any format.
|
||||
3. If your language doesn't have a region specified (Such as in `pt-BR`, `BR` being the region). Add a default region in `@/utils/language.ts` at `defaultLanguageCodes`
|
||||
4. If the flag in the language dropdown doesn't match the correct one. Add a default country in `@/utils/language.ts` at `countryPriority`.
|
@ -1,26 +1,41 @@
|
||||
import ar from "@/assets/locales/ar.json";
|
||||
import bg from "@/assets/locales/bg.json";
|
||||
import bn from "@/assets/locales/bn.json";
|
||||
import cs from "@/assets/locales/cs.json";
|
||||
import de from "@/assets/locales/de.json";
|
||||
import el from "@/assets/locales/el.json";
|
||||
import en from "@/assets/locales/en.json";
|
||||
import es from "@/assets/locales/es.json";
|
||||
import et from "@/assets/locales/et.json";
|
||||
import fa from "@/assets/locales/fa.json";
|
||||
import fr from "@/assets/locales/fr.json";
|
||||
import gl from "@/assets/locales/gl.json";
|
||||
import gu from "@/assets/locales/gu.json";
|
||||
import he from "@/assets/locales/he.json";
|
||||
import hi from "@/assets/locales/hi.json";
|
||||
import id from "@/assets/locales/id.json";
|
||||
import it from "@/assets/locales/it.json";
|
||||
import ja from "@/assets/locales/ja.json";
|
||||
import ko from "@/assets/locales/ko.json";
|
||||
import lv from "@/assets/locales/lv.json";
|
||||
import minion from "@/assets/locales/minion.json";
|
||||
import ne from "@/assets/locales/ne.json";
|
||||
import nl from "@/assets/locales/nl.json";
|
||||
import pa from "@/assets/locales/pa.json";
|
||||
import pirate from "@/assets/locales/pirate.json";
|
||||
import pl from "@/assets/locales/pl.json";
|
||||
import ptbr from "@/assets/locales/pt-BR.json";
|
||||
import ro from "@/assets/locales/ro.json";
|
||||
import ru from "@/assets/locales/ru.json";
|
||||
import sl from "@/assets/locales/sl.json";
|
||||
import sv from "@/assets/locales/sv.json";
|
||||
import ta from "@/assets/locales/ta.json";
|
||||
import th from "@/assets/locales/th.json";
|
||||
import tok from "@/assets/locales/tok.json";
|
||||
import tr from "@/assets/locales/tr.json";
|
||||
import uk from "@/assets/locales/uk.json";
|
||||
import vi from "@/assets/locales/vi.json";
|
||||
import zhhant from "@/assets/locales/zh-Hant.json";
|
||||
import zh from "@/assets/locales/zh.json";
|
||||
|
||||
export const locales = {
|
||||
@ -46,9 +61,22 @@ export const locales = {
|
||||
et,
|
||||
tok,
|
||||
hi,
|
||||
pt: ptbr,
|
||||
"pt-BR": ptbr,
|
||||
uk,
|
||||
bg,
|
||||
bn,
|
||||
el,
|
||||
fa,
|
||||
gu,
|
||||
id,
|
||||
ja,
|
||||
ko,
|
||||
sl,
|
||||
ta,
|
||||
"zh-HANT": zhhant,
|
||||
ru,
|
||||
gl,
|
||||
pa,
|
||||
ro,
|
||||
};
|
||||
export type Locales = keyof typeof locales;
|
||||
|
||||
export const rtlLocales: Locales[] = ["he", "ar"];
|
||||
|
@ -206,7 +206,8 @@
|
||||
"episodeBadge": "E{{episode}}",
|
||||
"loadingError": "خطأ في تحميل الموسم",
|
||||
"loadingList": "تحميل...",
|
||||
"loadingTitle": "تحميل..."
|
||||
"loadingTitle": "تحميل...",
|
||||
"unairedEpisodes": "تم تعطيل حلقة واحدة أو أكثر من هذا الموسم لأنه لم يتم بثها بعد."
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "سرعة التشغيل",
|
||||
|
@ -206,7 +206,8 @@
|
||||
"episodeBadge": "E{{episode}}",
|
||||
"loadingError": "Chyba při načítání sezóny",
|
||||
"loadingList": "Načítání...",
|
||||
"loadingTitle": "Načítání..."
|
||||
"loadingTitle": "Načítání...",
|
||||
"unairedEpisodes": "Jedna nebo více epizod v této sezóně nejsou dostupné, protože ještě nebyly odvysílány."
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "Rychlost přehrávání",
|
||||
|
@ -206,7 +206,8 @@
|
||||
"episodeBadge": "E{{episode}}",
|
||||
"loadingError": "Fehler beim Laden der Sitzung",
|
||||
"loadingList": "Wird geladen...",
|
||||
"loadingTitle": "Wird geladen..."
|
||||
"loadingTitle": "Wird geladen...",
|
||||
"unairedEpisodes": "Eine oder mehrere Episoden dieser Staffel wurden deaktiviert, weil sie noch nicht ausgestrahlt wurden."
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "Wiedergabegeschwindigkeit",
|
||||
|
@ -165,6 +165,12 @@
|
||||
"close": "Close"
|
||||
},
|
||||
"player": {
|
||||
"turnstile": {
|
||||
"verifyingHumanity": "Verifying your humanity...",
|
||||
"title": "We need to verify that you're human.",
|
||||
"description": "Please verify that you are human by completing the Captcha on the right. This is to keep movie-web safe!",
|
||||
"error": "Failed to verify your humanity. Please try again."
|
||||
},
|
||||
"back": {
|
||||
"default": "Back to home",
|
||||
"short": "Back"
|
||||
@ -261,6 +267,10 @@
|
||||
"text": "Could not load the media's metadata from TMDB. Please check whether TMDB is down or blocked on your internet connection.",
|
||||
"title": "Failed to load metadata"
|
||||
},
|
||||
"api": {
|
||||
"text": "Could not load API metadata, please check your internet connection.",
|
||||
"title": "Failed to load API metadata"
|
||||
},
|
||||
"notFound": {
|
||||
"badge": "Not found",
|
||||
"homeButton": "Back to home",
|
||||
|
@ -206,7 +206,8 @@
|
||||
"episodeBadge": "E{{episode}}",
|
||||
"loadingError": "Error al cargar la temporada",
|
||||
"loadingList": "Cargando...",
|
||||
"loadingTitle": "Cargando..."
|
||||
"loadingTitle": "Cargando...",
|
||||
"unairedEpisodes": "Uno o más episodios de esta temporada se han desactivado porque aún no se han emitido."
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "Velocidad de reproducción",
|
||||
@ -258,6 +259,10 @@
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"api": {
|
||||
"text": "No ha sido posible cargar la metadata de la API, por favor, comprueba tu conexión a internet.",
|
||||
"title": "No ha sido posible cargar los metadatos de la API"
|
||||
},
|
||||
"failed": {
|
||||
"badge": "Error",
|
||||
"homeButton": "Ir al inicio",
|
||||
@ -307,6 +312,12 @@
|
||||
"remaining": "{{timeLeft}} restante • Finaliza a las {{timeFinished, datetime}}",
|
||||
"shortRegular": "{{timeWatched}}",
|
||||
"shortRemaining": "-{{timeLeft}}"
|
||||
},
|
||||
"turnstile": {
|
||||
"description": "Por favor, confirma que eres humano completando el Captcha. Esto es para mantener movie-web seguro!",
|
||||
"error": "Ha habido un error al verificar tu humanidad. Por favor, prueba de nuevo.",
|
||||
"title": "Necesitamos verificar que eres humano.",
|
||||
"verifyingHumanity": "Verificando tu hunanidad…"
|
||||
}
|
||||
},
|
||||
"screens": {
|
||||
|
@ -206,7 +206,8 @@
|
||||
"episodeBadge": "E{{episode}}",
|
||||
"loadingError": "Hooaja laadimine ebaōnnestus",
|
||||
"loadingList": "Laadimine...",
|
||||
"loadingTitle": "Laadimine..."
|
||||
"loadingTitle": "Laadimine...",
|
||||
"unairedEpisodes": "Üks või mitu selle hooaja episoodi on välja lülitatud, sest neid ei ole veel eetris olnud."
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "Taasesituse kiirus",
|
||||
@ -258,6 +259,10 @@
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"api": {
|
||||
"text": "API metaandmete laadimine ebaõnnestus, palun kontrollige oma internetiühendust.",
|
||||
"title": "API metaandmete laadimine ebaõnnestus"
|
||||
},
|
||||
"failed": {
|
||||
"badge": "Ebaōnnestus",
|
||||
"homeButton": "Mine koju",
|
||||
@ -307,6 +312,12 @@
|
||||
"remaining": "{{timeLeft}} alles • Lõppeb {{timeFinished, datetime}}",
|
||||
"shortRegular": "{{timeWatched}}",
|
||||
"shortRemaining": "-{{timeLeft}}"
|
||||
},
|
||||
"turnstile": {
|
||||
"description": "Palun kinnitage, et olete inimene, täites paremal asuva Captcha. See on selleks, et hoida movie-web turvalisena!",
|
||||
"error": "Ei õnnestunud kontrollida teie inimlikkust. Palun proovige uuesti.",
|
||||
"title": "Me peame kontrollima, et te olete inimene.",
|
||||
"verifyingHumanity": "Kontrollime kas olete robot..."
|
||||
}
|
||||
},
|
||||
"screens": {
|
||||
|
@ -206,7 +206,8 @@
|
||||
"episodeBadge": "E{{episode}}",
|
||||
"loadingError": "Erreur de chargement de la saison",
|
||||
"loadingList": "Chargement...",
|
||||
"loadingTitle": "Chargement..."
|
||||
"loadingTitle": "Chargement...",
|
||||
"unairedEpisodes": "Un ou plusieurs épisodes de cette saison ont été désactivés car ils n'ont pas encore été diffusés."
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "Vitesse de lecture",
|
||||
|
437
src/assets/locales/gl.json
Normal file
437
src/assets/locales/gl.json
Normal file
@ -0,0 +1,437 @@
|
||||
{
|
||||
"about": {
|
||||
"description": "movie-web é unha aplicación web que busca transmisións na rede. O equipo ten como obxectivo manter un enfoque principalmente minimalista para consumir os contidos.",
|
||||
"faqTitle": "Preguntas frecuentes",
|
||||
"q1": {
|
||||
"body": "movie-web non aloxa ningún contido. Cando premes en algo para ver o contenido, búscase en internet o medio seleccionado. (Na pantalla de carga e na lapela 'fontes de video' podes ver que fonte se está a empregar. O contido nunca se carga en movie-web, todo realízase a través deste método de busca.",
|
||||
"title": "De onde proveñen os contidos?"
|
||||
},
|
||||
"q2": {
|
||||
"body": "Non é posible solicitar unha película. movie-web non xestiona ningún contido. Todo o contido é xestionado a través de fontes na rede.",
|
||||
"title": "Onde poido solicitar unha película a engadir?"
|
||||
},
|
||||
"q3": {
|
||||
"body": "Os nosos resultados de busqueda proveñen de The Movie Database (TMDB) e se mostran independentemente de se as nosas fontes multimedia teñen realmente o contido.",
|
||||
"title": "Os resultados da busca mostran a serie ou película... Por qué non poido reproducila?"
|
||||
},
|
||||
"title": "Acerca de movie-web"
|
||||
},
|
||||
"actions": {
|
||||
"copied": "Copiado",
|
||||
"copy": "Copiar"
|
||||
},
|
||||
"auth": {
|
||||
"createAccount": "Non tes unha conta aínda? <0>Crea unha conta.</0>",
|
||||
"deviceNameLabel": "Nome do dispositivo",
|
||||
"deviceNamePlaceholder": "Teléfono persoal",
|
||||
"generate": {
|
||||
"description": "A túa contraseña actua como o teu nome de usuario e contraseña. Asegúrate de mantelas seguras, xa que as necesitarás para iniciar sesión na túa conta",
|
||||
"next": "Gardei a contraseña exitosamente",
|
||||
"passphraseFrameLabel": "Contraseña",
|
||||
"title": "A túa contraseña"
|
||||
},
|
||||
"hasAccount": "Tes xa unha conta? <0>Inicia sesión aquí.</0>",
|
||||
"login": {
|
||||
"description": "Por favor, ingresa a túa contraseña para iniciar sesión na túa conta",
|
||||
"deviceLengthError": "Por favor, ingresa un nome de dispositivo",
|
||||
"passphraseLabel": "Contraseña de 12 caracteres",
|
||||
"passphrasePlaceholder": "Contraseña",
|
||||
"submit": "Iniciar sesión",
|
||||
"title": "Inicia sesión na túa conta",
|
||||
"validationError": "Contraseña incorrecta ou incompleta"
|
||||
},
|
||||
"register": {
|
||||
"information": {
|
||||
"color1": "Cór de perfil un",
|
||||
"color2": "Cór de perfil dous",
|
||||
"header": "Ingresa un nome para o teu dispositivo, elixe cores, e un icono de usuario",
|
||||
"icon": "Ícono de usuario",
|
||||
"next": "Seguinte",
|
||||
"title": "Información da conta"
|
||||
}
|
||||
},
|
||||
"trust": {
|
||||
"failed": {
|
||||
"text": "Configurachelo correctamente?",
|
||||
"title": "Non se puido conectar ao servidor"
|
||||
},
|
||||
"host": "Estaste a conectar a <0>{{hostname}}</0> - por favor, confirma se confías antes de crear a conta",
|
||||
"no": "Regresar",
|
||||
"title": "Confías neste servidor?",
|
||||
"yes": "Si, si que confío neste servidor"
|
||||
},
|
||||
"verify": {
|
||||
"description": "Por favor, ingresa a túa contraseña para confirmar que está gardada para crear a túa conta",
|
||||
"invalidData": "Os datos non son válidos",
|
||||
"noMatch": "A contraseña non coincide",
|
||||
"passphraseLabel": "A contraseña debe de ser de 12 caracteres",
|
||||
"recaptchaFailed": "A validación ReCaptcha fallou",
|
||||
"register": "Crear conta",
|
||||
"title": "Confirma a túa contraseña"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"badge": "Rompeu",
|
||||
"details": "Detalles do erro",
|
||||
"reloadPage": "Recargar a páxina",
|
||||
"showError": "Mostrar detalles do erro",
|
||||
"title": "Atopamos un erro!"
|
||||
},
|
||||
"footer": {
|
||||
"legal": {
|
||||
"disclaimer": "Descargo de responsabilidade",
|
||||
"disclaimerText": "movie-web non aloxa ningún arquivo, simplemente enlaza con servizos de terceiros. Os problemas legais deben ser tratados cós proovedores de arquivos e servizos. movie-web non se fai responsable dos arquivos multimedia mostrados polos provedores de video."
|
||||
},
|
||||
"links": {
|
||||
"discord": "Discord",
|
||||
"dmca": "DMCA",
|
||||
"github": "GitHub"
|
||||
},
|
||||
"tagline": "Disfruta das túas series e películas favoritas con esta aplicación de transmisión de código aberto."
|
||||
},
|
||||
"global": {
|
||||
"name": "movie-web",
|
||||
"pages": {
|
||||
"about": "Acerca de",
|
||||
"dmca": "DMCA",
|
||||
"login": "Iniciar sesión",
|
||||
"pagetitle": "{{title}} - movie-web",
|
||||
"register": "Rexistrarse",
|
||||
"settings": "Configuración"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"bookmarks": {
|
||||
"sectionTitle": "Marcadores"
|
||||
},
|
||||
"continueWatching": {
|
||||
"sectionTitle": "Continuar vendo"
|
||||
},
|
||||
"mediaList": {
|
||||
"stopEditing": "Deter edición"
|
||||
},
|
||||
"search": {
|
||||
"allResults": "Esto é todo o que temos!",
|
||||
"failed": "Error ao encontrar contido... intentao de novo!",
|
||||
"loading": "Cargando...",
|
||||
"noResults": "Non atopamos nada!",
|
||||
"placeholder": "Que che gustaría ver?",
|
||||
"sectionTitle": "Resultados da busca"
|
||||
},
|
||||
"titles": {
|
||||
"day": {
|
||||
"default": "Que che gustaría ver esta tarde?",
|
||||
"extra": [
|
||||
"Sínteste aventureiro? Jurassic Park podería ser a elección perfecta."
|
||||
]
|
||||
},
|
||||
"morning": {
|
||||
"default": "Que che gustaría ver esta mañá?",
|
||||
"extra": [
|
||||
"Escoitei que “Antes del amanecer” é boa"
|
||||
]
|
||||
},
|
||||
"night": {
|
||||
"default": "Que che gustaría ver esta noite?",
|
||||
"extra": [
|
||||
"Canso? Escoitei que “El Exorcista” é boa."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"episodeDisplay": "T{{season}} E{{episode}}",
|
||||
"types": {
|
||||
"movie": "Película",
|
||||
"show": "Serie"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"banner": {
|
||||
"offline": "Verifica a túa conexión a internet"
|
||||
},
|
||||
"menu": {
|
||||
"about": "Acerca de nós",
|
||||
"donation": "Doar",
|
||||
"logout": "Cerrar sesión",
|
||||
"register": "Sincronizar coa nube",
|
||||
"settings": "Configuración",
|
||||
"support": "Soporte"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"badge": "Non atopado",
|
||||
"goHome": "Volver ao inicio",
|
||||
"message": "Prometocho, buscamos en todas partes: debaixo dos contenedores, no armario, detrás do proxy, pero ao final non puidemos atopar a páxina que estabas buscando.",
|
||||
"title": "Non atopei a páxona que estabas a buscar"
|
||||
},
|
||||
"overlays": {
|
||||
"close": "Cerrar"
|
||||
},
|
||||
"player": {
|
||||
"back": {
|
||||
"default": "Volver ao inicio",
|
||||
"short": "Volver"
|
||||
},
|
||||
"casting": {
|
||||
"enabled": "Transmitiendo ao dispositivo..."
|
||||
},
|
||||
"menus": {
|
||||
"downloads": {
|
||||
"disclaimer": "As descargas proveñen do provedor. movie-web non ten control sobre as descargas e a súa procedencia.",
|
||||
"downloadPlaylist": "Descargar lista",
|
||||
"downloadSubtitle": "Descargar subtítulos actuais",
|
||||
"downloadVideo": "Descargar video",
|
||||
"hlsDisclaimer": "As descargas realizanse directamente dende o proovedor. movie-web non ten control sobre como se xestionan as descargas. Ten en conta que estás a descargar unha lista de reproducción HLS, dirixidos a usuarios familiarizados coa transmisión multimedia avanzada.",
|
||||
"onAndroid": {
|
||||
"1": "Para descargar en Android, fai click no botón de descarga e despois, na nova páxina, <bold>mantén presionado</bold>o vídeo e selecciona <bold>gardar</bold>.",
|
||||
"shortTitle": "Descargar / Android",
|
||||
"title": "Descargando en Android"
|
||||
},
|
||||
"onIos": {
|
||||
"1": "Para descargar en iOS, fai clic no botón de descarga e despois, na nova páxina, fai click en <bold><ios_share /></bold>, e despois <bold>Gardar en archivos <ios_files /></bold>.",
|
||||
"shortTitle": "Descargar / iOS",
|
||||
"title": "Descargando en iOS"
|
||||
},
|
||||
"onPc": {
|
||||
"1": "Nunha PC, fai click no botón de descargas e despois, na nova páxina, fai click dereito no video e selecciona <bold>Gardar vídeo como...</bold>",
|
||||
"shortTitle": "Descargar / PC",
|
||||
"title": "Descargando en PC"
|
||||
},
|
||||
"title": "Descargar"
|
||||
},
|
||||
"episodes": {
|
||||
"button": "Episodios",
|
||||
"emptyState": "Non hai episodios nesta temporada, Intentao máis tarde!",
|
||||
"episodeBadge": "E{{episode}}",
|
||||
"loadingError": "Error cargando a sesión",
|
||||
"loadingList": "Cargando...",
|
||||
"loadingTitle": "Cargando...",
|
||||
"unairedEpisodes": "Un ou máis episodios nesta temporada foron desactivados porque non sairon aínda."
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "Velocidade de reproducción",
|
||||
"title": "Configuración de reproducción"
|
||||
},
|
||||
"quality": {
|
||||
"automaticLabel": "Calidade automática",
|
||||
"hint": "Podes intentar <0>cambiar de fonte</0> para obter diferentes opcións de calidade.",
|
||||
"iosNoQuality": "Debido a limitacións definidas por Apple, a selección de calidade no está disponible en iOS para esta fonte. Podes intentar <0>cambiar a outra fonte</0> para obter diferentes opcións de calidade.",
|
||||
"title": "Calidade"
|
||||
},
|
||||
"settings": {
|
||||
"downloadItem": "Descargar",
|
||||
"enableSubtitles": "Activar subtítulos",
|
||||
"experienceSection": "Configuración de experiencia",
|
||||
"playbackItem": "Configuración do playback",
|
||||
"qualityItem": "Calidade",
|
||||
"sourceItem": "Fonte do video",
|
||||
"subtitleItem": "Configuración dos subtítulos",
|
||||
"videoSection": "Configuración de video"
|
||||
},
|
||||
"sources": {
|
||||
"failed": {
|
||||
"text": "Acaba de producirse un erro ao intentar atopar videos, por favor, intenta cunha fonte distinta.",
|
||||
"title": "Erro ao retirar"
|
||||
},
|
||||
"noEmbeds": {
|
||||
"text": "Non puidemos atopar ningún embed, por favor, intenta cunha fonte diferente.",
|
||||
"title": "No se atoparon embeds"
|
||||
},
|
||||
"noStream": {
|
||||
"text": "Nesta fonte non hai contidos sobre esta película ou episodio.",
|
||||
"title": "Sin fonte"
|
||||
},
|
||||
"title": "Fontes",
|
||||
"unknownOption": "Descoñecido"
|
||||
},
|
||||
"subtitles": {
|
||||
"customChoice": "Seleccionar subtítulos dende o arquivo",
|
||||
"customizeLabel": "Personalizar",
|
||||
"offChoice": "Apagar",
|
||||
"settings": {
|
||||
"backlink": "Subtítulos personalizados",
|
||||
"delay": "Retardo dos subtítulos",
|
||||
"fixCapitals": "Arreglar capitalización"
|
||||
},
|
||||
"title": "Subtítulos",
|
||||
"unknownLanguage": "Descoñecido"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"api": {
|
||||
"text": "Non puiden cargar os metadatos da API, por favor, comproba a túa conexión a internet.",
|
||||
"title": "Non foi posible cargar os metadatos da API"
|
||||
},
|
||||
"failed": {
|
||||
"badge": "Erro",
|
||||
"homeButton": "Ir ao inicio",
|
||||
"text": "Non se puideron cargar os metadatos do contido de TMDB. Por favor, verifica se TMDB está caído ou bloqueado na túa conexión a internet.",
|
||||
"title": "Error ao cargar os metadatos"
|
||||
},
|
||||
"notFound": {
|
||||
"badge": "Non atopado",
|
||||
"homeButton": "Volver ao inicio",
|
||||
"text": "Non puidemos encontrar o contenido que solicitache. Xa seña que se eliminara ou modificara a URL.",
|
||||
"title": "No se pudo atopar ese contenido."
|
||||
}
|
||||
},
|
||||
"nextEpisode": {
|
||||
"cancel": "Cancelar",
|
||||
"next": "Seguinte episodio"
|
||||
},
|
||||
"playbackError": {
|
||||
"badge": "Error de reproducción",
|
||||
"errors": {
|
||||
"errorAborted": "A obtención do contido foi cancelada pola solicitude do usuario.",
|
||||
"errorDecode": "A pesar de ser determinado previamente como utilizable, produciuse un erro ao intentar decodificar o recurso do contido, o que resultou nun erro.",
|
||||
"errorGenericMedia": "Produxose un erro descoñecido no contido.",
|
||||
"errorNetwork": "Produxose un erro de rede que impidideu obter o contido de maneira exitosa, a pesar de estar disponible anteriormente.",
|
||||
"errorNotSupported": "O contido ou o proovedor do contido non é compatible."
|
||||
},
|
||||
"homeButton": "Ir ao inicio",
|
||||
"text": "Produxose un erro ao intentar reproducir o contenido. Por favor, inténtao de novo.",
|
||||
"title": "Non se puido reproducir o video!"
|
||||
},
|
||||
"scraping": {
|
||||
"items": {
|
||||
"failure": "Ocurreu un erro",
|
||||
"notFound": "Non ten o video",
|
||||
"pending": "Verificando vídeos..."
|
||||
},
|
||||
"notFound": {
|
||||
"badge": "Non atopado",
|
||||
"detailsButton": "Mostrar detalles",
|
||||
"homeButton": "Ir ao inicio",
|
||||
"text": "Buscamos nos nosos proovedores e non puidemos atopar o contido que estás a buscar. Nós, non aloxamos o contido e non temos control sobre o que está dispoñible. Fai click en 'Mostrar detalles' a continuación para obter máis información.",
|
||||
"title": "Non puidemos atopar eso"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"regular": "{{timeWatched}} / {{duration}}",
|
||||
"remaining": "{{timeLeft}} restante • Finaliza ás {{timeFinished, datetime}}",
|
||||
"shortRegular": "{{timeWatched}}",
|
||||
"shortRemaining": "-{{timeLeft}}"
|
||||
},
|
||||
"turnstile": {
|
||||
"description": "Por favor, verifica que eres un humán completando o Captcha. Isto é para mantee movie-web seguro!",
|
||||
"error": "Houbo un erro ao verificar a túa humanidade. Por favor, volve a intentalo.",
|
||||
"title": "Necesitamos verificar que realmente eres un humán.",
|
||||
"verifyingHumanity": "Verificando a túa humanidade…"
|
||||
}
|
||||
},
|
||||
"screens": {
|
||||
"dmca": {
|
||||
"text": "Benvido/a á páxona de contacto DMA de movie-web! Respetamos os dereitos de propiedade intelectual e queremos abordar calqueiro problema de dereitos de autor de maneira más rápida. Se crees que o teu traballo con dereitos de autor está sendo empregado incorrectamente na nosa plataforma, envñia un aviso DMCA detallado ao correo electrónico que se mostra a continuación. Inclue unha descripción do material con dereitos de autor, os seus datos de contacto e unha declaración de boa fé. Estamos comprometidos a resolver estos asuntos o máis rápido posible e agradecemos a túa cooperación para manter a movie-web como un lugar que respeta a creatividade e os dereitos de autor.",
|
||||
"title": "DMCA"
|
||||
},
|
||||
"loadingApp": "Cargando aplicación",
|
||||
"loadingUser": "Cargando o teu perfil",
|
||||
"loadingUserError": {
|
||||
"logout": "Pechar sesión",
|
||||
"reset": "Reiniciar servidor personalizado",
|
||||
"text": "Erro ao cargar o teu perfil",
|
||||
"textWithReset": "Erro ao cargar o teu perfil dende o teu servidor personalizado, queres reiniciar e volver ao servidor por defecto?"
|
||||
},
|
||||
"migration": {
|
||||
"failed": "Erro ao migrar os teus datos.",
|
||||
"inProgress": "Porfavor, espera mientras migramos tus datos. Esto no debería llevar mucho."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"account": {
|
||||
"accountDetails": {
|
||||
"deviceNameLabel": "Nome do dispositivo",
|
||||
"deviceNamePlaceholder": "Teléfono persoal",
|
||||
"editProfile": "Editar",
|
||||
"logoutButton": "Pechar sesión"
|
||||
},
|
||||
"actions": {
|
||||
"delete": {
|
||||
"button": "Eliminar conta",
|
||||
"confirmButton": "Eliminar conta",
|
||||
"confirmDescription": "Estas seguro/a que queres eliminar a túa conta? Todos os datos serán eliminados!",
|
||||
"confirmTitle": "Estás seguro/a?",
|
||||
"text": "Esta acción é irreversible. Todos os datos serán eliminados e nada poderá ser recuperado.",
|
||||
"title": "Eliminar conta"
|
||||
},
|
||||
"title": "Accións"
|
||||
},
|
||||
"devices": {
|
||||
"deviceNameLabel": "Nome do dispositivo",
|
||||
"failed": "Erro ao cargar sesións",
|
||||
"removeDevice": "Quitar",
|
||||
"title": "Dispositivos"
|
||||
},
|
||||
"profile": {
|
||||
"finish": "Acabar de editar",
|
||||
"firstColor": "Cór de perfil un",
|
||||
"secondColor": "Cór de perfil dous",
|
||||
"title": "Editar foto de perfil",
|
||||
"userIcon": "Icono de usuario"
|
||||
},
|
||||
"register": {
|
||||
"cta": "Empezar",
|
||||
"text": "Compartir o teu progreso entre dispositivos e mantelos sincronizados.",
|
||||
"title": "Sincronizar á nube"
|
||||
},
|
||||
"title": "Conta"
|
||||
},
|
||||
"appearance": {
|
||||
"activeTheme": "Activo",
|
||||
"themes": {
|
||||
"blue": "Azul",
|
||||
"default": "Por defecto",
|
||||
"gray": "Gris",
|
||||
"red": "Vermello",
|
||||
"teal": "Turquesa"
|
||||
},
|
||||
"title": "Apariencia"
|
||||
},
|
||||
"connections": {
|
||||
"server": {
|
||||
"description": "Se che gustaría conectar un servidor personalizado de backend para almacenar os teus datos, activa esto e indica a URL.",
|
||||
"label": "Servidor personalizado",
|
||||
"urlLabel": "Servidor personalizado URL"
|
||||
},
|
||||
"title": "Conexións",
|
||||
"workers": {
|
||||
"addButton": "Añadir novo",
|
||||
"description": "Para facer que a aplicación funcione, todo o tráfico é organizado en proxies. Activa esta opción se queres empregar os teus propios workers.",
|
||||
"emptyState": "Non hai workers aínda, engade un abaixo",
|
||||
"label": "Usar proxy workers personalizados",
|
||||
"urlLabel": "URLs dos workers",
|
||||
"urlPlaceholder": "https://"
|
||||
}
|
||||
},
|
||||
"locale": {
|
||||
"language": "Lingua da aplicación",
|
||||
"languageDescription": "Lingua empregada en toda aplicación.",
|
||||
"title": "Local"
|
||||
},
|
||||
"reset": "Reinicio",
|
||||
"save": "Gardar",
|
||||
"sidebar": {
|
||||
"info": {
|
||||
"appVersion": "Versión da aplicación",
|
||||
"backendUrl": "URL do Backend",
|
||||
"backendVersion": "Versión do Backend",
|
||||
"hostname": "Nome do Host (Hostname)",
|
||||
"insecure": "Non seguro",
|
||||
"notLoggedIn": "Non iniciache sesión",
|
||||
"secure": "Seguro",
|
||||
"title": "Información da aplicación",
|
||||
"unknownVersion": "Descoñecido",
|
||||
"userId": "ID do usuario"
|
||||
}
|
||||
},
|
||||
"subtitles": {
|
||||
"backgroundLabel": "Opacidade do fondo",
|
||||
"colorLabel": "Cór",
|
||||
"previewQuote": "Non debo temer. O medo é o asasino da mente.",
|
||||
"textSizeLabel": "Tamaño da fonte",
|
||||
"title": "Subtítulos"
|
||||
},
|
||||
"unsaved": "Tes cambios sen gardar"
|
||||
}
|
||||
}
|
@ -206,7 +206,8 @@
|
||||
"episodeBadge": "એપિસોડ{{episode}}",
|
||||
"loadingError": "સીઝન લોડ કરવામાં ભૂલ",
|
||||
"loadingList": "લોડ થાય છે...",
|
||||
"loadingTitle": "લોડ થાય છે..."
|
||||
"loadingTitle": "લોડ થાય છે...",
|
||||
"unairedEpisodes": "આ સિઝનમાં એક અથવા વધુ એપિસોડ અક્ષમ કરવામાં આવ્યા છે કારણ કે તે હજુ સુધી પ્રસારિત થયા નથી."
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "પ્લેબેક ઝડપ",
|
||||
|
@ -155,7 +155,7 @@
|
||||
"donation": "दान करें",
|
||||
"logout": "लॉग आउट",
|
||||
"register": "क्लाउड से सिंक करें",
|
||||
"settings": "चेड चाड करे",
|
||||
"settings": "सेटिंग्स",
|
||||
"support": "सहायता"
|
||||
}
|
||||
},
|
||||
@ -206,7 +206,8 @@
|
||||
"episodeBadge": "E{{episode}}",
|
||||
"loadingError": "सीज़न लोड करने में त्रुटि",
|
||||
"loadingList": "लोड हो रहा है..।",
|
||||
"loadingTitle": "लोड हो रहा है..।"
|
||||
"loadingTitle": "लोड हो रहा है..।",
|
||||
"unairedEpisodes": "इस सीज़न में एक या अधिक एपिसोड अक्षम कर दिए गए हैं क्योंकि वे अभी तक प्रसारित नहीं हुए हैं।"
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "प्लेबैक गति",
|
||||
|
@ -206,7 +206,8 @@
|
||||
"episodeBadge": "A{{episode}}",
|
||||
"loadingError": "Er ging iets mis bij het laden van dit seizoen",
|
||||
"loadingList": "Aan het laden...",
|
||||
"loadingTitle": "Aan het zoeken..."
|
||||
"loadingTitle": "Aan het zoeken...",
|
||||
"unairedEpisodes": "Een of meer afleveringen van dit seizoen zijn uitgeschakeld omdat ze nog niet zijn uitgezonden."
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "Afspeelsnelheid",
|
||||
@ -361,8 +362,66 @@
|
||||
},
|
||||
"register": {
|
||||
"cta": "Aan de slag",
|
||||
"text": "Deel de voortgang van je film tussen apparaten en houd ze gesynchroniseerd."
|
||||
}
|
||||
}
|
||||
"text": "Deel uw kijkvoortgang tussen apparaten en houd ze gesynchroniseerd.",
|
||||
"title": "Synchroniseren met de cloud"
|
||||
},
|
||||
"title": "Account"
|
||||
},
|
||||
"appearance": {
|
||||
"activeTheme": "Actief",
|
||||
"themes": {
|
||||
"blue": "Blauw",
|
||||
"default": "Standaard",
|
||||
"gray": "Grijs",
|
||||
"red": "Rood",
|
||||
"teal": "Groenblauw"
|
||||
},
|
||||
"title": "Uiterlijk"
|
||||
},
|
||||
"connections": {
|
||||
"server": {
|
||||
"description": "Als je verbinding wilt maken met een eigen backend om je gegevens op te slaan, schakel dan deze optie in en geef de URL op.",
|
||||
"label": "Eigen server",
|
||||
"urlLabel": "Eigen server URL"
|
||||
},
|
||||
"title": "Verbindingen",
|
||||
"workers": {
|
||||
"addButton": "Nieuwe worker toevoegen",
|
||||
"description": "Om de applicatie te laten werken, wordt al het verkeer omgeleid via proxies. Schakel dit in als je je eigen workers wilt gebruiken.",
|
||||
"emptyState": "Nog geen workers, voeg er hieronder een toe",
|
||||
"label": "Eigen proxy werker gebruiken",
|
||||
"urlLabel": "Worker URLs",
|
||||
"urlPlaceholder": "https://"
|
||||
}
|
||||
},
|
||||
"locale": {
|
||||
"language": "Applicatietaal",
|
||||
"languageDescription": "Taal wordt toegepast op de hele applicatie.",
|
||||
"title": "Lokaal"
|
||||
},
|
||||
"reset": "Resetten",
|
||||
"save": "Wijzigingen opslaan",
|
||||
"sidebar": {
|
||||
"info": {
|
||||
"appVersion": "App versie",
|
||||
"backendUrl": "Backend URL",
|
||||
"backendVersion": "Backend versie",
|
||||
"hostname": "hostnaam",
|
||||
"insecure": "Onveilig",
|
||||
"notLoggedIn": "U bent niet ingelogd",
|
||||
"secure": "Veilig",
|
||||
"title": "App informatie",
|
||||
"unknownVersion": "Onbekend",
|
||||
"userId": "Gebruiker ID"
|
||||
}
|
||||
},
|
||||
"subtitles": {
|
||||
"backgroundLabel": "Achtergrond dekking",
|
||||
"colorLabel": "Kleur",
|
||||
"previewQuote": "Ik mag niet bang zijn. Angst doodt de geest.",
|
||||
"textSizeLabel": "Tekengrootte",
|
||||
"title": "Ondertiteling"
|
||||
},
|
||||
"unsaved": "U heeft niet-opgeslagen wijzigingen"
|
||||
}
|
||||
}
|
||||
|
18
src/assets/locales/pa.json
Normal file
18
src/assets/locales/pa.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"actions": {
|
||||
"copy": "ਕਾਪੀ"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"submit": "ਲੌਗ-ਇਨ"
|
||||
},
|
||||
"register": {
|
||||
"information": {
|
||||
"next": "ਅਗਲਾ"
|
||||
}
|
||||
},
|
||||
"trust": {
|
||||
"no": "ਵਾਪਸ ਜਾਓ"
|
||||
}
|
||||
}
|
||||
}
|
216
src/assets/locales/ro.json
Normal file
216
src/assets/locales/ro.json
Normal file
@ -0,0 +1,216 @@
|
||||
{
|
||||
"about": {
|
||||
"description": "movie-web este o aplicație web care caută fluxuri pe internet. Echipa urmărește o abordare mai ales minimalistă a consumului de conținut.",
|
||||
"faqTitle": "Intrebari obisnuite",
|
||||
"q1": {
|
||||
"body": "movie-web nu găzduiește niciun conținut. Când faceți clic pe ceva pentru a viziona, pe Internet este căutat media selectată (Pe ecranul de încărcare și în fila „Surse video”, puteți vedea ce sursă utilizați). Media nu este niciodată încărcată de movie-web, totul se face prin acest mecanism de căutare.",
|
||||
"title": "De unde vine conținutul?"
|
||||
},
|
||||
"q2": {
|
||||
"body": "Nu este posibil să solicitați o emisiune sau un film, movie-web nu gestionează niciun conținut. Tot conținutul este vizualizat prin surse de internet.",
|
||||
"title": "Unde pot solicita o emisiune sau un film?"
|
||||
},
|
||||
"q3": {
|
||||
"body": "Rezultatele căutării noastre sunt furnizate de The Movie Database (TMDB) și afișați indiferent dacă sursele noastre au de fapt conținutul.",
|
||||
"title": "Rezultatele căutării afișează emisiunea sau filmul, de ce nu îl pot reda?"
|
||||
},
|
||||
"title": "Despre movie-web"
|
||||
},
|
||||
"actions": {
|
||||
"copied": "Copiat",
|
||||
"copy": "Copie"
|
||||
},
|
||||
"auth": {
|
||||
"createAccount": "Nu aveți încă un cont? <0>Creați un cont.</0>",
|
||||
"deviceNameLabel": "Nume dispozitiv",
|
||||
"deviceNamePlaceholder": "Telefon personal",
|
||||
"generate": {
|
||||
"description": "Fraza de acces acționează ca nume de utilizator și parolă. Asigurați-vă că îl păstrați în siguranță, deoarece va trebui să îl introduceți pentru a vă conecta la contul dvs",
|
||||
"next": "Mi-am salvat expresia de acces",
|
||||
"passphraseFrameLabel": "Fraza de acces",
|
||||
"title": "Fraza dvs. de acces"
|
||||
},
|
||||
"hasAccount": "ai deja un cont? <0>Autentificați-vă aici.</0>",
|
||||
"login": {
|
||||
"description": "Vă rugăm să introduceți fraza de acces pentru a vă conecta la contul dvs",
|
||||
"deviceLengthError": "Introduceți un nume de dispozitiv",
|
||||
"passphraseLabel": "Expresie de acces din 12 cuvinte",
|
||||
"passphrasePlaceholder": "Fraza de acces",
|
||||
"submit": "Log in",
|
||||
"title": "conecteaza-te la contul tau",
|
||||
"validationError": "Fraza de acces incorectă sau incompletă"
|
||||
},
|
||||
"register": {
|
||||
"information": {
|
||||
"color1": "Culoarea profilului unu",
|
||||
"color2": "Culoarea profilului doi",
|
||||
"header": "Introduceți un nume pentru dispozitivul dvs. și alegeți culorile și o pictogramă de utilizator la alegerea dvs",
|
||||
"icon": "Pictograma utilizator",
|
||||
"next": "Următorul",
|
||||
"title": "Informatii despre cont"
|
||||
}
|
||||
},
|
||||
"trust": {
|
||||
"failed": {
|
||||
"text": "L-ai configurat corect?",
|
||||
"title": "Nu s-a putut ajunge la server"
|
||||
},
|
||||
"host": "Vă conectați la <0>{{hostname}}</0> - vă rugăm să confirmați că aveți încredere înainte de a vă crea un cont",
|
||||
"no": "Întoarce-te",
|
||||
"title": "Ai încredere în acest server?",
|
||||
"yes": "Am încredere în acest server"
|
||||
},
|
||||
"verify": {
|
||||
"description": "Introduceți expresia de acces de mai devreme pentru a confirma că ați salvat-o și pentru a vă crea contul",
|
||||
"invalidData": "Datele nu sunt valide",
|
||||
"noMatch": "Fraza de acces nu se potrivește",
|
||||
"passphraseLabel": "Fraza dvs. de acces de 12 cuvinte",
|
||||
"recaptchaFailed": "Validarea ReCaptcha a eșuat",
|
||||
"register": "Creează cont",
|
||||
"title": "Confirmați-vă fraza de acces"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"badge": "S-a spart",
|
||||
"details": "Detalii despre eroare",
|
||||
"reloadPage": "Reîncărcați pagina",
|
||||
"showError": "Afișați detalii despre eroare",
|
||||
"title": "Am intampinat o eroare!"
|
||||
},
|
||||
"footer": {
|
||||
"legal": {
|
||||
"disclaimer": "Disclaimer",
|
||||
"disclaimerText": "movie-web nu găzduiește niciun fișier, ci doar trimite la servicii terțe. Problemele juridice ar trebui abordate cu gazdele și furnizorii de fișiere. movie-web nu este responsabil pentru niciun fișier media afișat de furnizorii de video."
|
||||
},
|
||||
"links": {
|
||||
"discord": "Discord",
|
||||
"dmca": "DMCA",
|
||||
"github": "GitHub"
|
||||
},
|
||||
"tagline": "Urmăriți emisiunile și filmele preferate cu această aplicație de streaming open source."
|
||||
},
|
||||
"global": {
|
||||
"name": "movie-web",
|
||||
"pages": {
|
||||
"about": "Despre",
|
||||
"dmca": "DMCA",
|
||||
"login": "Log in",
|
||||
"pagetitle": "{{title}} - movie-web",
|
||||
"register": "Inregistreaza-te",
|
||||
"settings": "Setări"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"bookmarks": {
|
||||
"sectionTitle": "Marcaje"
|
||||
},
|
||||
"continueWatching": {
|
||||
"sectionTitle": "Continuați vizionarea"
|
||||
},
|
||||
"mediaList": {
|
||||
"stopEditing": "Opriți editarea"
|
||||
},
|
||||
"search": {
|
||||
"allResults": "Asta e tot ce avem!",
|
||||
"failed": "Găsire media eșuată, încearcă din nou!",
|
||||
"loading": "Se încarcă...",
|
||||
"noResults": "Nu am putut găsi nimic!",
|
||||
"placeholder": "La ce dorești să te uiți?",
|
||||
"sectionTitle": "Rezultate de căutare"
|
||||
},
|
||||
"titles": {
|
||||
"day": {
|
||||
"default": "La ce vrei să te uiți după-amiaza asta?",
|
||||
"extra": [
|
||||
"Te simți aventuros? Jurassic Park ar putea fi o alegere perfectă."
|
||||
]
|
||||
},
|
||||
"morning": {
|
||||
"default": "La ce dorești să te in uiți dimineață aceasta?",
|
||||
"extra": [
|
||||
"Aud că Before Sunrise este bun"
|
||||
]
|
||||
},
|
||||
"night": {
|
||||
"default": "La ce dorești să te uiți în astă seară?",
|
||||
"extra": [
|
||||
"Obosit? Aud că The Exorcist is good."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"episodeDisplay": "S{{season}} E{{episode}}",
|
||||
"types": {
|
||||
"movie": "Film",
|
||||
"show": "Spectacol"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"banner": {
|
||||
"offline": "Verificați-vă conexiunea de internet"
|
||||
},
|
||||
"menu": {
|
||||
"about": "Despre noi",
|
||||
"donation": "Donează",
|
||||
"logout": "Deconectați-vă",
|
||||
"register": "Sincronizare în cloud",
|
||||
"settings": "Setări",
|
||||
"support": "Ajutor"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"badge": "Nu a fost găsit",
|
||||
"goHome": "Înapoi acasă",
|
||||
"message": "Ne-am uitat peste tot: sub pubele, în dulap, În spatele proxy-ului dar din păcate nu am găsit pagina pe care dumneavoastră o căutați.",
|
||||
"title": "N-am putut găsi pagina"
|
||||
},
|
||||
"overlays": {
|
||||
"close": "Închide"
|
||||
},
|
||||
"player": {
|
||||
"back": {
|
||||
"default": "Înapoi acasă",
|
||||
"short": "Înapoi"
|
||||
},
|
||||
"casting": {
|
||||
"enabled": "Casting pe dispozitiv..."
|
||||
},
|
||||
"menus": {
|
||||
"episodes": {
|
||||
"button": "Episoade",
|
||||
"emptyState": "Nu sunt episoade in sezonul acesta, reveniți mai târziu!",
|
||||
"episodeBadge": "E{{episode}}",
|
||||
"loadingError": "Eroare la încărcarea sezonul",
|
||||
"loadingList": "Se încarcă...",
|
||||
"loadingTitle": "Se încarcă...",
|
||||
"unairedEpisodes": "Unul sau mai multe episoade din sezonul acesta sunt indisponibile deoarece incă nu au venit încă."
|
||||
},
|
||||
"settings": {
|
||||
"downloadItem": "Descarcă",
|
||||
"enableSubtitles": "Activează subtitlurile",
|
||||
"experienceSection": "Experiență de vizionare",
|
||||
"playbackItem": "Setări de redare",
|
||||
"qualityItem": "Calitate",
|
||||
"sourceItem": "Surse video",
|
||||
"subtitleItem": "Setările subtitlului",
|
||||
"videoSection": "Setări video"
|
||||
},
|
||||
"sources": {
|
||||
"noEmbeds": {
|
||||
"text": "Nu am putut găsi nicio incorporare, vă rog să încercați o altă sursă.",
|
||||
"title": "Nu a fost găsită nicio încorporare"
|
||||
},
|
||||
"noStream": {
|
||||
"text": "Sursa asta nu are nicio sursă de streaming pentru filmul său spectacolul.",
|
||||
"title": "Niciun stream"
|
||||
},
|
||||
"title": "Surse",
|
||||
"unknownOption": "Necunoscut"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"unsaved": "Aveți modificări nesalvate"
|
||||
}
|
||||
}
|
@ -1,6 +1,19 @@
|
||||
{
|
||||
"about": {
|
||||
"description": "movie-web - это веб-приложение, которое ищет в интернете потоки. Команда стремится к минималистичному подходу к потреблению контента.",
|
||||
"faqTitle": "Общие вопросы",
|
||||
"q1": {
|
||||
"body": "movie-web не размещает у себя никакого контента. Когда вы нажимаете на что-то для просмотра, в интернете происходит поиск выбранного медиа файла (на экране загрузки и во вкладке \"Видео источники\" вы можете увидеть, какой источник вы используете). Медиа файлы никогда не загружается на movie-web, все происходит через этот механизм поиска.",
|
||||
"title": "Откуда берётся контент?"
|
||||
},
|
||||
"q2": {
|
||||
"body": "Невозможно запросить сериал или фильм, movie-web не управляет никаким контентом. Весь контент просматривается через источники в интернете.",
|
||||
"title": "Где я могу запросить показ сериала или фильма?"
|
||||
},
|
||||
"q3": {
|
||||
"body": "Наши результаты поиска основаны на базе данных The Movie Database (TMDB) и отображаются вне зависимости от того, есть ли в наших источниках соответствующий контент.",
|
||||
"title": "В результатах поиска отображается сериал или фильм, но почему я не могу воспроизвести его?"
|
||||
},
|
||||
"title": "О movie-web"
|
||||
},
|
||||
"actions": {
|
||||
@ -12,6 +25,23 @@
|
||||
"deviceNameLabel": "Имя устройства",
|
||||
"deviceNamePlaceholder": "Личный телефон",
|
||||
"hasAccount": "У вас уже есть аккаунт? <0>Войдите здесь.</0>",
|
||||
"login": {
|
||||
"deviceLengthError": "Введите имя устройства",
|
||||
"passphraseLabel": "12-словная парольная фраза",
|
||||
"submit": "Авторизоваться",
|
||||
"title": "Войдите в свой аккаунт",
|
||||
"validationError": "Неверная или неполная парольная фраза"
|
||||
},
|
||||
"register": {
|
||||
"information": {
|
||||
"color1": "Цвет профиля один",
|
||||
"color2": "Цвет профиля два",
|
||||
"header": "Введите название устройства, выберите цвета и значок пользователя по своему усмотрению",
|
||||
"icon": "Значок пользователя",
|
||||
"next": "Далее",
|
||||
"title": "Информация об аккаунте"
|
||||
}
|
||||
},
|
||||
"trust": {
|
||||
"host": "Вы подключаетесь к <0>{{hostname}}</0> - пожалуйста, подтвердите, что вы доверяете ему, прежде чем создавать учётную запись",
|
||||
"no": "Вернуться назад",
|
||||
@ -22,16 +52,28 @@
|
||||
"register": "Создать учётную запись"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"details": "Подробности ошибки",
|
||||
"showError": "Показать сведения об ошибке"
|
||||
},
|
||||
"footer": {
|
||||
"legal": {
|
||||
"disclaimer": "Отказ от ответственности",
|
||||
"disclaimerText": "movie-web не размещает никаких файлов, а лишь предоставляет ссылки на сторонние сервисы. Юридические вопросы следует решать с владельцами файлов и поставщиками услуг. movie-web не несёт ответственности за любые медиа файлы, показанные поставщиками видео."
|
||||
},
|
||||
"links": {
|
||||
"discord": "Дискорд",
|
||||
"discord": "Discord",
|
||||
"dmca": "DMCA",
|
||||
"github": "GitHub"
|
||||
}
|
||||
},
|
||||
"tagline": "Смотрите любимые сериалы и фильмы с помощью этого приложения для потокового вещания с открытым исходным кодом."
|
||||
},
|
||||
"global": {
|
||||
"name": "movie-web",
|
||||
"pages": {
|
||||
"about": "О",
|
||||
"dmca": "DMCA",
|
||||
"pagetitle": "{{title}} - movie-web",
|
||||
"settings": "Настройки"
|
||||
}
|
||||
},
|
||||
@ -43,13 +85,25 @@
|
||||
"sectionTitle": "Продолжить просмотр"
|
||||
},
|
||||
"search": {
|
||||
"allResults": "Это все, что у нас есть!",
|
||||
"loading": "Загрузка..."
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"episodeDisplay": "С{{season}} Э{{episode}}",
|
||||
"types": {
|
||||
"movie": "Фильм",
|
||||
"show": "Сериал"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"banner": {
|
||||
"offline": "Проверьте подключение к Интернету"
|
||||
},
|
||||
"menu": {
|
||||
"about": "О нас",
|
||||
"donation": "Пожертвовать",
|
||||
"logout": "Выйти",
|
||||
"settings": "Настройки",
|
||||
"support": "Поддержка"
|
||||
}
|
||||
@ -63,16 +117,68 @@
|
||||
"downloadSubtitle": "Скачать текущие субтитры",
|
||||
"title": "Скачать"
|
||||
},
|
||||
"episodes": {
|
||||
"button": "Эпизоды",
|
||||
"loadingError": "Ошибка при загрузке сезона",
|
||||
"loadingList": "Загрузка...",
|
||||
"loadingTitle": "Загрузка..."
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "Скорость воспроизведения",
|
||||
"title": "Настройки воспроизведения"
|
||||
},
|
||||
"quality": {
|
||||
"automaticLabel": "Автоматическое качество"
|
||||
},
|
||||
"settings": {
|
||||
"downloadItem": "Скачать",
|
||||
"playbackItem": "Настройки воспроизведения",
|
||||
"qualityItem": "Качество",
|
||||
"sourceItem": "Видео источники",
|
||||
"subtitleItem": "Настройки субтитров",
|
||||
"videoSection": "Настройки видео"
|
||||
},
|
||||
"subtitles": {
|
||||
"title": "Субтитры"
|
||||
}
|
||||
},
|
||||
"playbackError": {
|
||||
"badge": "Ошибка воспроизведения",
|
||||
"errors": {
|
||||
"errorDecode": "Несмотря на то, что ранее этот медиаресурс был пригодным для использования, при попытке его декодирования произошла ошибка."
|
||||
},
|
||||
"text": "При попытке воспроизвести медиа файл произошла ошибка. Пожалуйста, попробуйте ещё раз.",
|
||||
"title": "Не удалось воспроизвести видео!"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"account": {
|
||||
"accountDetails": {
|
||||
"logoutButton": "Выйти"
|
||||
},
|
||||
"actions": {
|
||||
"delete": {
|
||||
"button": "Удалить аккаунт",
|
||||
"confirmButton": "Удалить аккаунт",
|
||||
"confirmDescription": "Вы уверены, что хотите удалить свой аккаунт? Все ваши данные будут потеряны!",
|
||||
"confirmTitle": "Вы уверены?",
|
||||
"text": "Это действие необратимо. Все данные будут удалены, и восстановить их будет невозможно.",
|
||||
"title": "Удалить аккаунт"
|
||||
},
|
||||
"title": "Действия"
|
||||
},
|
||||
"devices": {
|
||||
"deviceNameLabel": "Имя устройства",
|
||||
"removeDevice": "Удалить",
|
||||
"title": "Устройства"
|
||||
},
|
||||
"profile": {
|
||||
"finish": "Завершить редактирование",
|
||||
"firstColor": "Цвет профиля один",
|
||||
"secondColor": "Цвет профиля два",
|
||||
"title": "Редактирование изображения профиля",
|
||||
"userIcon": "Значок пользователя"
|
||||
},
|
||||
"register": {
|
||||
"title": "Синхронизировать с облаком"
|
||||
},
|
||||
@ -95,6 +201,7 @@
|
||||
"label": "Пользовательский сервер",
|
||||
"urlLabel": "URL-адрес пользовательского сервера"
|
||||
},
|
||||
"title": "Соединения",
|
||||
"workers": {
|
||||
"addButton": "Добавить новый прокси-сервер",
|
||||
"description": "Для работы приложения весь трафик маршрутизируется через прокси. Включите это, если вы хотите использовать свои собственных прокси-серверы.",
|
||||
|
@ -206,7 +206,8 @@
|
||||
"episodeBadge": "第{{episode}}集",
|
||||
"loadingError": "加载分季时发生错误",
|
||||
"loadingList": "载入中……",
|
||||
"loadingTitle": "载入中……"
|
||||
"loadingTitle": "载入中……",
|
||||
"unairedEpisodes": "本季中的一集或多集已因尚未播出而被禁用。"
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "播放速度",
|
||||
|
@ -1,53 +1,32 @@
|
||||
import classNames from "classnames";
|
||||
|
||||
import { getCountryCodeForLocale } from "@/utils/language";
|
||||
import "flag-icons/css/flag-icons.min.css";
|
||||
|
||||
export interface FlagIconProps {
|
||||
countryCode?: string;
|
||||
country?: string;
|
||||
langCode?: string;
|
||||
}
|
||||
|
||||
// Country code overrides
|
||||
const countryOverrides: Record<string, string> = {
|
||||
en: "gb",
|
||||
cs: "cz",
|
||||
el: "gr",
|
||||
fa: "ir",
|
||||
ko: "kr",
|
||||
he: "il",
|
||||
ze: "cn",
|
||||
ar: "sa",
|
||||
ja: "jp",
|
||||
bs: "ba",
|
||||
vi: "vn",
|
||||
zh: "cn",
|
||||
sl: "si",
|
||||
sv: "se",
|
||||
et: "ee",
|
||||
ne: "np",
|
||||
uk: "ua",
|
||||
hi: "in",
|
||||
};
|
||||
|
||||
export function FlagIcon(props: FlagIconProps) {
|
||||
let countryCode =
|
||||
(props.countryCode || "")?.split("-").pop()?.toLowerCase() || "";
|
||||
if (countryOverrides[countryCode])
|
||||
countryCode = countryOverrides[countryCode];
|
||||
let countryCode: string | null = props.country ?? null;
|
||||
if (props.langCode) countryCode = getCountryCodeForLocale(props.langCode);
|
||||
|
||||
if (countryCode === "tok")
|
||||
if (props.langCode === "tok")
|
||||
return (
|
||||
<div className="w-8 h-6 rounded bg-[#c8e1ed] flex justify-center items-center">
|
||||
<img src="/tokiPona.svg" className="w-7 h-5" />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (countryCode === "pirate")
|
||||
if (props.langCode === "pirate")
|
||||
return (
|
||||
<div className="w-8 h-6 rounded bg-[#2E3439] flex justify-center items-center">
|
||||
<img src="/skull.svg" className="w-4 h-4" />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (countryCode === "minion")
|
||||
if (props.langCode === "minion")
|
||||
return (
|
||||
<div className="w-8 h-6 rounded bg-[#ffff1a] flex justify-center items-center">
|
||||
<div className="w-4 h-4 border-2 border-gray-500 rounded-full bg-white flex justify-center items-center">
|
||||
@ -64,9 +43,9 @@ export function FlagIcon(props: FlagIconProps) {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
"!w-8 h-6 rounded overflow-hidden bg-cover bg-center block fi",
|
||||
"!w-8 min-w-8 h-6 rounded overflow-hidden bg-cover bg-center block fi",
|
||||
backgroundClass,
|
||||
props.countryCode ? `fi-${countryCode}` : undefined,
|
||||
countryCode ? `fi-${countryCode}` : undefined,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
@ -17,7 +17,7 @@ interface DropdownProps {
|
||||
|
||||
export function Dropdown(props: DropdownProps) {
|
||||
return (
|
||||
<div className="relative my-4 max-w-[18rem]">
|
||||
<div className="relative my-4 max-w-[25rem]">
|
||||
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}>
|
||||
{() => (
|
||||
<>
|
||||
|
@ -2,12 +2,14 @@ import classNames from "classnames";
|
||||
import FocusTrap from "focus-trap-react";
|
||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
import {
|
||||
useInternalOverlayRouter,
|
||||
useRouterAnchorUpdate,
|
||||
} from "@/hooks/useOverlayRouter";
|
||||
import { TurnstileProvider } from "@/stores/turnstile";
|
||||
|
||||
export interface OverlayProps {
|
||||
id: string;
|
||||
@ -15,6 +17,34 @@ export interface OverlayProps {
|
||||
darken?: boolean;
|
||||
}
|
||||
|
||||
function TurnstileInteractive() {
|
||||
const { t } = useTranslation();
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
// this may not rerender with different dom structure, must be exactly the same always
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"absolute w-10/12 max-w-[800px] bg-background-main p-20 rounded-lg select-none z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform",
|
||||
show ? "" : "hidden",
|
||||
)}
|
||||
>
|
||||
<div className="w-full grid lg:grid-cols-[1fr,auto] gap-12 items-center">
|
||||
<div className="text-left">
|
||||
<h2 className="text-type-emphasis font-bold text-xl mb-6">
|
||||
{t("player.turnstile.title")}
|
||||
</h2>
|
||||
<p>{t("player.turnstile.description")}</p>
|
||||
</div>
|
||||
<TurnstileProvider
|
||||
isInPopout
|
||||
onUpdateShow={(shouldShow) => setShow(shouldShow)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OverlayDisplay(props: { children: ReactNode }) {
|
||||
const router = useInternalOverlayRouter("hello world :)");
|
||||
const refRouter = useRef(router);
|
||||
@ -27,7 +57,12 @@ export function OverlayDisplay(props: { children: ReactNode }) {
|
||||
r.close();
|
||||
};
|
||||
}, []);
|
||||
return <div className="popout-location">{props.children}</div>;
|
||||
return (
|
||||
<div className="popout-location">
|
||||
<TurnstileInteractive />
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OverlayPortal(props: {
|
||||
|
@ -39,7 +39,7 @@ function ThumbnailDisplay(props: { at: number; show: boolean }) {
|
||||
|
||||
if (!props.show || !currentThumbnail) return null;
|
||||
return (
|
||||
<div className="flex flex-col items-center -translate-x-1/2">
|
||||
<div className="flex flex-col items-center -translate-x-1/2 pointer-events-none">
|
||||
<div className="w-screen flex justify-center">
|
||||
<div ref={ref}>
|
||||
<div
|
||||
|
@ -10,12 +10,14 @@ import { useCaptions } from "@/components/player/hooks/useCaptions";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { Input } from "@/components/player/internals/ContextMenu/Input";
|
||||
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
|
||||
import { getLanguageFromIETF } from "@/components/player/utils/language";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { CaptionListItem } from "@/stores/player/slices/source";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { useSubtitleStore } from "@/stores/subtitles";
|
||||
import { sortLangCodes } from "@/utils/sortLangCodes";
|
||||
import {
|
||||
getPrettyLanguageNameFromLocale,
|
||||
sortLangCodes,
|
||||
} from "@/utils/language";
|
||||
|
||||
export function CaptionOption(props: {
|
||||
countryCode?: string;
|
||||
@ -37,7 +39,7 @@ export function CaptionOption(props: {
|
||||
className="flex items-center"
|
||||
>
|
||||
<span data-code={props.countryCode} className="mr-3 inline-flex">
|
||||
<FlagIcon countryCode={props.countryCode} />
|
||||
<FlagIcon langCode={props.countryCode} />
|
||||
</span>
|
||||
<span>{props.children}</span>
|
||||
</span>
|
||||
@ -89,7 +91,8 @@ function useSubtitleList(subs: CaptionListItem[], searchQuery: string) {
|
||||
return useMemo(() => {
|
||||
const input = subs.map((t) => ({
|
||||
...t,
|
||||
languageName: getLanguageFromIETF(t.language) ?? unknownChoice,
|
||||
languageName:
|
||||
getPrettyLanguageNameFromLocale(t.language) ?? unknownChoice,
|
||||
}));
|
||||
const sorted = sortLangCodes(input.map((t) => t.language));
|
||||
let results = input.sort((a, b) => {
|
||||
|
@ -6,11 +6,11 @@ import { Toggle } from "@/components/buttons/Toggle";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { useCaptions } from "@/components/player/hooks/useCaptions";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { getLanguageFromIETF } from "@/components/player/utils/language";
|
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { qualityToString } from "@/stores/player/utils/qualities";
|
||||
import { useSubtitleStore } from "@/stores/subtitles";
|
||||
import { getPrettyLanguageNameFromLocale } from "@/utils/language";
|
||||
|
||||
export function SettingsMenu({ id }: { id: string }) {
|
||||
const { t } = useTranslation();
|
||||
@ -31,7 +31,7 @@ export function SettingsMenu({ id }: { id: string }) {
|
||||
const { toggleLastUsed } = useCaptions();
|
||||
|
||||
const selectedLanguagePretty = selectedCaptionLanguage
|
||||
? getLanguageFromIETF(selectedCaptionLanguage) ??
|
||||
? getPrettyLanguageNameFromLocale(selectedCaptionLanguage) ??
|
||||
t("player.menus.subtitles.unknownLanguage")
|
||||
: undefined;
|
||||
|
||||
|
@ -1,14 +0,0 @@
|
||||
import { getTag } from "@sozialhelden/ietf-language-tags";
|
||||
|
||||
export function getLanguageFromIETF(ietf: string): string | null {
|
||||
const tag = getTag(ietf, true);
|
||||
|
||||
const lang = tag?.language?.Description?.[0] ?? null;
|
||||
if (!lang) return null;
|
||||
|
||||
const region = tag?.region?.Description?.[0] ?? null;
|
||||
let regionText = "";
|
||||
if (region) regionText = ` (${region})`;
|
||||
|
||||
return `${lang}${regionText}`;
|
||||
}
|
@ -23,10 +23,9 @@ import { MigrationPart } from "@/pages/parts/migrations/MigrationPart";
|
||||
import { LargeTextPart } from "@/pages/parts/util/LargeTextPart";
|
||||
import App from "@/setup/App";
|
||||
import { conf } from "@/setup/config";
|
||||
import i18n from "@/setup/i18n";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { BookmarkSyncer } from "@/stores/bookmarks/BookmarkSyncer";
|
||||
import { useLanguageStore } from "@/stores/language";
|
||||
import { changeAppLanguage, useLanguageStore } from "@/stores/language";
|
||||
import { ProgressSyncer } from "@/stores/progress/ProgressSyncer";
|
||||
import { SettingsSyncer } from "@/stores/subtitles/SettingsSyncer";
|
||||
import { ThemeProvider } from "@/stores/theme";
|
||||
@ -123,7 +122,7 @@ function AuthWrapper() {
|
||||
|
||||
function MigrationRunner() {
|
||||
const status = useAsync(async () => {
|
||||
i18n.changeLanguage(useLanguageStore.getState().language);
|
||||
changeAppLanguage(useLanguageStore.getState().language);
|
||||
await initializeOldStores();
|
||||
}, []);
|
||||
const { t } = useTranslation();
|
||||
|
@ -43,7 +43,11 @@ export function MetaPart(props: MetaPartProps) {
|
||||
const { error, value, loading } = useAsync(async () => {
|
||||
const providerApiUrl = getLoadbalancedProviderApiUrl();
|
||||
if (providerApiUrl) {
|
||||
try {
|
||||
await fetchMetadata(providerApiUrl);
|
||||
} catch (err) {
|
||||
throw new Error("failed-api-metadata");
|
||||
}
|
||||
} else {
|
||||
setCachedMetadata([
|
||||
...providers.listSources(),
|
||||
@ -117,6 +121,28 @@ export function MetaPart(props: MetaPartProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (error && error.message === "failed-api-metadata") {
|
||||
return (
|
||||
<ErrorLayout>
|
||||
<ErrorContainer>
|
||||
<IconPill icon={Icons.WAND}>
|
||||
{t("player.metadata.failed.badge")}
|
||||
</IconPill>
|
||||
<Title>{t("player.metadata.api.text")}</Title>
|
||||
<Paragraph>{t("player.metadata.api.title")}</Paragraph>
|
||||
<Button
|
||||
href="/"
|
||||
theme="purple"
|
||||
padding="md:px-12 p-2.5"
|
||||
className="mt-6"
|
||||
>
|
||||
{t("player.metadata.failed.homeButton")}
|
||||
</Button>
|
||||
</ErrorContainer>
|
||||
</ErrorLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorLayout>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ProviderControls, ScrapeMedia } from "@movie-web/providers";
|
||||
import classNames from "classnames";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMountedState } from "react-use";
|
||||
import type { AsyncReturnType } from "type-fest";
|
||||
|
||||
@ -8,6 +9,8 @@ import {
|
||||
scrapePartsToProviderMetric,
|
||||
useReportProviders,
|
||||
} from "@/backend/helpers/report";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Loading } from "@/components/layout/Loading";
|
||||
import {
|
||||
ScrapeCard,
|
||||
ScrapeItem,
|
||||
@ -18,6 +21,7 @@ import {
|
||||
useListCenter,
|
||||
useScrape,
|
||||
} from "@/hooks/useProviderScrape";
|
||||
import { LargeTextPart } from "@/pages/parts/util/LargeTextPart";
|
||||
|
||||
export interface ScrapingProps {
|
||||
media: ScrapeMedia;
|
||||
@ -32,9 +36,11 @@ export function ScrapingPart(props: ScrapingProps) {
|
||||
const { report } = useReportProviders();
|
||||
const { startScraping, sourceOrder, sources, currentSource } = useScrape();
|
||||
const isMounted = useMountedState();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const [failedStartScrape, setFailedStartScrape] = useState<boolean>(false);
|
||||
const renderedOnce = useListCenter(
|
||||
containerRef,
|
||||
listRef,
|
||||
@ -72,7 +78,7 @@ export function ScrapingPart(props: ScrapingProps) {
|
||||
),
|
||||
);
|
||||
props.onGetStream?.(output);
|
||||
})();
|
||||
})().catch(() => setFailedStartScrape(true));
|
||||
}, [startScraping, props, report, isMounted]);
|
||||
|
||||
let currentProviderIndex = sourceOrder.findIndex(
|
||||
@ -81,11 +87,28 @@ export function ScrapingPart(props: ScrapingProps) {
|
||||
if (currentProviderIndex === -1)
|
||||
currentProviderIndex = sourceOrder.length - 1;
|
||||
|
||||
if (failedStartScrape)
|
||||
return (
|
||||
<LargeTextPart
|
||||
iconSlot={
|
||||
<Icon className="text-type-danger text-2xl" icon={Icons.WARNING} />
|
||||
}
|
||||
>
|
||||
{t("player.turnstile.error")}
|
||||
</LargeTextPart>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full w-full relative dir-neutral:origin-top-left flex"
|
||||
ref={containerRef}
|
||||
>
|
||||
{!sourceOrder || sourceOrder.length === 0 ? (
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-center flex flex-col justify-center z-0">
|
||||
<Loading className="mb-8" />
|
||||
<p>{t("player.turnstile.verifyingHumanity")}</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames({
|
||||
"absolute transition-[transform,opacity] opacity-0 dir-neutral:left-0":
|
||||
@ -97,7 +120,7 @@ export function ScrapingPart(props: ScrapingProps) {
|
||||
{sourceOrder.map((order) => {
|
||||
const source = sources[order.id];
|
||||
const distance = Math.abs(
|
||||
sourceOrder.findIndex((t) => t.id === order.id) -
|
||||
sourceOrder.findIndex((o) => o.id === order.id) -
|
||||
currentProviderIndex,
|
||||
);
|
||||
return (
|
||||
|
@ -4,7 +4,7 @@ import { FlagIcon } from "@/components/FlagIcon";
|
||||
import { Dropdown } from "@/components/form/Dropdown";
|
||||
import { Heading1 } from "@/components/utils/Text";
|
||||
import { appLanguageOptions } from "@/setup/i18n";
|
||||
import { sortLangCodes } from "@/utils/sortLangCodes";
|
||||
import { getLocaleInfo, sortLangCodes } from "@/utils/language";
|
||||
|
||||
export function LocalePart(props: {
|
||||
language: string;
|
||||
@ -17,11 +17,13 @@ export function LocalePart(props: {
|
||||
.sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code))
|
||||
.map((opt) => ({
|
||||
id: opt.code,
|
||||
name: `${opt.name} — ${opt.nativeName}`,
|
||||
leftIcon: <FlagIcon countryCode={opt.code} />,
|
||||
name: `${opt.name}${opt.nativeName ? ` — ${opt.nativeName}` : ""}`,
|
||||
leftIcon: <FlagIcon langCode={opt.code} />,
|
||||
}));
|
||||
|
||||
const selected = options.find((item) => item.id === props.language);
|
||||
const selected = options.find(
|
||||
(item) => item.id === getLocaleInfo(props.language)?.code,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import i18n from "i18next";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
import { locales } from "@/assets/languages";
|
||||
import { getLocaleInfo } from "@/utils/language";
|
||||
|
||||
// Languages
|
||||
const langCodes = Object.keys(locales);
|
||||
@ -10,43 +10,15 @@ const resources = Object.fromEntries(
|
||||
Object.entries(locales).map((entry) => [entry[0], { translation: entry[1] }]),
|
||||
);
|
||||
i18n.use(initReactI18next).init({
|
||||
fallbackLng: "en",
|
||||
fallbackLng: "en-US",
|
||||
resources,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
});
|
||||
|
||||
const extraLanguages: Record<
|
||||
string,
|
||||
{
|
||||
code: string;
|
||||
name: string;
|
||||
nativeName: string;
|
||||
}
|
||||
> = {
|
||||
pirate: {
|
||||
code: "pirate",
|
||||
name: "Pirate",
|
||||
nativeName: "Pirate Tongue",
|
||||
},
|
||||
minion: {
|
||||
code: "minion",
|
||||
name: "Minion",
|
||||
nativeName: "Minionese",
|
||||
},
|
||||
tok: {
|
||||
code: "tok",
|
||||
name: "Toki pona",
|
||||
nativeName: "Toki pona",
|
||||
},
|
||||
};
|
||||
|
||||
export const appLanguageOptions = langCodes.map((lang) => {
|
||||
const extraLang = extraLanguages[lang];
|
||||
if (extraLang) return extraLang;
|
||||
|
||||
const [langObj] = ISO6391.getLanguages([lang]);
|
||||
const langObj = getLocaleInfo(lang);
|
||||
if (!langObj)
|
||||
throw new Error(`Language with code ${lang} cannot be found in database`);
|
||||
return langObj;
|
||||
|
@ -11,24 +11,32 @@ interface BannerInstance {
|
||||
interface BannerStore {
|
||||
banners: BannerInstance[];
|
||||
isOnline: boolean;
|
||||
isTurnstile: boolean;
|
||||
location: string | null;
|
||||
updateHeight(id: string, height: number): void;
|
||||
showBanner(id: string): void;
|
||||
hideBanner(id: string): void;
|
||||
setLocation(loc: string | null): void;
|
||||
updateOnline(isOnline: boolean): void;
|
||||
updateTurnstile(isTurnstile: boolean): void;
|
||||
}
|
||||
|
||||
export const useBannerStore = create(
|
||||
immer<BannerStore>((set) => ({
|
||||
banners: [],
|
||||
isOnline: true,
|
||||
isTurnstile: false,
|
||||
location: null,
|
||||
updateOnline(isOnline) {
|
||||
set((s) => {
|
||||
s.isOnline = isOnline;
|
||||
});
|
||||
},
|
||||
updateTurnstile(isTurnstile) {
|
||||
set((s) => {
|
||||
s.isTurnstile = isTurnstile;
|
||||
});
|
||||
},
|
||||
setLocation(loc) {
|
||||
set((s) => {
|
||||
s.location = loc;
|
||||
|
@ -4,8 +4,8 @@ import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
|
||||
import { rtlLocales } from "@/assets/languages";
|
||||
import i18n from "@/setup/i18n";
|
||||
import { getLocaleInfo } from "@/utils/language";
|
||||
|
||||
export interface LanguageStore {
|
||||
language: string;
|
||||
@ -26,14 +26,25 @@ export const useLanguageStore = create(
|
||||
),
|
||||
);
|
||||
|
||||
export function changeAppLanguage(language: string) {
|
||||
const lang = getLocaleInfo(language);
|
||||
if (lang) i18n.changeLanguage(lang.code);
|
||||
}
|
||||
|
||||
export function isRightToLeft(language: string) {
|
||||
const lang = getLocaleInfo(language);
|
||||
if (!lang) return false;
|
||||
return lang.isRtl;
|
||||
}
|
||||
|
||||
export function LanguageProvider() {
|
||||
const language = useLanguageStore((s) => s.language);
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language);
|
||||
changeAppLanguage(language);
|
||||
}, [language]);
|
||||
|
||||
const isRtl = rtlLocales.includes(language as any);
|
||||
const isRtl = isRightToLeft(language);
|
||||
|
||||
return (
|
||||
<Helmet>
|
||||
|
@ -1,3 +1,5 @@
|
||||
import classNames from "classnames";
|
||||
import { useRef } from "react";
|
||||
import Turnstile, { BoundTurnstileObject } from "react-turnstile";
|
||||
import { create } from "zustand";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
@ -6,19 +8,31 @@ import { reportCaptchaSolve } from "@/backend/helpers/report";
|
||||
import { conf } from "@/setup/config";
|
||||
|
||||
export interface TurnstileStore {
|
||||
turnstile: BoundTurnstileObject | null;
|
||||
isInWidget: boolean;
|
||||
turnstiles: {
|
||||
controls: BoundTurnstileObject;
|
||||
isInPopout: boolean;
|
||||
id: string;
|
||||
}[];
|
||||
cbs: ((token: string | null) => void)[];
|
||||
setTurnstile(v: BoundTurnstileObject | null): void;
|
||||
setTurnstile(
|
||||
id: string,
|
||||
v: BoundTurnstileObject | null,
|
||||
isInPopout: boolean,
|
||||
): void;
|
||||
getToken(): Promise<string>;
|
||||
processToken(token: string | null): void;
|
||||
processToken(token: string | null, widgetId: string): void;
|
||||
}
|
||||
|
||||
export const useTurnstileStore = create(
|
||||
immer<TurnstileStore>((set, get) => ({
|
||||
turnstile: null,
|
||||
isInWidget: false,
|
||||
turnstiles: [],
|
||||
cbs: [],
|
||||
processToken(token) {
|
||||
processToken(token, widgetId) {
|
||||
const cbs = get().cbs;
|
||||
const turnstile = get().turnstiles.find((v) => v.id === widgetId);
|
||||
if (turnstile?.id !== widgetId) return;
|
||||
cbs.forEach((fn) => fn(token));
|
||||
set((s) => {
|
||||
s.cbs = [];
|
||||
@ -37,16 +51,26 @@ export const useTurnstileStore = create(
|
||||
});
|
||||
});
|
||||
},
|
||||
setTurnstile(v) {
|
||||
setTurnstile(id, controls, isInPopout) {
|
||||
set((s) => {
|
||||
s.turnstile = v;
|
||||
s.turnstiles = s.turnstiles.filter((v) => v.id !== id);
|
||||
if (controls) {
|
||||
s.turnstiles.push({
|
||||
controls,
|
||||
isInPopout,
|
||||
id,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
export function getTurnstile() {
|
||||
return useTurnstileStore.getState().turnstile;
|
||||
const turnstiles = useTurnstileStore.getState().turnstiles;
|
||||
const inPopout = turnstiles.find((v) => v.isInPopout);
|
||||
if (inPopout) return inPopout;
|
||||
return turnstiles[0];
|
||||
}
|
||||
|
||||
export function isTurnstileInitialized() {
|
||||
@ -55,9 +79,12 @@ export function isTurnstileInitialized() {
|
||||
|
||||
export async function getTurnstileToken() {
|
||||
const turnstile = getTurnstile();
|
||||
turnstile?.reset();
|
||||
turnstile?.execute();
|
||||
try {
|
||||
// I hate turnstile
|
||||
(window as any).turnstile.execute(
|
||||
document.querySelector(`#${turnstile.id}`),
|
||||
{},
|
||||
);
|
||||
const token = await useTurnstileStore.getState().getToken();
|
||||
reportCaptchaSolve(true);
|
||||
return token;
|
||||
@ -67,23 +94,44 @@ export async function getTurnstileToken() {
|
||||
}
|
||||
}
|
||||
|
||||
export function TurnstileProvider() {
|
||||
export function TurnstileProvider(props: {
|
||||
isInPopout?: boolean;
|
||||
onUpdateShow?: (show: boolean) => void;
|
||||
}) {
|
||||
const siteKey = conf().TURNSTILE_KEY;
|
||||
const idRef = useRef<string | null>(null);
|
||||
const setTurnstile = useTurnstileStore((s) => s.setTurnstile);
|
||||
const processToken = useTurnstileStore((s) => s.processToken);
|
||||
if (!siteKey) return null;
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
hidden: !props.isInPopout,
|
||||
})}
|
||||
>
|
||||
<Turnstile
|
||||
sitekey={siteKey}
|
||||
onLoad={(_widgetId, bound) => {
|
||||
setTurnstile(bound);
|
||||
onLoad={(widgetId, bound) => {
|
||||
idRef.current = widgetId;
|
||||
setTurnstile(widgetId, bound, !!props.isInPopout);
|
||||
}}
|
||||
onError={() => {
|
||||
processToken(null);
|
||||
const id = idRef.current;
|
||||
if (!id) return;
|
||||
processToken(null, id);
|
||||
}}
|
||||
onVerify={(token) => {
|
||||
processToken(token);
|
||||
const id = idRef.current;
|
||||
if (!id) return;
|
||||
processToken(token, id);
|
||||
props.onUpdateShow?.(false);
|
||||
}}
|
||||
onBeforeInteractive={() => {
|
||||
props.onUpdateShow?.(true);
|
||||
}}
|
||||
refreshExpired="never"
|
||||
execution="render"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
190
src/utils/language.ts
Normal file
190
src/utils/language.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import countryLanguages, { LanguageObj } from "@ladjs/country-language";
|
||||
import { getTag } from "@sozialhelden/ietf-language-tags";
|
||||
|
||||
const languageOrder = ["en", "hi", "fr", "de", "nl", "pt"];
|
||||
|
||||
// mapping of language code to country code.
|
||||
// multiple mappings can exist, since languages are spoken in multiple countries.
|
||||
// This mapping purely exists to prioritize a country over another in languages.
|
||||
// iso639_1 -> iso3166 Alpha-2
|
||||
const countryPriority: Record<string, string> = {
|
||||
en: "gb",
|
||||
nl: "nl",
|
||||
fr: "fr",
|
||||
de: "de",
|
||||
pt: "pt",
|
||||
ar: "sa",
|
||||
es: "es",
|
||||
zh: "cn",
|
||||
ko: "kr",
|
||||
ta: "lk",
|
||||
gl: "es",
|
||||
};
|
||||
|
||||
// list of iso639_1 Alpha-2 codes used as default languages
|
||||
const defaultLanguageCodes: string[] = [
|
||||
"en-US",
|
||||
"cs-CZ",
|
||||
"de-DE",
|
||||
"fr-FR",
|
||||
"pt-BR",
|
||||
"it-IT",
|
||||
"nl-NL",
|
||||
"pl-PL",
|
||||
"tr-TR",
|
||||
"vi-VN",
|
||||
"zh-CN",
|
||||
"he-IL",
|
||||
"sv-SE",
|
||||
"lv-LV",
|
||||
"th-TH",
|
||||
"ne-NP",
|
||||
"ar-SA",
|
||||
"es-ES",
|
||||
"et-EE",
|
||||
"bg-BG",
|
||||
"bn-BD",
|
||||
"el-GR",
|
||||
"fa-IR",
|
||||
"gu-IN",
|
||||
"id-ID",
|
||||
"ja-JP",
|
||||
"ko-KR",
|
||||
"sl-SI",
|
||||
"ta-LK",
|
||||
"ru-RU",
|
||||
"gl-ES",
|
||||
];
|
||||
|
||||
export interface LocaleInfo {
|
||||
name: string;
|
||||
nativeName?: string;
|
||||
code: string;
|
||||
isRtl?: boolean;
|
||||
}
|
||||
|
||||
const extraLanguages: Record<string, LocaleInfo> = {
|
||||
pirate: {
|
||||
code: "pirate",
|
||||
name: "Pirate",
|
||||
nativeName: "Pirate Tongue",
|
||||
},
|
||||
minion: {
|
||||
code: "minion",
|
||||
name: "Minion",
|
||||
nativeName: "Minionese",
|
||||
},
|
||||
tok: {
|
||||
code: "tok",
|
||||
name: "Toki pona",
|
||||
nativeName: "Toki pona",
|
||||
},
|
||||
};
|
||||
|
||||
function populateLanguageCode(language: string): string {
|
||||
if (language.includes("-")) return language;
|
||||
if (language.length !== 2) return language;
|
||||
return (
|
||||
defaultLanguageCodes.find((v) => v.startsWith(`${language}-`)) ?? language
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param locale idk what kinda code this takes, anytihhng in ietf format I guess
|
||||
* @returns pretty format for language, null if it no info can be found for language
|
||||
*/
|
||||
export function getPrettyLanguageNameFromLocale(locale: string): string | null {
|
||||
const tag = getTag(populateLanguageCode(locale), true);
|
||||
const lang = tag?.language?.Description?.[0] ?? null;
|
||||
if (!lang) return null;
|
||||
|
||||
const region = tag?.region?.Description?.[0] ?? null;
|
||||
let regionText = "";
|
||||
if (region) regionText = ` (${region})`;
|
||||
|
||||
return `${lang}${regionText}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort locale codes by occurance, rest on alphabetical order
|
||||
* @param langCodes list language codes to sort
|
||||
* @returns sorted version of inputted list
|
||||
*/
|
||||
export function sortLangCodes(langCodes: string[]) {
|
||||
const languagesOrder = [...languageOrder].reverse(); // Reverse is neccesary, not sure why
|
||||
|
||||
const results = langCodes.sort((a, b) => {
|
||||
const langOrderA = languagesOrder.findIndex(
|
||||
(v) => a.startsWith(`${v}-`) || a === v,
|
||||
);
|
||||
const langOrderB = languagesOrder.findIndex(
|
||||
(v) => b.startsWith(`${v}-`) || b === v,
|
||||
);
|
||||
if (langOrderA !== -1 || langOrderB !== -1) return langOrderB - langOrderA;
|
||||
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country code for locale
|
||||
* @param locale input locale
|
||||
* @returns country code or null
|
||||
*/
|
||||
export function getCountryCodeForLocale(locale: string): string | null {
|
||||
let output: LanguageObj | null = null as any as LanguageObj;
|
||||
const tag = getTag(locale, true);
|
||||
|
||||
if (!tag?.language?.Subtag) return null;
|
||||
// this function isnt async, so its garuanteed to work like this
|
||||
countryLanguages.getLanguage(tag.language.Subtag, (_err, lang) => {
|
||||
if (lang) output = lang;
|
||||
});
|
||||
if (!output) return null;
|
||||
const priority = countryPriority[output.iso639_1.toLowerCase()];
|
||||
if (output.countries.length === 0) {
|
||||
return priority ?? null;
|
||||
}
|
||||
if (priority) {
|
||||
const priotizedCountry = output.countries.find(
|
||||
(v) => v.code_2.toLowerCase() === priority,
|
||||
);
|
||||
if (priotizedCountry) return priotizedCountry.code_2.toLowerCase();
|
||||
}
|
||||
return output.countries[0].code_2.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information for a specific local
|
||||
* @param locale local code
|
||||
* @returns locale object
|
||||
*/
|
||||
export function getLocaleInfo(locale: string): LocaleInfo | null {
|
||||
const realLocale = populateLanguageCode(locale);
|
||||
const extraLang = extraLanguages[realLocale];
|
||||
if (extraLang) return extraLang;
|
||||
|
||||
const tag = getTag(realLocale, true);
|
||||
if (!tag?.language?.Subtag) return null;
|
||||
|
||||
let output: LanguageObj | null = null as any as LanguageObj;
|
||||
// this function isnt async, so its garuanteed to work like this
|
||||
countryLanguages.getLanguage(tag.language.Subtag, (_err, lang) => {
|
||||
if (lang) output = lang;
|
||||
});
|
||||
if (!output) return null;
|
||||
|
||||
const extras = [];
|
||||
if (tag.region?.Description) extras.push(tag.region.Description[0]);
|
||||
if (tag.script?.Description) extras.push(tag.script.Description[0]);
|
||||
const extraStringified = extras.map((v) => `(${v})`).join(" ");
|
||||
|
||||
return {
|
||||
code: tag.parts.langtag ?? realLocale,
|
||||
isRtl: output.direction === "RTL",
|
||||
name: output.name[0] + (extraStringified ? ` ${extraStringified}` : ""),
|
||||
nativeName: output.nativeName[0] ?? undefined,
|
||||
};
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
export function sortLangCodes(langCodes: string[]) {
|
||||
const languagesOrder = ["en", "hi", "fr", "de", "nl", "pt"].reverse(); // Reverse is neccesary, not sure why
|
||||
|
||||
const results = langCodes.sort((a, b) => {
|
||||
if (languagesOrder.indexOf(b) !== -1 || languagesOrder.indexOf(a) !== -1)
|
||||
return languagesOrder.indexOf(b) - languagesOrder.indexOf(a);
|
||||
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
@ -25,7 +25,7 @@ export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd());
|
||||
return {
|
||||
plugins: [
|
||||
million.vite({ auto: true }),
|
||||
million.vite({ auto: true, mute: true }),
|
||||
handlebars({
|
||||
vars: {
|
||||
opensearchEnabled: env.VITE_OPENSEARCH_ENABLED === "true",
|
||||
@ -124,8 +124,8 @@ export default defineConfig(({ mode }) => {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id: string) {
|
||||
if (id.includes("@sozialhelden+ietf-language-tags")) {
|
||||
return "ietf-language-tags";
|
||||
if (id.includes("@sozialhelden+ietf-language-tags") || id.includes("country-language")) {
|
||||
return "language-db";
|
||||
}
|
||||
if (id.includes("hls.js")) {
|
||||
return "hls";
|
||||
|
Loading…
x
Reference in New Issue
Block a user