Files
lampac/mods/WatchTogether/web/i18n.js
T

325 lines
12 KiB
JavaScript

/**
* Three languages, no libraries. Auto-detection picks between Ukrainian and English
* only: Russian is in the list but is never selected without an explicit choice.
*
* A dictionary value is either a string or an object of plural forms
* (one/few/many/other) resolved through Intl.PluralRules.
*/
export const LANGS = [
{ code: "uk", name: "Українська" },
{ code: "en", name: "English" },
{ code: "ru", name: "Русский" },
];
const DICT = {
uk: {
tagline: "Спільний перегляд із Lampa",
lang_label: "Мова",
join_heading: "Увійти за кодом",
code_ph: "Код кімнати",
pwd_ph: "Пароль",
join_btn: "Увійти",
rooms_heading: "Відкриті кімнати",
refresh_btn: "Оновити",
searching: "Шукаємо…",
empty_title: "Поки жодної кімнати",
empty_hint:
"Кімната з’явиться тут, щойно хтось у Lampa створить її з увімкненою публікацією. Якщо код уже є — введіть його вище.",
create_heading: "Створити кімнату",
url_ph: "Посилання на потік (.m3u8 або .mp4)",
title_ph: "Назва",
create_btn: "Створити",
url_hint: "Потрібне https-посилання, і сервер має дозволяти CORS.",
cancel_btn: "Скасувати",
done_btn: "Готово",
settings_heading: "Налаштування",
name_ph: "Ваше ім’я",
regenerate_name: "Згенерувати інше ім’я",
relay_label: "Реле",
publish_label: "Показувати мою кімнату у списку",
leave_btn: "Вийти",
host_badge: "ви хост",
switch_ph: "Нове посилання на потік",
switch_btn: "Перемкнути всім",
status_connecting: "З’єднання…",
status_online: "На зв’язку",
status_offline: "Зв’язок втрачено",
viewers: {
one: "{n} глядач",
few: "{n} глядачі",
many: "{n} глядачів",
other: "{n} глядача",
},
room_created: "Кімнату створено · код {id}",
join_ok: "Ви в кімнаті {name}",
joined: "{name} приєднався",
left_room: "{name} вийшов",
host_changed: "Новий хост: {name}",
you_are_host: "Тепер ви хост",
notice_hold: "Пауза — чекаємо, поки всі завантажать буфер",
notice_go: "Усі готові — продовжуємо",
act_resumed: "{name} продовжив",
act_paused: "{name} поставив на паузу",
act_seeked: "{name} перемотав",
someone: "учасник",
connecting: "Підключення…",
password_prompt: "Пароль кімнати {name}",
need_code: "Введіть код кімнати",
need_url: "Потрібне посилання на потік",
need_pwd_public:
"Кімната у списку потребує пароля — задайте його або вимкніть публікацію",
already_in_room: "Ви вже в кімнаті {name}",
create_failed: "Не вдалося створити кімнату",
join_failed: "Кімнату не знайдено або пароль невірний",
kicked: "Ви підключилися з іншого місця",
no_stream: "У кімнаті немає потоку",
relay_error: "Реле: {text}",
autoplay_blocked: "Браузер заблокував автозапуск — натисніть play",
mixed_content:
"Потік віддається по http, а сторінка по https — браузер це блокує",
cors_error:
"Потік не завантажується: схоже, сервер не віддає Access-Control-Allow-Origin",
playback_error: "Помилка відтворення: {details}",
media_error:
"Браузер не зміг відкрити потік (формат, CORS або mixed content)",
},
en: {
tagline: "Watch together with Lampa",
lang_label: "Language",
join_heading: "Join with a code",
code_ph: "Room code",
pwd_ph: "Password",
join_btn: "Join",
rooms_heading: "Open rooms",
refresh_btn: "Refresh",
searching: "Searching…",
empty_title: "No rooms right now",
empty_hint:
"A room appears here as soon as someone in Lampa creates one with publishing enabled. If you already have a code, enter it above.",
create_heading: "Create a room",
url_ph: "Stream link (.m3u8 or .mp4)",
title_ph: "Title",
create_btn: "Create",
url_hint: "Needs an https link from a server that allows CORS.",
cancel_btn: "Cancel",
done_btn: "Done",
settings_heading: "Settings",
name_ph: "Your name",
regenerate_name: "Generate another name",
relay_label: "Relay",
publish_label: "List my room publicly",
leave_btn: "Leave",
host_badge: "you host",
switch_ph: "New stream link",
switch_btn: "Switch for everyone",
status_connecting: "Connecting…",
status_online: "Connected",
status_offline: "Connection lost",
viewers: { one: "{n} viewer", other: "{n} viewers" },
room_created: "Room created · code {id}",
join_ok: "You are in {name}",
joined: "{name} joined",
left_room: "{name} left",
host_changed: "New host: {name}",
you_are_host: "You are the host now",
notice_hold: "Paused — waiting for everyone to buffer",
notice_go: "Everyone is ready — resuming",
act_resumed: "{name} resumed",
act_paused: "{name} paused",
act_seeked: "{name} skipped",
someone: "someone",
connecting: "Connecting…",
password_prompt: "Password for {name}",
need_code: "Enter a room code",
need_url: "A stream link is required",
need_pwd_public:
"A publicly listed room needs a password — set one or turn off publishing",
already_in_room: "You are already in {name}",
create_failed: "Could not create the room",
join_failed: "Room not found, or the password is wrong",
kicked: "You connected from somewhere else",
no_stream: "The room has no stream",
relay_error: "Relay: {text}",
autoplay_blocked: "The browser blocked autoplay — press play",
mixed_content:
"The stream is served over http while this page is https — the browser blocks that",
cors_error:
"The stream will not load: the server likely omits Access-Control-Allow-Origin",
playback_error: "Playback error: {details}",
media_error:
"The browser could not open the stream (format, CORS or mixed content)",
},
ru: {
tagline: "Совместный просмотр с Lampa",
lang_label: "Язык",
join_heading: "Войти по коду",
code_ph: "Код комнаты",
pwd_ph: "Пароль",
join_btn: "Войти",
rooms_heading: "Открытые комнаты",
refresh_btn: "Обновить",
searching: "Ищем…",
empty_title: "Пока ни одной комнаты",
empty_hint:
"Комната появится здесь, как только кто-то в Lampa создаст её с включённой публикацией. Если код уже есть — введите его выше.",
create_heading: "Создать комнату",
url_ph: "Ссылка на поток (.m3u8 или .mp4)",
title_ph: "Название",
create_btn: "Создать",
url_hint: "Нужна https-ссылка, и сервер должен разрешать CORS.",
cancel_btn: "Отмена",
done_btn: "Готово",
settings_heading: "Настройки",
name_ph: "Ваше имя",
regenerate_name: "Сгенерировать другое имя",
relay_label: "Реле",
publish_label: "Показывать мою комнату в списке",
leave_btn: "Выйти",
host_badge: "вы хост",
switch_ph: "Новая ссылка на поток",
switch_btn: "Переключить всем",
status_connecting: "Соединение…",
status_online: "На связи",
status_offline: "Связь потеряна",
viewers: {
one: "{n} зритель",
few: "{n} зрителя",
many: "{n} зрителей",
other: "{n} зрителя",
},
room_created: "Комната создана · код {id}",
join_ok: "Вы в комнате {name}",
joined: "{name} присоединился",
left_room: "{name} вышел",
host_changed: "Новый хост: {name}",
you_are_host: "Теперь вы хост",
notice_hold: "Пауза — ждём, пока все загрузят буфер",
notice_go: "Все готовы — продолжаем",
act_resumed: "{name} продолжил",
act_paused: "{name} поставил на паузу",
act_seeked: "{name} перемотал",
someone: "участник",
connecting: "Подключение…",
password_prompt: "Пароль комнаты {name}",
need_code: "Введите код комнаты",
need_url: "Нужна ссылка на поток",
need_pwd_public:
"Комната в списке требует пароль — задайте его или отключите публикацию",
already_in_room: "Вы уже в комнате {name}",
create_failed: "Не удалось создать комнату",
join_failed: "Комната не найдена или пароль неверный",
kicked: "Вы подключились из другого места",
no_stream: "В комнате нет потока",
relay_error: "Реле: {text}",
autoplay_blocked: "Браузер заблокировал автозапуск — нажмите play",
mixed_content:
"Поток отдаётся по http, а страница по https — браузер это блокирует",
cors_error:
"Поток не загружается: похоже, сервер не отдаёт Access-Control-Allow-Origin",
playback_error: "Ошибка воспроизведения: {details}",
media_error:
"Браузер не смог открыть поток (формат, CORS или mixed content)",
},
};
export const DICTS = DICT; // exposed so tests can check the dictionaries are complete
const STORE_KEY = "lparty_lang";
const listeners = new Set();
let current = "uk";
/** Russian is deliberately not reachable here: it is a manual choice only. */
export function detectLang() {
for (const tag of navigator.languages || [navigator.language || ""]) {
const code = String(tag).toLowerCase().split("-")[0];
if (code === "uk") return "uk";
if (code === "en") return "en";
}
return "uk";
}
export function getLang() {
return current;
}
export function setLang(code) {
if (!DICT[code]) return;
current = code;
try {
localStorage.setItem(STORE_KEY, code);
} catch (err) {}
document.documentElement.lang = code;
listeners.forEach((fn) => fn(code));
}
export function initLang() {
let saved = null;
try {
saved = localStorage.getItem(STORE_KEY);
} catch (err) {}
current = DICT[saved] ? saved : detectLang();
document.documentElement.lang = current;
return current;
}
export function onLangChange(fn) {
listeners.add(fn);
}
export function t(key, params) {
let value = DICT[current][key];
if (value === undefined) return key; // visible immediately; silence would be worse
if (typeof value === "object") {
const n = Number(params && params.n);
const form = new Intl.PluralRules(current).select(n);
value = value[form] ?? value.other;
}
return params
? value.replace(/\{(\w+)\}/g, (m, name) =>
params[name] === undefined ? m : params[name],
)
: value;
}
/**
* Repaints static nodes. data-i18n sets textContent, data-i18n-ph sets the
* placeholder, data-i18n-label sets aria-label.
*/
export function applyTranslations(root = document) {
root.querySelectorAll("[data-i18n]").forEach((el) => {
el.textContent = t(el.dataset.i18n);
});
root.querySelectorAll("[data-i18n-ph]").forEach((el) => {
el.placeholder = t(el.dataset.i18nPh);
});
root.querySelectorAll("[data-i18n-label]").forEach((el) => {
el.setAttribute("aria-label", t(el.dataset.i18nLabel));
});
}