mirror of
https://github.com/movie-web/movie-web.git
synced 2025-01-24 02:31:14 +01:00
commit
89f97fe849
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "movie-web",
|
"name": "movie-web",
|
||||||
"version": "4.2.2",
|
"version": "4.2.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://movie-web.app",
|
"homepage": "https://movie-web.app",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -28,6 +28,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/auto-animate": "^0.8.1",
|
"@formkit/auto-animate": "^0.8.1",
|
||||||
"@headlessui/react": "^1.7.17",
|
"@headlessui/react": "^1.7.17",
|
||||||
|
"@ladjs/country-language": "^1.0.3",
|
||||||
"@movie-web/providers": "^2.0.2",
|
"@movie-web/providers": "^2.0.2",
|
||||||
"@noble/hashes": "^1.3.3",
|
"@noble/hashes": "^1.3.3",
|
||||||
"@react-spring/web": "^9.7.3",
|
"@react-spring/web": "^9.7.3",
|
||||||
@ -44,7 +45,6 @@
|
|||||||
"hls.js": "^1.4.14",
|
"hls.js": "^1.4.14",
|
||||||
"i18next": "^23.7.11",
|
"i18next": "^23.7.11",
|
||||||
"immer": "^10.0.3",
|
"immer": "^10.0.3",
|
||||||
"iso-639-1": "^3.1.0",
|
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"million": "^2.6.4",
|
"million": "^2.6.4",
|
||||||
|
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@ -17,6 +17,9 @@ dependencies:
|
|||||||
'@headlessui/react':
|
'@headlessui/react':
|
||||||
specifier: ^1.7.17
|
specifier: ^1.7.17
|
||||||
version: 1.7.17(react-dom@18.2.0)(react@18.2.0)
|
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':
|
'@movie-web/providers':
|
||||||
specifier: ^2.0.2
|
specifier: ^2.0.2
|
||||||
version: 2.0.3
|
version: 2.0.3
|
||||||
@ -65,9 +68,6 @@ dependencies:
|
|||||||
immer:
|
immer:
|
||||||
specifier: ^10.0.3
|
specifier: ^10.0.3
|
||||||
version: 10.0.3
|
version: 10.0.3
|
||||||
iso-639-1:
|
|
||||||
specifier: ^3.1.0
|
|
||||||
version: 3.1.0
|
|
||||||
jwt-decode:
|
jwt-decode:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
@ -1912,6 +1912,11 @@ packages:
|
|||||||
'@jridgewell/resolve-uri': 3.1.1
|
'@jridgewell/resolve-uri': 3.1.1
|
||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@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:
|
/@movie-web/providers@2.0.3:
|
||||||
resolution: {integrity: sha512-6UNk5EebiNjGoFTuyHuu0eZZTreRYv0cdsn52CVYjm6CXG63w4dMbx8ybxcvMUrDF3o8bWlqnlovG142sdOmNw==}
|
resolution: {integrity: sha512-6UNk5EebiNjGoFTuyHuu0eZZTreRYv0cdsn52CVYjm6CXG63w4dMbx8ybxcvMUrDF3o8bWlqnlovG142sdOmNw==}
|
||||||
dependencies:
|
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 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 cs from "@/assets/locales/cs.json";
|
||||||
import de from "@/assets/locales/de.json";
|
import de from "@/assets/locales/de.json";
|
||||||
|
import el from "@/assets/locales/el.json";
|
||||||
import en from "@/assets/locales/en.json";
|
import en from "@/assets/locales/en.json";
|
||||||
import es from "@/assets/locales/es.json";
|
import es from "@/assets/locales/es.json";
|
||||||
import et from "@/assets/locales/et.json";
|
import et from "@/assets/locales/et.json";
|
||||||
|
import fa from "@/assets/locales/fa.json";
|
||||||
import fr from "@/assets/locales/fr.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 he from "@/assets/locales/he.json";
|
||||||
import hi from "@/assets/locales/hi.json";
|
import hi from "@/assets/locales/hi.json";
|
||||||
|
import id from "@/assets/locales/id.json";
|
||||||
import it from "@/assets/locales/it.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 lv from "@/assets/locales/lv.json";
|
||||||
import minion from "@/assets/locales/minion.json";
|
import minion from "@/assets/locales/minion.json";
|
||||||
import ne from "@/assets/locales/ne.json";
|
import ne from "@/assets/locales/ne.json";
|
||||||
import nl from "@/assets/locales/nl.json";
|
import nl from "@/assets/locales/nl.json";
|
||||||
|
import pa from "@/assets/locales/pa.json";
|
||||||
import pirate from "@/assets/locales/pirate.json";
|
import pirate from "@/assets/locales/pirate.json";
|
||||||
import pl from "@/assets/locales/pl.json";
|
import pl from "@/assets/locales/pl.json";
|
||||||
import ptbr from "@/assets/locales/pt-BR.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 sv from "@/assets/locales/sv.json";
|
||||||
|
import ta from "@/assets/locales/ta.json";
|
||||||
import th from "@/assets/locales/th.json";
|
import th from "@/assets/locales/th.json";
|
||||||
import tok from "@/assets/locales/tok.json";
|
import tok from "@/assets/locales/tok.json";
|
||||||
import tr from "@/assets/locales/tr.json";
|
import tr from "@/assets/locales/tr.json";
|
||||||
import uk from "@/assets/locales/uk.json";
|
import uk from "@/assets/locales/uk.json";
|
||||||
import vi from "@/assets/locales/vi.json";
|
import vi from "@/assets/locales/vi.json";
|
||||||
|
import zhhant from "@/assets/locales/zh-Hant.json";
|
||||||
import zh from "@/assets/locales/zh.json";
|
import zh from "@/assets/locales/zh.json";
|
||||||
|
|
||||||
export const locales = {
|
export const locales = {
|
||||||
@ -46,9 +61,22 @@ export const locales = {
|
|||||||
et,
|
et,
|
||||||
tok,
|
tok,
|
||||||
hi,
|
hi,
|
||||||
pt: ptbr,
|
"pt-BR": ptbr,
|
||||||
uk,
|
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 type Locales = keyof typeof locales;
|
||||||
|
|
||||||
export const rtlLocales: Locales[] = ["he", "ar"];
|
|
||||||
|
@ -206,7 +206,8 @@
|
|||||||
"episodeBadge": "E{{episode}}",
|
"episodeBadge": "E{{episode}}",
|
||||||
"loadingError": "خطأ في تحميل الموسم",
|
"loadingError": "خطأ في تحميل الموسم",
|
||||||
"loadingList": "تحميل...",
|
"loadingList": "تحميل...",
|
||||||
"loadingTitle": "تحميل..."
|
"loadingTitle": "تحميل...",
|
||||||
|
"unairedEpisodes": "تم تعطيل حلقة واحدة أو أكثر من هذا الموسم لأنه لم يتم بثها بعد."
|
||||||
},
|
},
|
||||||
"playback": {
|
"playback": {
|
||||||
"speedLabel": "سرعة التشغيل",
|
"speedLabel": "سرعة التشغيل",
|
||||||
|
@ -206,7 +206,8 @@
|
|||||||
"episodeBadge": "E{{episode}}",
|
"episodeBadge": "E{{episode}}",
|
||||||
"loadingError": "Chyba při načítání sezóny",
|
"loadingError": "Chyba při načítání sezóny",
|
||||||
"loadingList": "Načítání...",
|
"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": {
|
"playback": {
|
||||||
"speedLabel": "Rychlost přehrávání",
|
"speedLabel": "Rychlost přehrávání",
|
||||||
|
@ -206,7 +206,8 @@
|
|||||||
"episodeBadge": "E{{episode}}",
|
"episodeBadge": "E{{episode}}",
|
||||||
"loadingError": "Fehler beim Laden der Sitzung",
|
"loadingError": "Fehler beim Laden der Sitzung",
|
||||||
"loadingList": "Wird geladen...",
|
"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": {
|
"playback": {
|
||||||
"speedLabel": "Wiedergabegeschwindigkeit",
|
"speedLabel": "Wiedergabegeschwindigkeit",
|
||||||
|
@ -165,6 +165,12 @@
|
|||||||
"close": "Close"
|
"close": "Close"
|
||||||
},
|
},
|
||||||
"player": {
|
"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": {
|
"back": {
|
||||||
"default": "Back to home",
|
"default": "Back to home",
|
||||||
"short": "Back"
|
"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.",
|
"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"
|
"title": "Failed to load metadata"
|
||||||
},
|
},
|
||||||
|
"api": {
|
||||||
|
"text": "Could not load API metadata, please check your internet connection.",
|
||||||
|
"title": "Failed to load API metadata"
|
||||||
|
},
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"badge": "Not found",
|
"badge": "Not found",
|
||||||
"homeButton": "Back to home",
|
"homeButton": "Back to home",
|
||||||
|
@ -206,7 +206,8 @@
|
|||||||
"episodeBadge": "E{{episode}}",
|
"episodeBadge": "E{{episode}}",
|
||||||
"loadingError": "Error al cargar la temporada",
|
"loadingError": "Error al cargar la temporada",
|
||||||
"loadingList": "Cargando...",
|
"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": {
|
"playback": {
|
||||||
"speedLabel": "Velocidad de reproducción",
|
"speedLabel": "Velocidad de reproducción",
|
||||||
@ -258,6 +259,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"metadata": {
|
"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": {
|
"failed": {
|
||||||
"badge": "Error",
|
"badge": "Error",
|
||||||
"homeButton": "Ir al inicio",
|
"homeButton": "Ir al inicio",
|
||||||
@ -307,6 +312,12 @@
|
|||||||
"remaining": "{{timeLeft}} restante • Finaliza a las {{timeFinished, datetime}}",
|
"remaining": "{{timeLeft}} restante • Finaliza a las {{timeFinished, datetime}}",
|
||||||
"shortRegular": "{{timeWatched}}",
|
"shortRegular": "{{timeWatched}}",
|
||||||
"shortRemaining": "-{{timeLeft}}"
|
"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": {
|
"screens": {
|
||||||
|
@ -206,7 +206,8 @@
|
|||||||
"episodeBadge": "E{{episode}}",
|
"episodeBadge": "E{{episode}}",
|
||||||
"loadingError": "Hooaja laadimine ebaōnnestus",
|
"loadingError": "Hooaja laadimine ebaōnnestus",
|
||||||
"loadingList": "Laadimine...",
|
"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": {
|
"playback": {
|
||||||
"speedLabel": "Taasesituse kiirus",
|
"speedLabel": "Taasesituse kiirus",
|
||||||
@ -258,6 +259,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
"api": {
|
||||||
|
"text": "API metaandmete laadimine ebaõnnestus, palun kontrollige oma internetiühendust.",
|
||||||
|
"title": "API metaandmete laadimine ebaõnnestus"
|
||||||
|
},
|
||||||
"failed": {
|
"failed": {
|
||||||
"badge": "Ebaōnnestus",
|
"badge": "Ebaōnnestus",
|
||||||
"homeButton": "Mine koju",
|
"homeButton": "Mine koju",
|
||||||
@ -307,6 +312,12 @@
|
|||||||
"remaining": "{{timeLeft}} alles • Lõppeb {{timeFinished, datetime}}",
|
"remaining": "{{timeLeft}} alles • Lõppeb {{timeFinished, datetime}}",
|
||||||
"shortRegular": "{{timeWatched}}",
|
"shortRegular": "{{timeWatched}}",
|
||||||
"shortRemaining": "-{{timeLeft}}"
|
"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": {
|
"screens": {
|
||||||
|
@ -206,7 +206,8 @@
|
|||||||
"episodeBadge": "E{{episode}}",
|
"episodeBadge": "E{{episode}}",
|
||||||
"loadingError": "Erreur de chargement de la saison",
|
"loadingError": "Erreur de chargement de la saison",
|
||||||
"loadingList": "Chargement...",
|
"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": {
|
"playback": {
|
||||||
"speedLabel": "Vitesse de lecture",
|
"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}}",
|
"episodeBadge": "એપિસોડ{{episode}}",
|
||||||
"loadingError": "સીઝન લોડ કરવામાં ભૂલ",
|
"loadingError": "સીઝન લોડ કરવામાં ભૂલ",
|
||||||
"loadingList": "લોડ થાય છે...",
|
"loadingList": "લોડ થાય છે...",
|
||||||
"loadingTitle": "લોડ થાય છે..."
|
"loadingTitle": "લોડ થાય છે...",
|
||||||
|
"unairedEpisodes": "આ સિઝનમાં એક અથવા વધુ એપિસોડ અક્ષમ કરવામાં આવ્યા છે કારણ કે તે હજુ સુધી પ્રસારિત થયા નથી."
|
||||||
},
|
},
|
||||||
"playback": {
|
"playback": {
|
||||||
"speedLabel": "પ્લેબેક ઝડપ",
|
"speedLabel": "પ્લેબેક ઝડપ",
|
||||||
|
@ -155,7 +155,7 @@
|
|||||||
"donation": "दान करें",
|
"donation": "दान करें",
|
||||||
"logout": "लॉग आउट",
|
"logout": "लॉग आउट",
|
||||||
"register": "क्लाउड से सिंक करें",
|
"register": "क्लाउड से सिंक करें",
|
||||||
"settings": "चेड चाड करे",
|
"settings": "सेटिंग्स",
|
||||||
"support": "सहायता"
|
"support": "सहायता"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -206,7 +206,8 @@
|
|||||||
"episodeBadge": "E{{episode}}",
|
"episodeBadge": "E{{episode}}",
|
||||||
"loadingError": "सीज़न लोड करने में त्रुटि",
|
"loadingError": "सीज़न लोड करने में त्रुटि",
|
||||||
"loadingList": "लोड हो रहा है..।",
|
"loadingList": "लोड हो रहा है..।",
|
||||||
"loadingTitle": "लोड हो रहा है..।"
|
"loadingTitle": "लोड हो रहा है..।",
|
||||||
|
"unairedEpisodes": "इस सीज़न में एक या अधिक एपिसोड अक्षम कर दिए गए हैं क्योंकि वे अभी तक प्रसारित नहीं हुए हैं।"
|
||||||
},
|
},
|
||||||
"playback": {
|
"playback": {
|
||||||
"speedLabel": "प्लेबैक गति",
|
"speedLabel": "प्लेबैक गति",
|
||||||
|
@ -206,7 +206,8 @@
|
|||||||
"episodeBadge": "A{{episode}}",
|
"episodeBadge": "A{{episode}}",
|
||||||
"loadingError": "Er ging iets mis bij het laden van dit seizoen",
|
"loadingError": "Er ging iets mis bij het laden van dit seizoen",
|
||||||
"loadingList": "Aan het laden...",
|
"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": {
|
"playback": {
|
||||||
"speedLabel": "Afspeelsnelheid",
|
"speedLabel": "Afspeelsnelheid",
|
||||||
@ -361,8 +362,66 @@
|
|||||||
},
|
},
|
||||||
"register": {
|
"register": {
|
||||||
"cta": "Aan de slag",
|
"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": {
|
"about": {
|
||||||
|
"description": "movie-web - это веб-приложение, которое ищет в интернете потоки. Команда стремится к минималистичному подходу к потреблению контента.",
|
||||||
"faqTitle": "Общие вопросы",
|
"faqTitle": "Общие вопросы",
|
||||||
|
"q1": {
|
||||||
|
"body": "movie-web не размещает у себя никакого контента. Когда вы нажимаете на что-то для просмотра, в интернете происходит поиск выбранного медиа файла (на экране загрузки и во вкладке \"Видео источники\" вы можете увидеть, какой источник вы используете). Медиа файлы никогда не загружается на movie-web, все происходит через этот механизм поиска.",
|
||||||
|
"title": "Откуда берётся контент?"
|
||||||
|
},
|
||||||
|
"q2": {
|
||||||
|
"body": "Невозможно запросить сериал или фильм, movie-web не управляет никаким контентом. Весь контент просматривается через источники в интернете.",
|
||||||
|
"title": "Где я могу запросить показ сериала или фильма?"
|
||||||
|
},
|
||||||
|
"q3": {
|
||||||
|
"body": "Наши результаты поиска основаны на базе данных The Movie Database (TMDB) и отображаются вне зависимости от того, есть ли в наших источниках соответствующий контент.",
|
||||||
|
"title": "В результатах поиска отображается сериал или фильм, но почему я не могу воспроизвести его?"
|
||||||
|
},
|
||||||
"title": "О movie-web"
|
"title": "О movie-web"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
@ -12,6 +25,23 @@
|
|||||||
"deviceNameLabel": "Имя устройства",
|
"deviceNameLabel": "Имя устройства",
|
||||||
"deviceNamePlaceholder": "Личный телефон",
|
"deviceNamePlaceholder": "Личный телефон",
|
||||||
"hasAccount": "У вас уже есть аккаунт? <0>Войдите здесь.</0>",
|
"hasAccount": "У вас уже есть аккаунт? <0>Войдите здесь.</0>",
|
||||||
|
"login": {
|
||||||
|
"deviceLengthError": "Введите имя устройства",
|
||||||
|
"passphraseLabel": "12-словная парольная фраза",
|
||||||
|
"submit": "Авторизоваться",
|
||||||
|
"title": "Войдите в свой аккаунт",
|
||||||
|
"validationError": "Неверная или неполная парольная фраза"
|
||||||
|
},
|
||||||
|
"register": {
|
||||||
|
"information": {
|
||||||
|
"color1": "Цвет профиля один",
|
||||||
|
"color2": "Цвет профиля два",
|
||||||
|
"header": "Введите название устройства, выберите цвета и значок пользователя по своему усмотрению",
|
||||||
|
"icon": "Значок пользователя",
|
||||||
|
"next": "Далее",
|
||||||
|
"title": "Информация об аккаунте"
|
||||||
|
}
|
||||||
|
},
|
||||||
"trust": {
|
"trust": {
|
||||||
"host": "Вы подключаетесь к <0>{{hostname}}</0> - пожалуйста, подтвердите, что вы доверяете ему, прежде чем создавать учётную запись",
|
"host": "Вы подключаетесь к <0>{{hostname}}</0> - пожалуйста, подтвердите, что вы доверяете ему, прежде чем создавать учётную запись",
|
||||||
"no": "Вернуться назад",
|
"no": "Вернуться назад",
|
||||||
@ -22,16 +52,28 @@
|
|||||||
"register": "Создать учётную запись"
|
"register": "Создать учётную запись"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"details": "Подробности ошибки",
|
||||||
|
"showError": "Показать сведения об ошибке"
|
||||||
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
|
"legal": {
|
||||||
|
"disclaimer": "Отказ от ответственности",
|
||||||
|
"disclaimerText": "movie-web не размещает никаких файлов, а лишь предоставляет ссылки на сторонние сервисы. Юридические вопросы следует решать с владельцами файлов и поставщиками услуг. movie-web не несёт ответственности за любые медиа файлы, показанные поставщиками видео."
|
||||||
|
},
|
||||||
"links": {
|
"links": {
|
||||||
"discord": "Дискорд",
|
"discord": "Discord",
|
||||||
"dmca": "DMCA",
|
"dmca": "DMCA",
|
||||||
"github": "GitHub"
|
"github": "GitHub"
|
||||||
}
|
},
|
||||||
|
"tagline": "Смотрите любимые сериалы и фильмы с помощью этого приложения для потокового вещания с открытым исходным кодом."
|
||||||
},
|
},
|
||||||
"global": {
|
"global": {
|
||||||
|
"name": "movie-web",
|
||||||
"pages": {
|
"pages": {
|
||||||
|
"about": "О",
|
||||||
"dmca": "DMCA",
|
"dmca": "DMCA",
|
||||||
|
"pagetitle": "{{title}} - movie-web",
|
||||||
"settings": "Настройки"
|
"settings": "Настройки"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -43,13 +85,25 @@
|
|||||||
"sectionTitle": "Продолжить просмотр"
|
"sectionTitle": "Продолжить просмотр"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
"allResults": "Это все, что у нас есть!",
|
||||||
"loading": "Загрузка..."
|
"loading": "Загрузка..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"media": {
|
||||||
|
"episodeDisplay": "С{{season}} Э{{episode}}",
|
||||||
|
"types": {
|
||||||
|
"movie": "Фильм",
|
||||||
|
"show": "Сериал"
|
||||||
|
}
|
||||||
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
|
"banner": {
|
||||||
|
"offline": "Проверьте подключение к Интернету"
|
||||||
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"about": "О нас",
|
"about": "О нас",
|
||||||
"donation": "Пожертвовать",
|
"donation": "Пожертвовать",
|
||||||
|
"logout": "Выйти",
|
||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
"support": "Поддержка"
|
"support": "Поддержка"
|
||||||
}
|
}
|
||||||
@ -63,16 +117,68 @@
|
|||||||
"downloadSubtitle": "Скачать текущие субтитры",
|
"downloadSubtitle": "Скачать текущие субтитры",
|
||||||
"title": "Скачать"
|
"title": "Скачать"
|
||||||
},
|
},
|
||||||
|
"episodes": {
|
||||||
|
"button": "Эпизоды",
|
||||||
|
"loadingError": "Ошибка при загрузке сезона",
|
||||||
|
"loadingList": "Загрузка...",
|
||||||
|
"loadingTitle": "Загрузка..."
|
||||||
|
},
|
||||||
|
"playback": {
|
||||||
|
"speedLabel": "Скорость воспроизведения",
|
||||||
|
"title": "Настройки воспроизведения"
|
||||||
|
},
|
||||||
"quality": {
|
"quality": {
|
||||||
"automaticLabel": "Автоматическое качество"
|
"automaticLabel": "Автоматическое качество"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"downloadItem": "Скачать",
|
||||||
|
"playbackItem": "Настройки воспроизведения",
|
||||||
|
"qualityItem": "Качество",
|
||||||
|
"sourceItem": "Видео источники",
|
||||||
|
"subtitleItem": "Настройки субтитров",
|
||||||
|
"videoSection": "Настройки видео"
|
||||||
|
},
|
||||||
"subtitles": {
|
"subtitles": {
|
||||||
"title": "Субтитры"
|
"title": "Субтитры"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"playbackError": {
|
||||||
|
"badge": "Ошибка воспроизведения",
|
||||||
|
"errors": {
|
||||||
|
"errorDecode": "Несмотря на то, что ранее этот медиаресурс был пригодным для использования, при попытке его декодирования произошла ошибка."
|
||||||
|
},
|
||||||
|
"text": "При попытке воспроизвести медиа файл произошла ошибка. Пожалуйста, попробуйте ещё раз.",
|
||||||
|
"title": "Не удалось воспроизвести видео!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"account": {
|
"account": {
|
||||||
|
"accountDetails": {
|
||||||
|
"logoutButton": "Выйти"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"delete": {
|
||||||
|
"button": "Удалить аккаунт",
|
||||||
|
"confirmButton": "Удалить аккаунт",
|
||||||
|
"confirmDescription": "Вы уверены, что хотите удалить свой аккаунт? Все ваши данные будут потеряны!",
|
||||||
|
"confirmTitle": "Вы уверены?",
|
||||||
|
"text": "Это действие необратимо. Все данные будут удалены, и восстановить их будет невозможно.",
|
||||||
|
"title": "Удалить аккаунт"
|
||||||
|
},
|
||||||
|
"title": "Действия"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"deviceNameLabel": "Имя устройства",
|
||||||
|
"removeDevice": "Удалить",
|
||||||
|
"title": "Устройства"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"finish": "Завершить редактирование",
|
||||||
|
"firstColor": "Цвет профиля один",
|
||||||
|
"secondColor": "Цвет профиля два",
|
||||||
|
"title": "Редактирование изображения профиля",
|
||||||
|
"userIcon": "Значок пользователя"
|
||||||
|
},
|
||||||
"register": {
|
"register": {
|
||||||
"title": "Синхронизировать с облаком"
|
"title": "Синхронизировать с облаком"
|
||||||
},
|
},
|
||||||
@ -95,6 +201,7 @@
|
|||||||
"label": "Пользовательский сервер",
|
"label": "Пользовательский сервер",
|
||||||
"urlLabel": "URL-адрес пользовательского сервера"
|
"urlLabel": "URL-адрес пользовательского сервера"
|
||||||
},
|
},
|
||||||
|
"title": "Соединения",
|
||||||
"workers": {
|
"workers": {
|
||||||
"addButton": "Добавить новый прокси-сервер",
|
"addButton": "Добавить новый прокси-сервер",
|
||||||
"description": "Для работы приложения весь трафик маршрутизируется через прокси. Включите это, если вы хотите использовать свои собственных прокси-серверы.",
|
"description": "Для работы приложения весь трафик маршрутизируется через прокси. Включите это, если вы хотите использовать свои собственных прокси-серверы.",
|
||||||
|
@ -206,7 +206,8 @@
|
|||||||
"episodeBadge": "第{{episode}}集",
|
"episodeBadge": "第{{episode}}集",
|
||||||
"loadingError": "加载分季时发生错误",
|
"loadingError": "加载分季时发生错误",
|
||||||
"loadingList": "载入中……",
|
"loadingList": "载入中……",
|
||||||
"loadingTitle": "载入中……"
|
"loadingTitle": "载入中……",
|
||||||
|
"unairedEpisodes": "本季中的一集或多集已因尚未播出而被禁用。"
|
||||||
},
|
},
|
||||||
"playback": {
|
"playback": {
|
||||||
"speedLabel": "播放速度",
|
"speedLabel": "播放速度",
|
||||||
|
@ -1,53 +1,32 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import { getCountryCodeForLocale } from "@/utils/language";
|
||||||
import "flag-icons/css/flag-icons.min.css";
|
import "flag-icons/css/flag-icons.min.css";
|
||||||
|
|
||||||
export interface FlagIconProps {
|
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) {
|
export function FlagIcon(props: FlagIconProps) {
|
||||||
let countryCode =
|
let countryCode: string | null = props.country ?? null;
|
||||||
(props.countryCode || "")?.split("-").pop()?.toLowerCase() || "";
|
if (props.langCode) countryCode = getCountryCodeForLocale(props.langCode);
|
||||||
if (countryOverrides[countryCode])
|
|
||||||
countryCode = countryOverrides[countryCode];
|
|
||||||
|
|
||||||
if (countryCode === "tok")
|
if (props.langCode === "tok")
|
||||||
return (
|
return (
|
||||||
<div className="w-8 h-6 rounded bg-[#c8e1ed] flex justify-center items-center">
|
<div className="w-8 h-6 rounded bg-[#c8e1ed] flex justify-center items-center">
|
||||||
<img src="/tokiPona.svg" className="w-7 h-5" />
|
<img src="/tokiPona.svg" className="w-7 h-5" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (countryCode === "pirate")
|
if (props.langCode === "pirate")
|
||||||
return (
|
return (
|
||||||
<div className="w-8 h-6 rounded bg-[#2E3439] flex justify-center items-center">
|
<div className="w-8 h-6 rounded bg-[#2E3439] flex justify-center items-center">
|
||||||
<img src="/skull.svg" className="w-4 h-4" />
|
<img src="/skull.svg" className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (countryCode === "minion")
|
if (props.langCode === "minion")
|
||||||
return (
|
return (
|
||||||
<div className="w-8 h-6 rounded bg-[#ffff1a] flex justify-center items-center">
|
<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">
|
<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 (
|
return (
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
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,
|
backgroundClass,
|
||||||
props.countryCode ? `fi-${countryCode}` : undefined,
|
countryCode ? `fi-${countryCode}` : undefined,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -17,7 +17,7 @@ interface DropdownProps {
|
|||||||
|
|
||||||
export function Dropdown(props: DropdownProps) {
|
export function Dropdown(props: DropdownProps) {
|
||||||
return (
|
return (
|
||||||
<div className="relative my-4 max-w-[18rem]">
|
<div className="relative my-4 max-w-[25rem]">
|
||||||
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}>
|
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}>
|
||||||
{() => (
|
{() => (
|
||||||
<>
|
<>
|
||||||
|
@ -2,12 +2,14 @@ import classNames from "classnames";
|
|||||||
import FocusTrap from "focus-trap-react";
|
import FocusTrap from "focus-trap-react";
|
||||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Transition } from "@/components/utils/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
import {
|
import {
|
||||||
useInternalOverlayRouter,
|
useInternalOverlayRouter,
|
||||||
useRouterAnchorUpdate,
|
useRouterAnchorUpdate,
|
||||||
} from "@/hooks/useOverlayRouter";
|
} from "@/hooks/useOverlayRouter";
|
||||||
|
import { TurnstileProvider } from "@/stores/turnstile";
|
||||||
|
|
||||||
export interface OverlayProps {
|
export interface OverlayProps {
|
||||||
id: string;
|
id: string;
|
||||||
@ -15,6 +17,34 @@ export interface OverlayProps {
|
|||||||
darken?: boolean;
|
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 }) {
|
export function OverlayDisplay(props: { children: ReactNode }) {
|
||||||
const router = useInternalOverlayRouter("hello world :)");
|
const router = useInternalOverlayRouter("hello world :)");
|
||||||
const refRouter = useRef(router);
|
const refRouter = useRef(router);
|
||||||
@ -27,7 +57,12 @@ export function OverlayDisplay(props: { children: ReactNode }) {
|
|||||||
r.close();
|
r.close();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
return <div className="popout-location">{props.children}</div>;
|
return (
|
||||||
|
<div className="popout-location">
|
||||||
|
<TurnstileInteractive />
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OverlayPortal(props: {
|
export function OverlayPortal(props: {
|
||||||
|
@ -39,7 +39,7 @@ function ThumbnailDisplay(props: { at: number; show: boolean }) {
|
|||||||
|
|
||||||
if (!props.show || !currentThumbnail) return null;
|
if (!props.show || !currentThumbnail) return null;
|
||||||
return (
|
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 className="w-screen flex justify-center">
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
<div
|
<div
|
||||||
|
@ -10,12 +10,14 @@ import { useCaptions } from "@/components/player/hooks/useCaptions";
|
|||||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||||
import { Input } from "@/components/player/internals/ContextMenu/Input";
|
import { Input } from "@/components/player/internals/ContextMenu/Input";
|
||||||
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
|
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
|
||||||
import { getLanguageFromIETF } from "@/components/player/utils/language";
|
|
||||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
import { CaptionListItem } from "@/stores/player/slices/source";
|
import { CaptionListItem } from "@/stores/player/slices/source";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
import { useSubtitleStore } from "@/stores/subtitles";
|
import { useSubtitleStore } from "@/stores/subtitles";
|
||||||
import { sortLangCodes } from "@/utils/sortLangCodes";
|
import {
|
||||||
|
getPrettyLanguageNameFromLocale,
|
||||||
|
sortLangCodes,
|
||||||
|
} from "@/utils/language";
|
||||||
|
|
||||||
export function CaptionOption(props: {
|
export function CaptionOption(props: {
|
||||||
countryCode?: string;
|
countryCode?: string;
|
||||||
@ -37,7 +39,7 @@ export function CaptionOption(props: {
|
|||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
>
|
>
|
||||||
<span data-code={props.countryCode} className="mr-3 inline-flex">
|
<span data-code={props.countryCode} className="mr-3 inline-flex">
|
||||||
<FlagIcon countryCode={props.countryCode} />
|
<FlagIcon langCode={props.countryCode} />
|
||||||
</span>
|
</span>
|
||||||
<span>{props.children}</span>
|
<span>{props.children}</span>
|
||||||
</span>
|
</span>
|
||||||
@ -89,7 +91,8 @@ function useSubtitleList(subs: CaptionListItem[], searchQuery: string) {
|
|||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const input = subs.map((t) => ({
|
const input = subs.map((t) => ({
|
||||||
...t,
|
...t,
|
||||||
languageName: getLanguageFromIETF(t.language) ?? unknownChoice,
|
languageName:
|
||||||
|
getPrettyLanguageNameFromLocale(t.language) ?? unknownChoice,
|
||||||
}));
|
}));
|
||||||
const sorted = sortLangCodes(input.map((t) => t.language));
|
const sorted = sortLangCodes(input.map((t) => t.language));
|
||||||
let results = input.sort((a, b) => {
|
let results = input.sort((a, b) => {
|
||||||
|
@ -6,11 +6,11 @@ import { Toggle } from "@/components/buttons/Toggle";
|
|||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { useCaptions } from "@/components/player/hooks/useCaptions";
|
import { useCaptions } from "@/components/player/hooks/useCaptions";
|
||||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||||
import { getLanguageFromIETF } from "@/components/player/utils/language";
|
|
||||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
import { qualityToString } from "@/stores/player/utils/qualities";
|
import { qualityToString } from "@/stores/player/utils/qualities";
|
||||||
import { useSubtitleStore } from "@/stores/subtitles";
|
import { useSubtitleStore } from "@/stores/subtitles";
|
||||||
|
import { getPrettyLanguageNameFromLocale } from "@/utils/language";
|
||||||
|
|
||||||
export function SettingsMenu({ id }: { id: string }) {
|
export function SettingsMenu({ id }: { id: string }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -31,7 +31,7 @@ export function SettingsMenu({ id }: { id: string }) {
|
|||||||
const { toggleLastUsed } = useCaptions();
|
const { toggleLastUsed } = useCaptions();
|
||||||
|
|
||||||
const selectedLanguagePretty = selectedCaptionLanguage
|
const selectedLanguagePretty = selectedCaptionLanguage
|
||||||
? getLanguageFromIETF(selectedCaptionLanguage) ??
|
? getPrettyLanguageNameFromLocale(selectedCaptionLanguage) ??
|
||||||
t("player.menus.subtitles.unknownLanguage")
|
t("player.menus.subtitles.unknownLanguage")
|
||||||
: undefined;
|
: 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 { LargeTextPart } from "@/pages/parts/util/LargeTextPart";
|
||||||
import App from "@/setup/App";
|
import App from "@/setup/App";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import i18n from "@/setup/i18n";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { BookmarkSyncer } from "@/stores/bookmarks/BookmarkSyncer";
|
import { BookmarkSyncer } from "@/stores/bookmarks/BookmarkSyncer";
|
||||||
import { useLanguageStore } from "@/stores/language";
|
import { changeAppLanguage, useLanguageStore } from "@/stores/language";
|
||||||
import { ProgressSyncer } from "@/stores/progress/ProgressSyncer";
|
import { ProgressSyncer } from "@/stores/progress/ProgressSyncer";
|
||||||
import { SettingsSyncer } from "@/stores/subtitles/SettingsSyncer";
|
import { SettingsSyncer } from "@/stores/subtitles/SettingsSyncer";
|
||||||
import { ThemeProvider } from "@/stores/theme";
|
import { ThemeProvider } from "@/stores/theme";
|
||||||
@ -123,7 +122,7 @@ function AuthWrapper() {
|
|||||||
|
|
||||||
function MigrationRunner() {
|
function MigrationRunner() {
|
||||||
const status = useAsync(async () => {
|
const status = useAsync(async () => {
|
||||||
i18n.changeLanguage(useLanguageStore.getState().language);
|
changeAppLanguage(useLanguageStore.getState().language);
|
||||||
await initializeOldStores();
|
await initializeOldStores();
|
||||||
}, []);
|
}, []);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -43,7 +43,11 @@ export function MetaPart(props: MetaPartProps) {
|
|||||||
const { error, value, loading } = useAsync(async () => {
|
const { error, value, loading } = useAsync(async () => {
|
||||||
const providerApiUrl = getLoadbalancedProviderApiUrl();
|
const providerApiUrl = getLoadbalancedProviderApiUrl();
|
||||||
if (providerApiUrl) {
|
if (providerApiUrl) {
|
||||||
|
try {
|
||||||
await fetchMetadata(providerApiUrl);
|
await fetchMetadata(providerApiUrl);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("failed-api-metadata");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setCachedMetadata([
|
setCachedMetadata([
|
||||||
...providers.listSources(),
|
...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) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<ErrorLayout>
|
<ErrorLayout>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ProviderControls, ScrapeMedia } from "@movie-web/providers";
|
import { ProviderControls, ScrapeMedia } from "@movie-web/providers";
|
||||||
import classNames from "classnames";
|
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 { useMountedState } from "react-use";
|
||||||
import type { AsyncReturnType } from "type-fest";
|
import type { AsyncReturnType } from "type-fest";
|
||||||
|
|
||||||
@ -8,6 +9,8 @@ import {
|
|||||||
scrapePartsToProviderMetric,
|
scrapePartsToProviderMetric,
|
||||||
useReportProviders,
|
useReportProviders,
|
||||||
} from "@/backend/helpers/report";
|
} from "@/backend/helpers/report";
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { Loading } from "@/components/layout/Loading";
|
||||||
import {
|
import {
|
||||||
ScrapeCard,
|
ScrapeCard,
|
||||||
ScrapeItem,
|
ScrapeItem,
|
||||||
@ -18,6 +21,7 @@ import {
|
|||||||
useListCenter,
|
useListCenter,
|
||||||
useScrape,
|
useScrape,
|
||||||
} from "@/hooks/useProviderScrape";
|
} from "@/hooks/useProviderScrape";
|
||||||
|
import { LargeTextPart } from "@/pages/parts/util/LargeTextPart";
|
||||||
|
|
||||||
export interface ScrapingProps {
|
export interface ScrapingProps {
|
||||||
media: ScrapeMedia;
|
media: ScrapeMedia;
|
||||||
@ -32,9 +36,11 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||||||
const { report } = useReportProviders();
|
const { report } = useReportProviders();
|
||||||
const { startScraping, sourceOrder, sources, currentSource } = useScrape();
|
const { startScraping, sourceOrder, sources, currentSource } = useScrape();
|
||||||
const isMounted = useMountedState();
|
const isMounted = useMountedState();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const listRef = useRef<HTMLDivElement | null>(null);
|
const listRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [failedStartScrape, setFailedStartScrape] = useState<boolean>(false);
|
||||||
const renderedOnce = useListCenter(
|
const renderedOnce = useListCenter(
|
||||||
containerRef,
|
containerRef,
|
||||||
listRef,
|
listRef,
|
||||||
@ -72,7 +78,7 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
props.onGetStream?.(output);
|
props.onGetStream?.(output);
|
||||||
})();
|
})().catch(() => setFailedStartScrape(true));
|
||||||
}, [startScraping, props, report, isMounted]);
|
}, [startScraping, props, report, isMounted]);
|
||||||
|
|
||||||
let currentProviderIndex = sourceOrder.findIndex(
|
let currentProviderIndex = sourceOrder.findIndex(
|
||||||
@ -81,11 +87,28 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||||||
if (currentProviderIndex === -1)
|
if (currentProviderIndex === -1)
|
||||||
currentProviderIndex = sourceOrder.length - 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-full w-full relative dir-neutral:origin-top-left flex"
|
className="h-full w-full relative dir-neutral:origin-top-left flex"
|
||||||
ref={containerRef}
|
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
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"absolute transition-[transform,opacity] opacity-0 dir-neutral:left-0":
|
"absolute transition-[transform,opacity] opacity-0 dir-neutral:left-0":
|
||||||
@ -97,7 +120,7 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||||||
{sourceOrder.map((order) => {
|
{sourceOrder.map((order) => {
|
||||||
const source = sources[order.id];
|
const source = sources[order.id];
|
||||||
const distance = Math.abs(
|
const distance = Math.abs(
|
||||||
sourceOrder.findIndex((t) => t.id === order.id) -
|
sourceOrder.findIndex((o) => o.id === order.id) -
|
||||||
currentProviderIndex,
|
currentProviderIndex,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
@ -4,7 +4,7 @@ import { FlagIcon } from "@/components/FlagIcon";
|
|||||||
import { Dropdown } from "@/components/form/Dropdown";
|
import { Dropdown } from "@/components/form/Dropdown";
|
||||||
import { Heading1 } from "@/components/utils/Text";
|
import { Heading1 } from "@/components/utils/Text";
|
||||||
import { appLanguageOptions } from "@/setup/i18n";
|
import { appLanguageOptions } from "@/setup/i18n";
|
||||||
import { sortLangCodes } from "@/utils/sortLangCodes";
|
import { getLocaleInfo, sortLangCodes } from "@/utils/language";
|
||||||
|
|
||||||
export function LocalePart(props: {
|
export function LocalePart(props: {
|
||||||
language: string;
|
language: string;
|
||||||
@ -17,11 +17,13 @@ export function LocalePart(props: {
|
|||||||
.sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code))
|
.sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code))
|
||||||
.map((opt) => ({
|
.map((opt) => ({
|
||||||
id: opt.code,
|
id: opt.code,
|
||||||
name: `${opt.name} — ${opt.nativeName}`,
|
name: `${opt.name}${opt.nativeName ? ` — ${opt.nativeName}` : ""}`,
|
||||||
leftIcon: <FlagIcon countryCode={opt.code} />,
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import ISO6391 from "iso-639-1";
|
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
import { locales } from "@/assets/languages";
|
import { locales } from "@/assets/languages";
|
||||||
|
import { getLocaleInfo } from "@/utils/language";
|
||||||
|
|
||||||
// Languages
|
// Languages
|
||||||
const langCodes = Object.keys(locales);
|
const langCodes = Object.keys(locales);
|
||||||
@ -10,43 +10,15 @@ const resources = Object.fromEntries(
|
|||||||
Object.entries(locales).map((entry) => [entry[0], { translation: entry[1] }]),
|
Object.entries(locales).map((entry) => [entry[0], { translation: entry[1] }]),
|
||||||
);
|
);
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
fallbackLng: "en",
|
fallbackLng: "en-US",
|
||||||
resources,
|
resources,
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false, // not needed for react as it escapes by default
|
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) => {
|
export const appLanguageOptions = langCodes.map((lang) => {
|
||||||
const extraLang = extraLanguages[lang];
|
const langObj = getLocaleInfo(lang);
|
||||||
if (extraLang) return extraLang;
|
|
||||||
|
|
||||||
const [langObj] = ISO6391.getLanguages([lang]);
|
|
||||||
if (!langObj)
|
if (!langObj)
|
||||||
throw new Error(`Language with code ${lang} cannot be found in database`);
|
throw new Error(`Language with code ${lang} cannot be found in database`);
|
||||||
return langObj;
|
return langObj;
|
||||||
|
@ -11,24 +11,32 @@ interface BannerInstance {
|
|||||||
interface BannerStore {
|
interface BannerStore {
|
||||||
banners: BannerInstance[];
|
banners: BannerInstance[];
|
||||||
isOnline: boolean;
|
isOnline: boolean;
|
||||||
|
isTurnstile: boolean;
|
||||||
location: string | null;
|
location: string | null;
|
||||||
updateHeight(id: string, height: number): void;
|
updateHeight(id: string, height: number): void;
|
||||||
showBanner(id: string): void;
|
showBanner(id: string): void;
|
||||||
hideBanner(id: string): void;
|
hideBanner(id: string): void;
|
||||||
setLocation(loc: string | null): void;
|
setLocation(loc: string | null): void;
|
||||||
updateOnline(isOnline: boolean): void;
|
updateOnline(isOnline: boolean): void;
|
||||||
|
updateTurnstile(isTurnstile: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useBannerStore = create(
|
export const useBannerStore = create(
|
||||||
immer<BannerStore>((set) => ({
|
immer<BannerStore>((set) => ({
|
||||||
banners: [],
|
banners: [],
|
||||||
isOnline: true,
|
isOnline: true,
|
||||||
|
isTurnstile: false,
|
||||||
location: null,
|
location: null,
|
||||||
updateOnline(isOnline) {
|
updateOnline(isOnline) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.isOnline = isOnline;
|
s.isOnline = isOnline;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
updateTurnstile(isTurnstile) {
|
||||||
|
set((s) => {
|
||||||
|
s.isTurnstile = isTurnstile;
|
||||||
|
});
|
||||||
|
},
|
||||||
setLocation(loc) {
|
setLocation(loc) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.location = loc;
|
s.location = loc;
|
||||||
|
@ -4,8 +4,8 @@ import { create } from "zustand";
|
|||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import { immer } from "zustand/middleware/immer";
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
|
||||||
import { rtlLocales } from "@/assets/languages";
|
|
||||||
import i18n from "@/setup/i18n";
|
import i18n from "@/setup/i18n";
|
||||||
|
import { getLocaleInfo } from "@/utils/language";
|
||||||
|
|
||||||
export interface LanguageStore {
|
export interface LanguageStore {
|
||||||
language: string;
|
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() {
|
export function LanguageProvider() {
|
||||||
const language = useLanguageStore((s) => s.language);
|
const language = useLanguageStore((s) => s.language);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
i18n.changeLanguage(language);
|
changeAppLanguage(language);
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
const isRtl = rtlLocales.includes(language as any);
|
const isRtl = isRightToLeft(language);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
|
import { useRef } from "react";
|
||||||
import Turnstile, { BoundTurnstileObject } from "react-turnstile";
|
import Turnstile, { BoundTurnstileObject } from "react-turnstile";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { immer } from "zustand/middleware/immer";
|
import { immer } from "zustand/middleware/immer";
|
||||||
@ -6,19 +8,31 @@ import { reportCaptchaSolve } from "@/backend/helpers/report";
|
|||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
export interface TurnstileStore {
|
export interface TurnstileStore {
|
||||||
turnstile: BoundTurnstileObject | null;
|
isInWidget: boolean;
|
||||||
|
turnstiles: {
|
||||||
|
controls: BoundTurnstileObject;
|
||||||
|
isInPopout: boolean;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
cbs: ((token: string | null) => void)[];
|
cbs: ((token: string | null) => void)[];
|
||||||
setTurnstile(v: BoundTurnstileObject | null): void;
|
setTurnstile(
|
||||||
|
id: string,
|
||||||
|
v: BoundTurnstileObject | null,
|
||||||
|
isInPopout: boolean,
|
||||||
|
): void;
|
||||||
getToken(): Promise<string>;
|
getToken(): Promise<string>;
|
||||||
processToken(token: string | null): void;
|
processToken(token: string | null, widgetId: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTurnstileStore = create(
|
export const useTurnstileStore = create(
|
||||||
immer<TurnstileStore>((set, get) => ({
|
immer<TurnstileStore>((set, get) => ({
|
||||||
turnstile: null,
|
isInWidget: false,
|
||||||
|
turnstiles: [],
|
||||||
cbs: [],
|
cbs: [],
|
||||||
processToken(token) {
|
processToken(token, widgetId) {
|
||||||
const cbs = get().cbs;
|
const cbs = get().cbs;
|
||||||
|
const turnstile = get().turnstiles.find((v) => v.id === widgetId);
|
||||||
|
if (turnstile?.id !== widgetId) return;
|
||||||
cbs.forEach((fn) => fn(token));
|
cbs.forEach((fn) => fn(token));
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.cbs = [];
|
s.cbs = [];
|
||||||
@ -37,16 +51,26 @@ export const useTurnstileStore = create(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setTurnstile(v) {
|
setTurnstile(id, controls, isInPopout) {
|
||||||
set((s) => {
|
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() {
|
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() {
|
export function isTurnstileInitialized() {
|
||||||
@ -55,9 +79,12 @@ export function isTurnstileInitialized() {
|
|||||||
|
|
||||||
export async function getTurnstileToken() {
|
export async function getTurnstileToken() {
|
||||||
const turnstile = getTurnstile();
|
const turnstile = getTurnstile();
|
||||||
turnstile?.reset();
|
|
||||||
turnstile?.execute();
|
|
||||||
try {
|
try {
|
||||||
|
// I hate turnstile
|
||||||
|
(window as any).turnstile.execute(
|
||||||
|
document.querySelector(`#${turnstile.id}`),
|
||||||
|
{},
|
||||||
|
);
|
||||||
const token = await useTurnstileStore.getState().getToken();
|
const token = await useTurnstileStore.getState().getToken();
|
||||||
reportCaptchaSolve(true);
|
reportCaptchaSolve(true);
|
||||||
return token;
|
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 siteKey = conf().TURNSTILE_KEY;
|
||||||
|
const idRef = useRef<string | null>(null);
|
||||||
const setTurnstile = useTurnstileStore((s) => s.setTurnstile);
|
const setTurnstile = useTurnstileStore((s) => s.setTurnstile);
|
||||||
const processToken = useTurnstileStore((s) => s.processToken);
|
const processToken = useTurnstileStore((s) => s.processToken);
|
||||||
if (!siteKey) return null;
|
if (!siteKey) return null;
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
hidden: !props.isInPopout,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<Turnstile
|
<Turnstile
|
||||||
sitekey={siteKey}
|
sitekey={siteKey}
|
||||||
onLoad={(_widgetId, bound) => {
|
onLoad={(widgetId, bound) => {
|
||||||
setTurnstile(bound);
|
idRef.current = widgetId;
|
||||||
|
setTurnstile(widgetId, bound, !!props.isInPopout);
|
||||||
}}
|
}}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
processToken(null);
|
const id = idRef.current;
|
||||||
|
if (!id) return;
|
||||||
|
processToken(null, id);
|
||||||
}}
|
}}
|
||||||
onVerify={(token) => {
|
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());
|
const env = loadEnv(mode, process.cwd());
|
||||||
return {
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
million.vite({ auto: true }),
|
million.vite({ auto: true, mute: true }),
|
||||||
handlebars({
|
handlebars({
|
||||||
vars: {
|
vars: {
|
||||||
opensearchEnabled: env.VITE_OPENSEARCH_ENABLED === "true",
|
opensearchEnabled: env.VITE_OPENSEARCH_ENABLED === "true",
|
||||||
@ -124,8 +124,8 @@ export default defineConfig(({ mode }) => {
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks(id: string) {
|
manualChunks(id: string) {
|
||||||
if (id.includes("@sozialhelden+ietf-language-tags")) {
|
if (id.includes("@sozialhelden+ietf-language-tags") || id.includes("country-language")) {
|
||||||
return "ietf-language-tags";
|
return "language-db";
|
||||||
}
|
}
|
||||||
if (id.includes("hls.js")) {
|
if (id.includes("hls.js")) {
|
||||||
return "hls";
|
return "hls";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user