From 5d4cb7864dc3ca19669877def6c298eb96d19b16 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Sat, 22 Mar 2025 14:27:25 +0100 Subject: new translation system --- client-web/locale/de.ini | 91 +++++++++++++++++++++ client-web/locale/en.ini | 91 +++++++++++++++++++++ client-web/scripts/apply_translations.ts | 0 client-web/scripts/find_missing_translations.ts | 29 ------- client-web/scripts/gen_param_table.ts | 27 ------- client-web/scripts/reformat_json.ts | 16 ---- client-web/scripts/translate_argos.py | 31 ------- client-web/source/chat.ts | 18 ++--- client-web/source/download_stream.ts | 4 +- client-web/source/helper.ts | 4 +- client-web/source/index.ts | 20 ++--- client-web/source/keybinds.ts | 4 +- client-web/source/locale.ts | 29 +++++++ client-web/source/locale/de.ts | 101 ----------------------- client-web/source/locale/en.ts | 101 ----------------------- client-web/source/locale/es.ts | 102 ------------------------ client-web/source/locale/ja.ts | 102 ------------------------ client-web/source/locale/mod.ts | 89 --------------------- client-web/source/menu.ts | 26 +++--- client-web/source/preferences/decl.ts | 5 +- client-web/source/preferences/mod.ts | 3 +- client-web/source/preferences/ui.ts | 14 ++-- client-web/source/resource/file.ts | 22 ++--- client-web/source/resource/track.ts | 16 ++-- client-web/source/room_watches.ts | 16 ++-- client-web/source/user/local.ts | 4 +- client-web/source/user/mod.ts | 6 +- client-web/source/user/remote.ts | 20 ++--- server/src/assets.rs | 9 ++- server/src/main.rs | 4 +- 30 files changed, 309 insertions(+), 695 deletions(-) create mode 100644 client-web/locale/de.ini create mode 100644 client-web/locale/en.ini delete mode 100644 client-web/scripts/apply_translations.ts delete mode 100644 client-web/scripts/find_missing_translations.ts delete mode 100644 client-web/scripts/gen_param_table.ts delete mode 100644 client-web/scripts/reformat_json.ts delete mode 100644 client-web/scripts/translate_argos.py create mode 100644 client-web/source/locale.ts delete mode 100644 client-web/source/locale/de.ts delete mode 100644 client-web/source/locale/en.ts delete mode 100644 client-web/source/locale/es.ts delete mode 100644 client-web/source/locale/ja.ts delete mode 100644 client-web/source/locale/mod.ts diff --git a/client-web/locale/de.ini b/client-web/locale/de.ini new file mode 100644 index 0000000..13a9af8 --- /dev/null +++ b/client-web/locale/de.ini @@ -0,0 +1,91 @@ +[keks-meet] +chat.image_alt=Bild (Klicken zum Öffnen) +chat.input.label=Sende nachricht +chat.input.placeholder=Schreibe eine nachricht +chat.join_message={name} ist beigetreten. +chat.leave_message={name} geht. +chat.summary.empty_message=(leere nachricht) +chat.summary.image=(bild) +chat=Chat +config.audio_activity_threshold=Audioaktivitätsschwellwert +config.auto_gain_control=Automatische Mikrofonlautstärkeanpassung +config.camera_enabled=Füge eine Kameraspur beim Start hinzu +config.camera_facing_mode=Bevorzugte Kameraausrichtung +config.clear_prefs=Du willst alle Einstellungen löschen? Benutz den hier +config.echo_cancellation=Echounterdrückung +config.enable_onbeforeunload=Frage nach Bestätigung beim Verlassen der Seite, wenn Spuren geteilt sind +config.image_view_popup=Öffne Bilder in einem neuen Tab +config.language=Sprache +config.microphone_enabled=Füge eine Mikrofonspur beim Start hinzu +config.microphone_gain=Mikrofonlautstärke +config.native_noise_suppression=Schlage dem Browser vor, selbst Rauschen zu unterdrücken +config.notification.grant=Berechtigen +config.notification.perm_explain=Um Benarchichtigungen zu erhalten, musst du keks-meet die Berechtigung dafür geben. +config.notify_chat=Sende Benachrichtigungen für eingehende Chatnachrichten +config.notify_join=Sende Benachrichtigungen, wenn Benutzer beitreten +config.notify_leave=Sende Benachrichtigungen, wenn Benutzer gehen +config.optional_audio_default_enable=Audiospuren automatisch aktivieren +config.optional_video_default_enable=Videospuren automatisch aktivieren +config.preview_encoding_quality=Preview encoding quality (0 - 100) +config.preview_rate=Preview rate +config.preview_resolution=Preview resolution +config.rnnoise=Benutze RNNoise für Rauschunterdrückung +config.room_watches=Bekannte Räume (Als semikolongetrennte Liste von name=geheimnis Paaren) +config.screencast_audio=Anwendungsaudio bei Bildschirmübertragung aufzeichnen +config.screencast_enabled=Füge eine Bildschirmspur beim Start hinzu +config.send_previews=Send video previews +config.show_log=Zeige ausführlichen log +config.username=Benutzername +config.video_fps=Bevorzugte Bildrate (in Hz) für Bildschirm und Kamera +config.video_resolution=Bevorzugte horizontale Auflösung für Bildschirm und Kamera +config.warn_redirect=Interne Option, die der Server bei einer Weiterleitung setzt +config.webrtc_debug=Zeige erweiterte Informationen zu WebRTC zeugs +confirm_quit=Du teilst Dinge. Wirklich verlassen? +confirm_update=Really update? +controls.leave=Verlassen +controls=Steuerung +documentation=Dokumentation +license=Lizenz +res.audio_stream=Audioübertragung +res.camera=Kamera +res.disable=Deaktivieren +res.enable={name} aktivieren +res.file.download_again=Nochmal Heruntenladen +res.file.download=Herunterladen +res.file.downloading=Lädt herunten… +res.file=Datei +res.fullscreen=Vollbild +res.local=Lokal +res.microphone.mute=Stumm +res.microphone=Mikrofon +res.screen=Bildschirm +res.stop_sharing=Teilen beenden +res.video_stream=Videoübertragung +settings=Einstellungen +source_code=Quellcode +status.await_channel_close=Warten auf das Schließen des Übertragungskanals… +status.await_channel_open=Warten auf Übertragungskanal… +status.await_stream=Übertragung startet… +status.buffering=Puffert… +status.checking=Prüfen... +status.closing=Kanal schließt… +status.connected=Verbunden +status.disconnected=Verbindung getrennt +status.drain_buffer=mount => `Puffer leeren… (buffer={amount}) +status.failed=Verbindung fehlgeschlagen +status.no_conn=Nicht verbunden +unknown_user=Unbekannter Benutzer +warn.mem_download=Download zu Arbeitsspeicher, weil Serviceworker nicht verfügbar sind. +warn.no_crypto=SubtleCrypto ist nicht verfügbar +warn.no_sw=Dein Browser unterstützt die Service Worker API nicht. Erzwungene Updates sind nicht vermeidbar. +warn.no_webrtc=WebRTC wird nicht unterstützt. +warn.old_url=Du wurdest vom alten URL-Format weitergeleitet. Der Server kennt jetzt das Raumgeheimniss. Verschlüsslung ist nicht sicher. +warn.secure_context=Die Seite ist kein 'Secure Context' +warn.short_secret=Raumgeheimniss sehr kurz. Verschlüsslung ist nicht sicher. +room_watches.edit.finish=Fertig +room_watches.edit.add_current_room=Aktuellen Raum hinzufügen +room_watches.edit.add=Hinzufügen +room_watches.edit.move_down=Runter +room_watches.edit.move_up=Hoch +room_watches.edit=Bearbeiten +room_watches=Bekannte Räume diff --git a/client-web/locale/en.ini b/client-web/locale/en.ini new file mode 100644 index 0000000..327af67 --- /dev/null +++ b/client-web/locale/en.ini @@ -0,0 +1,91 @@ +[keks-meet] +chat.image_alt=Image (click to open) +chat.input.label=send message +chat.inputplaceholder=Type a message +chat.join_message={name} joined. +chat.leave_message={name} left. +chat.summary.empty_message=(empty message) +chat.summary.image=(image) +chat=Chat +config.audio_activity_threshold=Audio activity threshold +config.auto_gain_control=Automatically adjust mic gain +config.camera_enabled=Add one camera track on startup +config.camera_facing_mode=Prefer user-facing or env-facing camera +config.clear_prefs=Want to clear all settings? Use this: +config.echo_cancellation=Cancel echo +config.enable_onbeforeunload=Prompt for confirmation when leaving the site while local resources are shared +config.image_view_popup=Open image in popup instead of new tab +config.language=Interface Language +config.microphone_enabled=Add one microphone track on startup +config.microphone_gain=Amplify microphone volume +config.native_noise_suppression=Suggest the browser to do noise suppression +config.notification.grant=Grant +config.notification.perm_explain=For keks-meet to send notifications, it needs you to grant permission. +config.notify_chat=Send notifications for incoming chat messages +config.notify_join=Send notifications when users join +config.notify_leave=Send notifications when users leave +config.optional_audio_default_enable=Enable audio tracks by default +config.optional_video_default_enable=Enable video tracks by default +config.preview_encoding_quality=Preview encoding quality (0 - 100) +config.preview_rate=Preview rate +config.preview_resolution=Preview resolution +config.redirect=Internal option that is set by a server redirect. +config.rnnoise=Use RNNoise for noise suppression +config.room_watches=Known rooms (as semicolon seperated list of name=secret pairs) +config.screencast_audio=Include audio when sharing your screen. +config.screencast_enabled=Add one screencast track on startup +config.send_previews=Send video previews +config.show_log=Show extended log +config.username=Username +config.video_fps=Preferred framerate (in 1/s) for screencast and camera +config.video_resolution=Preferred horizontal resolution for screencast and camera +config.webrtc_debug=Show additional information for WebRTC related stuff +confirm_quit=You have local resources shared. Really quit? +confirm_update=Really update? +controls.leave=Leave +controls=Controls +documentation=Documentation +license=License +res.audio_stream=audio stream +res.camera=Camera +res.disable=Disable +res.enable=Enable {name} +res.file.download_again=Download again +res.file.download=Download +res.file.downloading=Downloading… +res.file=File +res.fullscreen=Fullscreen +res.local=Local +res.microphone.mute=Mute +res.microphone=Microphone +res.screen=Screen +res.stop_sharing=Stop sharing +res.video_stream=video stream +settings=Settings +source_code=Source code +status.await_channel_close=Waiting for data channel to close… +status.await_channel_open=Waiting for data channel to open… +status.await_stream=Awaiting stream… +status.buffering=Buffering… +status.checking=Checking... +status.closing=Channel closing… +status.connected=Connected +status.disconnected=Disconnected +status.drain_buffer=Draining buffers… (buffer: {amount}) +status.failed=Connection failed +status.no_conn=Not connected +unknown_user=Unknown user +warn.mem_download=Downloading to memory because serviceworker is not available. +warn.no_crypto=SubtleCrypto not availible +warn.no_sw=Your browser does not support the Service Worker API, forced automatic updates are unavoidable. +warn.no_webrtc=WebRTC not supported. +warn.old_url=You were redirected from the old URL format. The server knows the room secret now - E2EE is insecure! +warn.secure_context=This page is not a 'Secure Context' +warn.short_secret=Room name is very short. E2EE is insecure! +room_watches.edit.add_current_room=Add current room +room_watches.edit.add=Add +room_watches.edit.finish=Finish edit +room_watches.edit.move_down=Move down +room_watches.edit.move_up=Move up +room_watches.edit=Edit +room_watches=Known Rooms diff --git a/client-web/scripts/apply_translations.ts b/client-web/scripts/apply_translations.ts deleted file mode 100644 index e69de29..0000000 diff --git a/client-web/scripts/find_missing_translations.ts b/client-web/scripts/find_missing_translations.ts deleted file mode 100644 index d39590a..0000000 --- a/client-web/scripts/find_missing_translations.ts +++ /dev/null @@ -1,29 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -/// -import { LOCALES } from "../source/locale/mod.ts"; - -const global_lc = "en" - -function traverse_object(target: any, current: any): any { - if (typeof target == "string") return target - if (typeof target == "function") return undefined - const out = {} as any - for (const key in target) { - if (!current) { - out[key] = target[key] - } else { - if (key in current) continue - out[key] = traverse_object(target[key], current) - } - } - return out -} - -const master = LOCALES[global_lc] -for (const lc in LOCALES) { - if (lc == global_lc) continue - if (lc.search("-") != -1) continue - const k = traverse_object(master, LOCALES[lc]); - if (JSON.stringify(k).length <= 2) continue - console.log(JSON.stringify({ source: global_lc, target: lc, strings: k })); -} diff --git a/client-web/scripts/gen_param_table.ts b/client-web/scripts/gen_param_table.ts deleted file mode 100644 index 49634c0..0000000 --- a/client-web/scripts/gen_param_table.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2023 metamuffin -*/ -import { PREF_DECLS } from "../source/preferences/decl.ts"; -import { PrefDecl } from "../source/preferences/mod.ts"; - -console.log(`Option name|Type|Default|Description`); -console.log(`---|---|---|---`); - -const P = PREF_DECLS as Record> -for (const key in P) { - const e = P[key]; - if (key == "username") e.default = "guest-…" // maybe generalize - const q = (e: string) => `\`${e}\`` - console.log([ - q(key), - typeof e.type, - e.default === undefined ? "-" : q(JSON.stringify(e.default)), - (e.description ?? "*none*") + ( - e.possible_values - ? " (" + e.possible_values.map(e => JSON.stringify(e)).map(q).join(" / ") + ")" - : "" - ) - ].join("|")); -} \ No newline at end of file diff --git a/client-web/scripts/reformat_json.ts b/client-web/scripts/reformat_json.ts deleted file mode 100644 index 35fc1dd..0000000 --- a/client-web/scripts/reformat_json.ts +++ /dev/null @@ -1,16 +0,0 @@ - - - - -const decoder = new TextDecoder(); -let text = "" -for await (const chunk of Deno.stdin.readable) { - text += decoder.decode(chunk); -} - -for (const ob of text.split("\n")) { - if (!ob.length) continue - console.log(JSON.stringify(JSON.parse(ob), null, 4)); -} - - diff --git a/client-web/scripts/translate_argos.py b/client-web/scripts/translate_argos.py deleted file mode 100644 index 2f45446..0000000 --- a/client-web/scripts/translate_argos.py +++ /dev/null @@ -1,31 +0,0 @@ -import json -import sys -from argostranslate import translate - -for line in sys.stdin: - task = json.loads(line) - srclang = task["source"] - dstlang = task["target"] - - installed_languages = { lang.code: lang for lang in translate.load_installed_languages() } - if srclang not in installed_languages: - raise Exception(f"need language {srclang}") - if dstlang not in installed_languages: - raise Exception(f"need language {dstlang}") - srclang = installed_languages[srclang] - dstlang = installed_languages[dstlang] - translator = srclang.get_translation(dstlang) - if translator is None: - raise Exception("no translator available") - - - def tr(key, ob): - if ob == None: return None - if isinstance(ob, list): return [ tr(None,e) for e in ob ] - if isinstance(ob, dict): return { k: tr(k,v) for k, v in ob.items() } - if isinstance(ob, str): - print(f"{srclang.code}->{dstlang.code} {key}", file=sys.stderr) - return translator.translate(ob) - - print(json.dumps(tr("root", task["strings"]))) - diff --git a/client-web/source/chat.ts b/client-web/source/chat.ts index 4531a76..b876e5c 100644 --- a/client-web/source/chat.ts +++ b/client-web/source/chat.ts @@ -7,7 +7,7 @@ import { ChatMessage } from "../../common/packets.d.ts"; import { e, image_view, notify } from "./helper.ts"; -import { PO } from "./locale/mod.ts"; +import { tr } from "./locale.ts"; import { log } from "./logger.ts"; import { chat_control } from "./menu.ts"; import { PREFS } from "./preferences/mod.ts"; @@ -31,15 +31,15 @@ export class Chat { constructor() { const send = document.createElement("input") - send.ariaLabel = PO.chatbox_label + send.ariaLabel = tr("chat.input.label") send.type = "text" - send.placeholder = PO.chatbox_placeholder + send.placeholder = tr("chat.input.placeholder") const messages = e("div", { class: "messages", aria_live: "polite" }) const controls = e("div", { class: "controls" }) controls.append(send) - this.element = e("section", { class: "chat", aria_label: PO.chat, role: "dialog" }, messages, controls) + this.element = e("section", { class: "chat", aria_label: tr("chat"), role: "dialog" }, messages, controls) this.messages = messages this.controls = controls this.send_el = send @@ -87,13 +87,13 @@ export class Chat { add_control_message(m: ControlMessage) { const el = e("div", { class: ["message", "control-message"] }, - ...(m.join ? PO.join_message : PO.leave_message)(e("span", { class: "author" }, m.join?.display_name ?? m.leave?.display_name ?? "")) + tr(m.join ? "chat.join_message" : "chat.leave_message", { name: m.join?.display_name ?? m.leave?.display_name ?? "" }) ) this.messages.append(el) el.scrollIntoView({ block: "end", behavior: "smooth", inline: "end" }) } - create_text_message(text: string) : HTMLElement { + create_text_message(text: string): HTMLElement { const div = document.createElement("div") div.classList.add("text") @@ -105,7 +105,7 @@ export class Chat { } add_message(sender: User, message: ChatMessage) { - const els : HTMLElement[] = [] + const els: HTMLElement[] = [] if (message.text) els.push(this.create_text_message(message.text)) if (message.image) els.push(image_view(message.image, { class: "image" })) @@ -114,9 +114,9 @@ export class Chat { this.messages.append(el) el.scrollIntoView({ block: "end", behavior: "smooth", inline: "end" }) - let body_str = PO.summary_empty_message + let body_str = tr("chat.summary.empty_message") if (message.text) body_str = message.text - if (message.image) body_str = PO.summery_image + if (message.image) body_str = tr("chat.summary.image") if (!(sender instanceof LocalUser) && PREFS.notify_chat) notify(body_str, sender.display_name) } } diff --git a/client-web/source/download_stream.ts b/client-web/source/download_stream.ts index 330fcb2..cc8c16c 100644 --- a/client-web/source/download_stream.ts +++ b/client-web/source/download_stream.ts @@ -4,12 +4,12 @@ Copyright (C) 2023 metamuffin */ /// -import { PO } from "./locale/mod.ts"; +import { tr } from "./locale.ts"; import { log } from "./logger.ts" import { send_sw_message, SW_ENABLED } from "./sw/client.ts" function FallbackStreamDownload(size: number, filename?: string, progress?: (position: number) => void) { - log({ scope: "*", warn: true }, PO.warn_mem_download) + log({ scope: "*", warn: true }, tr("warn.mem_download")) let position = 0 let buffer = new Uint8Array(size) return { diff --git a/client-web/source/helper.ts b/client-web/source/helper.ts index 70b7c28..9b5b43d 100644 --- a/client-web/source/helper.ts +++ b/client-web/source/helper.ts @@ -5,7 +5,7 @@ */ /// -import { PO } from "./locale/mod.ts"; +import { tr } from "./locale.ts"; import { PREFS } from "./preferences/mod.ts"; interface Opts { @@ -64,7 +64,7 @@ export function image_view(url: string, opts?: Opts): HTMLElement { const img = document.createElement("img") apply_opts(img, opts ?? {}) img.src = url - img.alt = PO.image_alt + img.alt = tr("chat.image_alt") img.addEventListener("click", () => { globalThis.open(url, "_blank", `noreferrer=true,noopener=true,popup=${PREFS.image_view_popup}`) }) diff --git a/client-web/source/index.ts b/client-web/source/index.ts index 73572cb..1e16d23 100644 --- a/client-web/source/index.ts +++ b/client-web/source/index.ts @@ -14,8 +14,8 @@ import { SignalingConnection } from "./protocol/mod.ts"; import { Room } from "./room.ts" import { control_bar, info_br } from "./menu.ts"; import { Chat } from "./chat.ts" -import { init_locale } from "./locale/mod.ts"; -import { PO } from "./locale/mod.ts"; +import { init_locale } from "./locale.ts"; +import { tr } from "./locale.ts"; export const VERSION = "1.0.4" globalThis.addEventListener("DOMContentLoaded", () => main()) @@ -54,7 +54,7 @@ function set_room(state: AppState, secret: string, rtc_config: RTCConfiguration) state.center.removeChild(state.room.element) state.room.destroy() } - if (secret.length < 8) log({ scope: "crypto", warn: true }, PO.warn_short_secret) + if (secret.length < 8) log({ scope: "crypto", warn: true }, tr("warn.short_secret")) if (secret.split("#").length > 1) document.title = `${secret.split("#")[0]} | keks-meet` state.room = new Room(state.conn, state.chat, rtc_config) state.chat.room = state.room @@ -72,15 +72,15 @@ export async function main() { const config: ClientConfig = await config_res.json() log("*", "config loaded. starting") - init_locale(PREFS.language ?? "en-US") + await init_locale() document.body.querySelectorAll(".loading").forEach(e => e.remove()) - if (!globalThis.isSecureContext) log({ scope: "*", warn: true }, PO.warn_secure_context) - if (!globalThis.RTCPeerConnection) return log({ scope: "webrtc", error: true }, PO.warn_no_webrtc) - if (!globalThis.crypto.subtle) return log({ scope: "crypto", error: true }, PO.warn_no_crypto) - if (!globalThis.navigator.serviceWorker) log({ scope: "*", warn: true }, PO.warn_no_sw) - if (PREFS.warn_redirect) log({ scope: "crypto", warn: true }, PO.warn_old_url) + if (!globalThis.isSecureContext) log({ scope: "*", warn: true }, tr("warn.secure_context")) + if (!globalThis.RTCPeerConnection) return log({ scope: "webrtc", error: true }, tr("warn.no_webrtc")) + if (!globalThis.crypto.subtle) return log({ scope: "crypto", error: true }, tr("warn.no_crypto")) + if (!globalThis.navigator.serviceWorker) log({ scope: "*", warn: true }, tr("warn.no_sw")) + if (PREFS.warn_redirect) log({ scope: "crypto", warn: true }, tr("warn.old_url")) const sud = e("div", { class: "side-ui" }) const state: AppState = { @@ -112,7 +112,7 @@ export async function main() { globalThis.onbeforeunload = ev => { if (state.room && state.room.local_user.resources.size != 0 && PREFS.enable_onbeforeunload) { ev.preventDefault() - return PO.confirm_quit + return tr("confirm_quit") } } diff --git a/client-web/source/keybinds.ts b/client-web/source/keybinds.ts index 9284b92..781008a 100644 --- a/client-web/source/keybinds.ts +++ b/client-web/source/keybinds.ts @@ -6,7 +6,7 @@ /// import { AppState } from "./index.ts"; -import { PO } from "./locale/mod.ts"; +import { tr } from "./locale.ts"; import { chat_control } from "./menu.ts"; import { create_camera_res, create_mic_res, create_screencast_res } from "./resource/track.ts"; import { update_serviceworker } from "./sw/client.ts"; @@ -25,7 +25,7 @@ export function setup_keybinds(state: AppState) { if (ev.code == "KeyS") state.room?.local_user.await_add_resource(create_screencast_res()) if (ev.code == "KeyC" && !ev.ctrlKey) state.room?.local_user.await_add_resource(create_camera_res()) if (ev.code == "KeyC" && ev.ctrlKey) state.room?.local_user.resources.forEach(t => t.destroy()) - if (ev.code == "KeyU") if (globalThis.confirm(PO.confirm_update)) update_serviceworker() + if (ev.code == "KeyU") if (globalThis.confirm(tr("confirm_update"))) update_serviceworker() if (ev.code == "KeyV") state.chat?.remove_oldest_message() } }) diff --git a/client-web/source/locale.ts b/client-web/source/locale.ts new file mode 100644 index 0000000..09849cc --- /dev/null +++ b/client-web/source/locale.ts @@ -0,0 +1,29 @@ +import { PREFS } from "./preferences/mod.ts"; + +export const LANGS = ["en", "de"] + +const translations: { [key: string]: string } = {} + +export async function init_locale() { + let lang = "en" + if (PREFS.language == "system") { + const nl = navigator.language.split("-")[0] + if (LANGS.includes(nl)) lang = nl + } + if (LANGS.includes(PREFS.language)) lang = PREFS.language + + const resp = await fetch(`/locale/${lang}.ini`) + if (!resp.ok) throw new Error("language load failed"); + const ini = await resp.text() + for (const line of ini.split("\n")) { + if (!line.length || line == "[keks-meet]") continue + const [key, value] = line.split("=", 2) + translations[key] = value + } + console.log(translations); + +} + +export function tr(key: string, params: { [key: string]: string } = {}): string { + return (translations[key] ?? `MISSING TR ${key}`).replace(/{(\w+)}/ig, (_m, n) => params[n]) +} diff --git a/client-web/source/locale/de.ts b/client-web/source/locale/de.ts deleted file mode 100644 index 663aac4..0000000 --- a/client-web/source/locale/de.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2024 metamuffin -*/ -import { LanguageStrings } from "./mod.ts"; - -export const PO_DE: LanguageStrings = { - microphone: "Mikrofon", - chatbox_placeholder: "Schreibe eine nachricht", - chatbox_label: "Sende nachricht", - join_message: author => [author, " ist beigetreten."], - leave_message: author => [author, " geht."], - summary_empty_message: "(leere nachricht)", - summery_image: "(bild)", - camera: "Kamera", - file: "Datei", - fullscreen: "Vollbild", - leave: "Verlassen", - screen: "Bildschirm", - image_alt: "Bild (Klicken zum Öffnen)", - warn_mem_download: "Download zu Arbeitsspeicher, weil Serviceworker nicht verfügbar sind.", - confirm_update: "Really update?", - warn_short_secret: "Raumgeheimniss sehr kurz; Verschlüsslung ist nicht sicher.", - warn_secure_context: "Die Seite ist kein 'Secure Context'", - warn_no_webrtc: "WebRTC wird nicht unterstützt.", - warn_no_crypto: "SubtleCrypto ist nicht verfügbar", - warn_no_sw: "Dein Browser unterstützt die Service Worker API nicht; Erzwungene Updates sind nicht vermeidbar.", - warn_old_url: "Du wurdest vom alten URL-Format weitergeleitet. Der Server kennt jetzt das Raumgeheimniss; Verschlüsslung ist nicht sicher.", - confirm_quit: "Du teilst Dinge. Wirklich verlassen?", - controls: "Steuerung", - license: "Lizenz", - source_code: "Quellcode", - stop_sharing: "Teilen beenden", - documentation: "Dokumentation", - known_rooms: "Bekannte Räume", - chat: "Chat", - settings: "Einstellungen", - edit: "Bearbeiten", - finish_edit: "Fertig", - local: "Lokal", - add_current_room: "Aktuellen Raum hinzufügen", - add: "Hinzufügen", - move_down: "Runter", - move_up: "Hoch", - unknown_user: "Unbekannter Benutzer", - status_checking: "Prüfen...", - status_connected: "Verbunden", - status_failed: "Verbindung fehlgeschlagen", - status_disconnected: "Verbindung getrennt", - status_no_conn: "Nicht verbunden", - status_await_channel_open: "Warten auf Übertragungskanal…", - status_await_channel_close: "Warten auf das Schließen des Übertragungskanals…", - downloading: "Lädt herunten…", - download_again: "Nochmal Heruntenladen", - download: "Herunterladen", - status_drain_buffer: amount => `Puffer leeren… (buffer: ${amount})`, - status_buffering: "Puffert…", - status_closing: "Kanal schließt…", - mute: "Stumm", - video_stream: "Videoübertragung", - audio_stream: "Audioübertragung", - disable: "Deaktivieren", - enable: thing => `${thing} aktivieren`, - status_await_stream: "Übertragung startet…", - notification_perm_explain: "Um Benarchichtigungen zu erhalten, musst du keks-meet die Berechtigung dafür geben. ", - grant: "Berechtigen", - clear_prefs: "Du willst alle Einstellungen löschen? Benutz den hier: ", - setting_descs: { - language: "Sprache", - warn_redirect: "Interne Option, die der Server bei einer Weiterleitung setzt", - image_view_popup: "Öffne Bilder in einem neuen Tab", - webrtc_debug: "Zeige erweiterte Informationen zu WebRTC zeugs", - screencast_audio: "Anwendungsaudio bei Bildschirmübertragung aufzeichnen", - microphone_enabled: "Füge eine Mikrofonspur beim Start hinzu", - screencast_enabled: "Füge eine Bildschirmspur beim Start hinzu", - camera_enabled: "Füge eine Kameraspur beim Start hinzu", - rnnoise: "Benutze RNNoise für Rauschunterdrückung", - native_noise_suppression: "Schlage dem Browser vor, selbst Rauschen zu unterdrücken", - microphone_gain: "Mikrofonlautstärke", - video_fps: "Bevorzugte Bildrate (in Hz) für Bildschirm und Kamera", - video_resolution: "Bevorzugte horizontale Auflösung für Bildschirm und Kamera", - camera_facing_mode: "Bevorzugte Kameraausrichtung", - auto_gain_control: "Automatische Mikrofonlautstärkeanpassung", - echo_cancellation: "Echounterdrückung", - audio_activity_threshold: "Audioaktivitätsschwellwert", - optional_audio_default_enable: "Audiospuren automatisch aktivieren", - optional_video_default_enable: "Videospuren automatisch aktivieren", - notify_chat: "Sende Benachrichtigungen für eingehende Chatnachrichten", - notify_join: "Sende Benachrichtigungen, wenn Benutzer beitreten", - notify_leave: "Sende Benachrichtigungen, wenn Benutzer gehen", - enable_onbeforeunload: "Frage nach Bestätigung beim Verlassen der Seite, wenn Spuren geteilt sind", - room_watches: "Bekannte Räume (Als semikolongetrennte Liste von name=geheimnis Paaren)", - username: "Benutzername", - show_log: "Zeige ausführlichen log", - preview_rate: "Preview rate", - send_previews: "Send video previews", - preview_resolution: "Preview resolution", - preview_encoding_quality: "Preview encoding quality (0 - 100)", - } -} diff --git a/client-web/source/locale/en.ts b/client-web/source/locale/en.ts deleted file mode 100644 index 4498412..0000000 --- a/client-web/source/locale/en.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2024 metamuffin -*/ -import { LanguageStrings } from "./mod.ts"; - -export const PO_EN: LanguageStrings = { - microphone: "Microphone", - chatbox_placeholder: "Type a message", - chatbox_label: "send message", - join_message: author => [author, " joined."], - leave_message: author => [author, " left."], - summary_empty_message: "(empty message)", - summery_image: "(image)", - camera: "Camera", - file: "File", - fullscreen: "Fullscreen", - leave: "Leave", - screen: "Screen", - image_alt: "Image (click to open)", - warn_mem_download: "Downloading to memory because serviceworker is not available.", - confirm_update: "Really update?", - warn_short_secret: "Room name is very short. E2EE is insecure!", - warn_secure_context: "This page is not a 'Secure Context'", - warn_no_webrtc: "WebRTC not supported.", - warn_no_crypto: "SubtleCrypto not availible", - warn_no_sw: "Your browser does not support the Service Worker API, forced automatic updates are unavoidable.", - warn_old_url: "You were redirected from the old URL format. The server knows the room secret now - E2EE is insecure!", - confirm_quit: "You have local resources shared. Really quit?", - controls: "Controls", - license: "License", - source_code: "Source code", - stop_sharing: "Stop sharing", - documentation: "Documentation", - known_rooms: "Known Rooms", - chat: "Chat", - settings: "Settings", - edit: "Edit", - finish_edit: "Finish edit", - add_current_room: "Add current room", - add: "Add", - move_down: "Move down", - move_up: "Move up", - unknown_user: "Unknown user", - status_checking: "Checking...", - status_connected: "Connected", - status_failed: "Connection failed", - status_disconnected: "Disconnected", - status_no_conn: "Not connected", - status_await_channel_open: "Waiting for data channel to open…", - status_await_channel_close: "Waiting for data channel to close…", - downloading: "Downloading…", - download_again: "Download again", - download: "Download", - status_drain_buffer: amount => `Draining buffers… (buffer: ${amount})`, - status_buffering: "Buffering…", - status_closing: "Channel closing…", - mute: "Mute", - video_stream: "video stream", - audio_stream: "audio stream", - local: "Local", - disable: "Disable", - enable: thing => `Enable ${thing}`, - status_await_stream: "Awaiting stream…", - notification_perm_explain: "For keks-meet to send notifications, it needs you to grant permission: ", - grant: "Grant", - clear_prefs: "Want to clear all settings? Use this:", - setting_descs: { - language: "Interface Language", - warn_redirect: "Internal option that is set by a server redirect.", - image_view_popup: "Open image in popup instead of new tab", - webrtc_debug: "Show additional information for WebRTC related stuff", - screencast_audio: "Include audio when sharing your screen.", - microphone_enabled: "Add one microphone track on startup", - screencast_enabled: "Add one screencast track on startup", - camera_enabled: "Add one camera track on startup", - rnnoise: "Use RNNoise for noise suppression", - native_noise_suppression: "Suggest the browser to do noise suppression", - microphone_gain: "Amplify microphone volume", - video_fps: "Preferred framerate (in 1/s) for screencast and camera", - video_resolution: "Preferred horizontal resolution for screencast and camera", - camera_facing_mode: "Prefer user-facing or env-facing camera", - auto_gain_control: "Automatically adjust mic gain", - echo_cancellation: "Cancel echo", - audio_activity_threshold: "Audio activity threshold", - optional_audio_default_enable: "Enable audio tracks by default", - optional_video_default_enable: "Enable video tracks by default", - notify_chat: "Send notifications for incoming chat messages", - notify_join: "Send notifications when users join", - notify_leave: "Send notifications when users leave", - enable_onbeforeunload: "Prompt for confirmation when leaving the site while local resources are shared", - room_watches: "Known rooms (as semicolon seperated list of name=secret pairs)", - username: "Username", - show_log: "Show extended log", - preview_rate: "Preview rate", - send_previews: "Send video previews", - preview_resolution: "Preview resolution", - preview_encoding_quality: "Preview encoding quality (0 - 100)", - } -} diff --git a/client-web/source/locale/es.ts b/client-web/source/locale/es.ts deleted file mode 100644 index 1ee6e4e..0000000 --- a/client-web/source/locale/es.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2024 metamuffin -*/ -import { LanguageStrings } from "./mod.ts"; - -// TODO this is mostly autogenerated. please fix it. -export const PO_ES: LanguageStrings = { - join_message: author => [author, " unir."], - status_drain_buffer: amount => `Draining buffers... (buffer: ${amount})`, - enable: thing => `Activar ${thing}`, - leave_message: author => [author, " salir."], - microphone: "Microfono", - chatbox_placeholder: "Escriba un mensaje", - chatbox_label: "Enviar mensaje", - summary_empty_message: "(mensaje vacío)", - summery_image: "(imagen)", - camera: "Cámara", - file: "Archivo", - fullscreen: "Pantalla completa", - leave: "Salir", - screen: "Pantalla", - image_alt: "Imagen (haga clic para abrir)", - warn_mem_download: "Descarga a la memoria porque el servicio de trabajo no está disponible.", - confirm_update: "¿En serio?", - warn_short_secret: "El nombre de la habitación es muy corto. ¡E2EE es inseguro!", - warn_secure_context: "Esta página no es un 'Contexto Seguro' '", - warn_no_webrtc: "WebRTC no es compatible.", - warn_no_crypto: "SubtleCrypto no disponible", - warn_no_sw: "Su navegador no admite el Service Worker API, las actualizaciones automáticas forzadas son inevitables.", - warn_old_url: "Usted fue redireccionado desde el antiguo formato URL. El servidor conoce el secreto de la habitación ahora - E2EE es inseguro!", - confirm_quit: "Tienes recursos locales compartidos. ¿De verdad renunciaste?", - controls: "Controles", - license: "Licencia", - source_code: "Código fuente", - stop_sharing: "Deja de compartir", - documentation: "Documentación", - known_rooms: "Habitaciones conocidas", - chat: "Chat", - settings: "Ajustes", - edit: "Editar", - finish_edit: "Edición final", - add_current_room: "Agregar habitación actual", - add: "Añadir", - move_down: "Muévete.", - move_up: "Muévanse.", - unknown_user: "Usuario desconocido", - status_checking: "Comprobando...", - status_connected: "Conectado", - status_failed: "La conexión falló", - status_disconnected: "Desconectado", - status_no_conn: "No conectado", - status_await_channel_open: "Esperando que el canal de datos se abra...", - status_await_channel_close: "Esperando que el canal de datos cierre...", - downloading: "Descargando...", - download_again: "Descargar de nuevo", - download: "Descargar", - status_buffering: "Buffering...", - status_closing: "Cierre de canales...", - mute: "Mute", - video_stream: "secuencia de vídeo", - audio_stream: "flujo de audio", - local: "Local", - disable: "Desactivar", - status_await_stream: "A la espera de la corriente...", - notification_perm_explain: "Para que keks-meet envíe notificaciones, necesita que usted conceda permiso:", - grant: "Grant", - clear_prefs: "¿Quieres limpiar todos los ajustes? Usa esto:", - setting_descs: { - language: "Lengua de interfacio", - warn_redirect: "Opción interna que se establece por un servidor redireccionar.", - image_view_popup: "Imagen abierta en popup en lugar de nueva pestaña", - webrtc_debug: "Mostrar información adicional para cosas relacionadas con WebRTC", - screencast_audio: "Incluya el audio al compartir su pantalla.", - microphone_enabled: "Añadir una pista de micrófono en el arranque", - screencast_enabled: "Añadir una pista de pantalla en el inicio", - camera_enabled: "Añadir una pista de cámara en el inicio", - rnnoise: "Use RNNoise para la supresión del ruido", - native_noise_suppression: "Sugerir el navegador para hacer la supresión del ruido", - microphone_gain: "Amplificar el volumen del micrófono", - video_fps: "Marco preferido (en 1/s) para pantalla y cámara", - video_resolution: "Resolución horizontal preferida para pantalla y cámara", - camera_facing_mode: "Preferir cámara de cara al usuario o env-facing", - auto_gain_control: "Ajuste automático de ganancia de micrófono", - echo_cancellation: "Cancelar eco", - audio_activity_threshold: "Nivel de actividad de audio", - optional_audio_default_enable: "Permitir pistas de audio por defecto", - optional_video_default_enable: "Permitir pistas de vídeo por defecto", - notify_chat: "Enviar notificaciones para mensajes de chat entrantes", - notify_join: "Enviar notificaciones cuando los usuarios se unan", - notify_leave: "Enviar notificaciones cuando los usuarios dejan", - enable_onbeforeunload: "Prompt for confirmation when leaving the site while local resources are shared", - room_watches: "Habitaciones conocidas (como semicolon seperated list of name=secret pairs)", - username: "Nombre de usuario", - show_log: "Mostrar registro extendido.", - preview_rate: "Preview rate", - send_previews: "Send video previews", - preview_resolution: "Preview resolution", - preview_encoding_quality: "Preview encoding quality (0 - 100)", - } -} diff --git a/client-web/source/locale/ja.ts b/client-web/source/locale/ja.ts deleted file mode 100644 index c20b926..0000000 --- a/client-web/source/locale/ja.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2024 metamuffin -*/ -import { LanguageStrings } from "./mod.ts"; - -// TODO this is mostly autogenerated. please fix it. -export const PO_JA: LanguageStrings = { - join_message: author => [author, " は会議に参加しました。"], - status_drain_buffer: amount => `バッファの削除... (バッファ: ${amount})`, - enable: thing => `${thing} アクセス。`, - leave_message: author => [author, " は会議を左に。"], - microphone: "マイクロホン", - chatbox_placeholder: "メッセージの種類", - chatbox_label: "送信メッセージ", - summary_empty_message: "(空のメッセージ)", - summery_image: "(写真)", - camera: "カメラ", - file: "ファイル", - fullscreen: "フルスクリーン", - leave: "おすすめ", - screen: "スクリーン", - image_alt: "画像(クリックして開きます)", - warn_mem_download: "サービスワーカーが利用できないので、メモリにダウンロードしてください。", - confirm_update: "本当に更新?", - warn_short_secret: "部屋名がとても短いです。 E2EEは安全です!", - warn_secure_context: "このページは「セキュアコンテキスト」ではありません お問い合わせ", - warn_no_webrtc: "WebRTC はサポートしていません。", - warn_no_crypto: "SubtleCrypto 利用不可", - warn_no_sw: "ブラウザはサービスワーカーAPIをサポートしていません。強制自動更新は無効です。", - warn_old_url: "古いURL形式からリダイレクトされました。 サーバは部屋の秘密を今知っている - E2EE は安全です!", - confirm_quit: "現地のリソース共有があります。 本当に終了?", - controls: "コントロール", - license: "ライセンス", - source_code: "ソースコード", - stop_sharing: "共有の停止", - documentation: "ドキュメント", - known_rooms: "既知の客室", - chat: "チャット", - settings: "コンテンツ", - edit: "編集", - finish_edit: "終わりの編集", - add_current_room: "現在の部屋を追加", - add: "追加する", - move_down: "移動する", - move_up: "移動する", - unknown_user: "不明なユーザー", - status_checking: "チェックイン", - status_connected: "コネクテッド", - status_failed: "接続失敗", - status_disconnected: "接続解除", - status_no_conn: "接続しない", - status_await_channel_open: "データチャネルが開くのを待って...", - status_await_channel_close: "データチャネルを閉じるのを待って...", - downloading: "ダウンロード...", - download_again: "ダウンロード", - download: "ダウンロード", - status_buffering: "バッファリング...", - status_closing: "チャンネル閉鎖...", - mute: "ミュート", - video_stream: "ビデオストリーム", - audio_stream: "オーディオストリーム", - local: "ローカル", - disable: "免責事項", - status_await_stream: "待ち時間の流れ", - notification_perm_explain: "通知を送信するkeks-meetの場合、許可を付与する必要があります。", - grant: "助成金", - clear_prefs: "すべての設定をクリアしたいですか? これを使う:", - setting_descs: { - language: "インターフェイス言語", - warn_redirect: "サーバリダイレクトで設定する内部オプション。", - image_view_popup: "新しいタブではなくポップアップでイメージを開く", - webrtc_debug: "WebRTC関連コンテンツの追加情報を表示", - screencast_audio: "画面を共有するときにオーディオを含める。", - microphone_enabled: "起動時に1台のマイクトラックを追加", - screencast_enabled: "スタートアップで1つのスクリーンキャストトラックを追加", - camera_enabled: "起動時に1つのカメラトラックを追加", - rnnoise: "騒音抑制にRNNoiseを使用", - native_noise_suppression: "ノイズ抑制を行うブラウザを提案する", - microphone_gain: "マイクの音量を増幅", - video_fps: "スクリーンキャストとカメラの推奨フレームレート(1 /秒)", - video_resolution: "スクリーンキャストおよびカメラのための好まれる横の決断", - camera_facing_mode: "優先ユーザーフェーシングまたはエンブフェーシングカメラ", - auto_gain_control: "自動的にmic利益を調節して下さい", - echo_cancellation: "キャンセルエコー", - audio_activity_threshold: "オーディオ活動のしきい値", - optional_audio_default_enable: "デフォルトでオーディオトラックを有効にする", - optional_video_default_enable: "デフォルトでビデオトラックを有効にする", - notify_chat: "チャットメッセージを受信するための通知を送信する", - notify_join: "ユーザーが参加したときに通知を送信する", - notify_leave: "ユーザーが退去したときに通知を送信", - enable_onbeforeunload: "ローカルリソースが共有されている間、サイトを離れるときに確認のためのプロンプト", - room_watches: "既知の客室(セミコロンは、name=secretペアの区切りリストとして)", - username: "ユーザ名", - show_log: "拡張ログを表示します。", - preview_rate: "Preview rate", - send_previews: "Send video previews", - preview_resolution: "Preview resolution", - preview_encoding_quality: "Preview encoding quality (0 - 100)", - }, -} diff --git a/client-web/source/locale/mod.ts b/client-web/source/locale/mod.ts deleted file mode 100644 index 1082330..0000000 --- a/client-web/source/locale/mod.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { PO_DE } from "./de.ts"; -import { PO_EN } from "./en.ts"; -import { PREF_DECLS } from "../preferences/decl.ts"; -import { PO_JA } from "./ja.ts"; -import { PO_ES } from "./es.ts"; - -export let PO: LanguageStrings; - -export function init_locale(lang: string) { - if (lang == "system") lang = navigator.language - if (!LOCALES[lang]) lang = "en-US" - PO = LOCALES[lang] -} - -export const LOCALES: { [key: string]: LanguageStrings } = { - "en": PO_EN, - "en-US": PO_EN, - "en-GB": PO_EN, // close enough - "de": PO_DE, - "de-DE": PO_DE, - "ja": PO_JA, - "ja-JP": PO_JA, - "es": PO_ES, - "es-ES": PO_ES, -} - -export interface LanguageStrings { - microphone: string, - camera: string, - screen: string, - file: string, - fullscreen: string, - local: string, - warn_short_secret: string, - warn_no_webrtc: string, - warn_secure_context: string, - warn_no_crypto: string, - warn_no_sw: string, - warn_old_url: string, - warn_mem_download: string, - chatbox_placeholder: string, - chatbox_label: string, - confirm_quit: string, - controls: string, - license: string, - source_code: string, - documentation: string, - chat: string, - settings: string, - known_rooms: string, - leave: string, - confirm_update: string, - image_alt: string, - join_message(author: HTMLElement | string): (HTMLElement | string)[], - leave_message(author: HTMLElement | string): (HTMLElement | string)[], - summary_empty_message: string, - summery_image: string, - edit: string, - finish_edit: string, - add_current_room: string, - add: string, - move_up: string, - move_down: string, - unknown_user: string, - status_connected: string, - status_no_conn: string, - status_checking: string, - status_disconnected: string, - status_failed: string, - downloading: string, - download: string, - download_again: string, - stop_sharing: string, - status_await_channel_open: string, - status_await_channel_close: string, - status_drain_buffer(amount: number): string, - status_buffering: string, - status_closing: string, - mute: string, - video_stream: string, - audio_stream: string, - enable: (thing: string) => string, - disable: string, - notification_perm_explain: string, - grant: string, - status_await_stream: string, - clear_prefs: string, - setting_descs: { [key in keyof typeof PREF_DECLS]: string }, -} diff --git a/client-web/source/menu.ts b/client-web/source/menu.ts index 93deef1..10c2b67 100644 --- a/client-web/source/menu.ts +++ b/client-web/source/menu.ts @@ -8,7 +8,7 @@ import { e, sleep } from "./helper.ts" import { AppState } from "./index.ts"; import { VERSION } from "./index.ts" -import { PO } from "./locale/mod.ts"; +import { tr } from "./locale.ts"; import { ui_preferences } from "./preferences/ui.ts" import { create_file_res } from "./resource/file.ts"; import { create_camera_res, create_mic_res, create_screencast_res } from "./resource/track.ts"; @@ -29,30 +29,30 @@ export function info_br() { return e("footer", { class: "info-br" }, e("p", { class: "version" }, `keks-meet ${VERSION}`), - item(PO.license, "https://codeberg.org/metamuffin/keks-meet/raw/branch/master/COPYING"), - item(PO.source_code, "https://codeberg.org/metamuffin/keks-meet"), - item(PO.documentation, "https://codeberg.org/metamuffin/keks-meet/src/branch/master/readme.md"), + item(tr("license"), "https://codeberg.org/metamuffin/keks-meet/raw/branch/master/COPYING"), + item(tr("source_code"), "https://codeberg.org/metamuffin/keks-meet"), + item(tr("documentation"), "https://codeberg.org/metamuffin/keks-meet/src/branch/master/readme.md"), ) } export let chat_control: (s?: boolean) => void; export function control_bar(state: AppState, side_ui_container: HTMLElement): HTMLElement { - const leave = e("button", { icon: "leave", class: "abort", onclick() { globalThis.location.href = "/" } }, PO.leave) - const chat = side_ui(side_ui_container, state.chat.element, "chat", PO.chat, state.chat) - const prefs = side_ui(side_ui_container, ui_preferences(), "settings", PO.settings) - const rwatches = side_ui(side_ui_container, ui_room_watches(state.conn), "room", PO.known_rooms) + const leave = e("button", { icon: "leave", class: "abort", onclick() { globalThis.location.href = "/" } }, tr("controls.leave")) + const chat = side_ui(side_ui_container, state.chat.element, "chat", tr("chat"), state.chat) + const prefs = side_ui(side_ui_container, ui_preferences(), "settings", tr("settings")) + const rwatches = side_ui(side_ui_container, ui_room_watches(state.conn), "room", tr("room_watches")) const local_controls = [ - e("button", { icon: "microphone", onclick: () => state.room?.local_user.await_add_resource(create_mic_res()) }, PO.microphone), - e("button", { icon: "camera", onclick: () => state.room?.local_user.await_add_resource(create_camera_res()) }, PO.camera), - e("button", { icon: "screen", onclick: () => state.room?.local_user.await_add_resource(create_screencast_res()) }, PO.screen), - e("button", { icon: "file", onclick: () => state.room?.local_user.await_add_resource(create_file_res()) }, PO.file), + e("button", { icon: "microphone", onclick: () => state.room?.local_user.await_add_resource(create_mic_res()) }, tr("res.microphone")), + e("button", { icon: "camera", onclick: () => state.room?.local_user.await_add_resource(create_camera_res()) }, tr("res.camera")), + e("button", { icon: "screen", onclick: () => state.room?.local_user.await_add_resource(create_screencast_res()) }, tr("res.screen")), + e("button", { icon: "file", onclick: () => state.room?.local_user.await_add_resource(create_file_res()) }, tr("res.file")), ] chat_control = chat.set_state; return e("div", { class: "control-bar", role: "toolbar", - aria_label: PO.controls, + aria_label: tr("controls"), onkeydown: (_el, ev) => { if (ev.code == "ArrowLeft") { let n = document.activeElement?.previousElementSibling diff --git a/client-web/source/preferences/decl.ts b/client-web/source/preferences/decl.ts index 536c5c7..ab57f0e 100644 --- a/client-web/source/preferences/decl.ts +++ b/client-web/source/preferences/decl.ts @@ -3,9 +3,8 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2024 metamuffin */ -// there should be no deps to dom APIs in this file for the tablegen to work -import { LOCALES } from "../locale/mod.ts"; +import { LANGS } from "../locale.ts"; export function hex_id(len = 8): string { if (len > 8) return hex_id() + hex_id(len - 8) @@ -18,7 +17,7 @@ const optional = (a: T): T | undefined => a export const PREF_DECLS = { username: { type: string, default: "guest-" + hex_id(), allow_url: true }, - language: { type: string, possible_values: ["system", ...Object.keys(LOCALES)], default: "system", allow_url: true }, + language: { type: string, possible_values: LANGS, default: "system", allow_url: true }, /* MEDIA */ rnnoise: { type: bool, default: true, allow_url: true }, diff --git a/client-web/source/preferences/mod.ts b/client-web/source/preferences/mod.ts index bbf9bfb..b7e048f 100644 --- a/client-web/source/preferences/mod.ts +++ b/client-web/source/preferences/mod.ts @@ -6,7 +6,6 @@ import { log } from "../logger.ts"; import { PREF_DECLS } from "./decl.ts"; - export interface PrefDecl { default?: T, type: T, @@ -92,7 +91,7 @@ export function generate_section(): string { export function load_params(): { raw_params: { [key: string]: string }, rsecret: string } { const raw_params: Record = {} - const [rsecret, param_str] = decodeURIComponent(window.location.hash.substring(1)).split("?") + const [rsecret, param_str] = decodeURIComponent(globalThis.location.hash.substring(1)).split("?") if (!param_str) return { rsecret, raw_params: {} } for (const kv of param_str.split("&")) { const [key, value] = kv.split("=") diff --git a/client-web/source/preferences/ui.ts b/client-web/source/preferences/ui.ts index 0f26c14..a737e01 100644 --- a/client-web/source/preferences/ui.ts +++ b/client-web/source/preferences/ui.ts @@ -6,7 +6,7 @@ /// import { e } from "../helper.ts"; -import { PO } from "../locale/mod.ts"; +import { tr } from "../locale.ts"; import { PREF_DECLS } from "./decl.ts"; import { change_pref, on_pref_changed, PrefDecl, PREFS } from "./mod.ts"; @@ -87,24 +87,24 @@ export function ui_preferences(): HTMLElement { use_opt_ = use_opt; } - const label = e("label", { for: id }, PO.setting_descs[key] ?? `[${key}]`) + const label = e("label", { for: id }, tr(`config.${key}`)) return e("tr", { class: "pref" }, e("td", {}, label), e("td", {}, use_opt_ ?? ""), e("td", {}, prim_control ?? "")) }) const notification_perm = Notification.permission == "granted" ? e("div", {}) : e("div", {}, - e("span", {}, PO.notification_perm_explain), - e("button", { onclick: () => Notification.requestPermission() }, PO.grant), + e("span", {}, tr("config.notification.perm_explain")), + e("button", { onclick: () => Notification.requestPermission() }, tr("config.notification.grant")), ) const reset = e("div", {}, - e("span", {}, PO.clear_prefs), + e("span", {}, tr("config.clear_prefs")), e("button", { onclick: () => { if (confirm("really clear all preferences?")) { localStorage.clear(); globalThis.location.reload() } } }, "RESET"), ) const table = document.createElement("table") table.append(...rows) - return e("div", { class: "preferences", role: "dialog", aria_label: PO.settings }, - e("h2", {}, PO.settings), + return e("div", { class: "preferences", role: "dialog", aria_label: tr("config") }, + e("h2", {}, tr("config")), notification_perm, e("br", {}), table, e("br", {}), reset diff --git a/client-web/source/resource/file.ts b/client-web/source/resource/file.ts index 0554e2f..4bd92ed 100644 --- a/client-web/source/resource/file.ts +++ b/client-web/source/resource/file.ts @@ -10,7 +10,7 @@ import { log } from "../logger.ts"; import { StreamDownload } from "../download_stream.ts"; import { RemoteUser } from "../user/remote.ts"; import { LocalResource, ResourceHandlerDecl } from "./mod.ts"; -import { PO } from "../locale/mod.ts"; +import { tr } from "../locale.ts"; const MAX_CHUNK_SIZE = 1 << 15; @@ -20,14 +20,14 @@ export const resource_file: ResourceHandlerDecl = { const download_button = e("button", { onclick: self => { enable() - self.textContent = PO.downloading + self.textContent = tr("res.file.downloading") self.disabled = true } - }, PO.download) + }, tr("res.file.download")) return { info, el: e("div", {}, - e("span", {}, `${PO.file}: ${JSON.stringify(info.label)} (${display_filesize(info.size!)})`), + e("span", {}, `${tr("res.file")}: ${JSON.stringify(info.label)} (${display_filesize(info.size!)})`), download_button, ), on_statechange(_s) { }, @@ -38,7 +38,7 @@ export const resource_file: ResourceHandlerDecl = { this.el.appendChild(display.el) const reset = () => { download_button.disabled = false - download_button.textContent = PO.download_again + download_button.textContent = tr("res.file.download_again") this.el.removeChild(display.el) disable() } @@ -124,7 +124,7 @@ function file_res_inner(file: File): LocalResource { transfers_abort.forEach(abort => abort()) }, el: e("div", { class: "file" }, - e("button", { class: "abort", onclick(_) { destroy() } }, PO.stop_sharing), + e("button", { class: "abort", onclick(_) { destroy() } }, tr("res.stop_sharing")), e("span", {}, `Sharing file: ${JSON.stringify(file.name)}`), transfers_el ), @@ -137,16 +137,16 @@ function file_res_inner(file: File): LocalResource { log("dc", `${user.display_name} started transfer`); const display = transfer_status_el(user) transfers_el.appendChild(display.el) - display.status = PO.status_await_channel_open + display.status = tr("status_await_channel_open") let position = 0 const finish = async () => { channel.send("end") while (channel.bufferedAmount) { - display.status = PO.status_drain_buffer(channel.bufferedAmount) + display.status = tr("status.drain_buffer", { amount: channel.bufferedAmount.toString() }) await sleep(10) } - display.status = PO.status_await_channel_close + display.status = tr("status.await_channel_close") } const feed = async () => { const { value: chunk, done }: { value?: Uint8Array, done: boolean } = await reader.read() @@ -171,7 +171,7 @@ function file_res_inner(file: File): LocalResource { const abort_cb = () => { channel.close(); } channel.onbufferedamountlow = () => feed_until_full() channel.onopen = _ev => { - display.status = PO.status_buffering + display.status = tr("status.buffering") log("dc", `${user.display_name}: channel open`); feed_until_full() } @@ -179,7 +179,7 @@ function file_res_inner(file: File): LocalResource { log("dc", `${user.display_name}: channel error`); } channel.onclosing = _ev => { - display.status = PO.status_closing + display.status = tr("status.closing") } channel.onclose = _ev => { log("dc", `${user.display_name}: channel closed`); diff --git a/client-web/source/resource/track.ts b/client-web/source/resource/track.ts index e2af5e9..ac1e8a6 100644 --- a/client-web/source/resource/track.ts +++ b/client-web/source/resource/track.ts @@ -6,7 +6,7 @@ /// import { ProvideInfo } from "../../../common/packets.d.ts"; import { e } from "../helper.ts"; -import { PO } from "../locale/mod.ts"; +import { tr } from "../locale.ts"; import { log } from "../logger.ts"; import { on_pref_changed, PREFS } from "../preferences/mod.ts"; import { get_rnnoise_node } from "../rnnoise.ts"; @@ -20,12 +20,12 @@ export const resource_track: ResourceHandlerDecl = { if (preview_enabled) user.send_to({ preview_request: { id: info.id } }) let preview_request_timeout: number | undefined; - const enable_label = PO.enable(`"${info.label ?? info.kind}"`) + const enable_label = tr("res.enable", { name: `"${info.label ?? info.kind}"` }) const enable_button = e("button", { class: "center", onclick: self => { self.disabled = true; - self.textContent = PO.status_await_stream; + self.textContent = tr("status.await_stream"); enable() } }, enable_label) @@ -71,7 +71,7 @@ export const resource_track: ResourceHandlerDecl = { enable_button.textContent = enable_label; self.remove() } - }, PO.disable)) + }, tr("res.disable"))) create_track_display(this.el, stream, false) }, } @@ -81,7 +81,7 @@ export const resource_track: ResourceHandlerDecl = { export function new_local_track(info: ProvideInfo, stream: MediaStream, ...extra_controls: HTMLElement[]): LocalResource { let destroy: () => void; const el = e("div", { class: `media-${stream.getVideoTracks().length > 0 ? "video" : "audio"}` }, - e("button", { icon: "stop", class: ["abort", "topleft"], onclick: () => destroy() }, PO.stop_sharing), + e("button", { icon: "stop", class: ["abort", "topleft"], onclick: () => destroy() }, tr("res.stop_sharing")), ...extra_controls ); @@ -151,7 +151,7 @@ function create_track_display(target: HTMLElement, stream: MediaStream, local: b media_el.srcObject = stream media_el.autoplay = true media_el.controls = !is_video - media_el.ariaLabel = is_video ? PO.video_stream : PO.audio_stream + media_el.ariaLabel = is_video ? tr("res.video_stream") : tr("res.audio_stream") media_el.addEventListener("pause", () => media_el.play()) if (local) media_el.muted = true @@ -177,7 +177,7 @@ function create_track_display(target: HTMLElement, stream: MediaStream, local: b else media_el.requestFullscreen() } - }, PO.fullscreen) + }, tr("res.fullscreen")) target.prepend(fullscreen) } @@ -263,7 +263,7 @@ export async function create_mic_res() { const mute = document.createElement("input") mute.type = "checkbox" - const mute_label = e("label", { class: "check-button" }, PO.mute) + const mute_label = e("label", { class: "check-button" }, tr("res.microphone.mute")) mute_label.prepend(mute) const res = new_local_track({ id: destination.stream.id, kind: "track", track_kind: "audio", label: "Microphone" }, destination.stream, mute_label) diff --git a/client-web/source/room_watches.ts b/client-web/source/room_watches.ts index d738359..66d11fd 100644 --- a/client-web/source/room_watches.ts +++ b/client-web/source/room_watches.ts @@ -5,7 +5,7 @@ */ /// import { array_swap, e } from "./helper.ts"; -import { PO } from "./locale/mod.ts"; +import { tr } from "./locale.ts"; import { PREFS, change_pref } from "./preferences/mod.ts"; import { room_hash } from "./protocol/crypto.ts"; import { SignalingConnection } from "./protocol/mod.ts"; @@ -61,8 +61,8 @@ export function ui_room_watches(conn: SignalingConnection): HTMLElement { e("div", { class: "users" }, ...ucont), )) if (edit) el.append(e("button", { onclick(_) { watches = watches.filter(e => e != w); update_listing() } }, "X")) - if (edit && wi > 0) el.append(e("button", { onclick(_) { array_swap(watches, wi, wi - 1); update_listing() } }, PO.move_up)) - if (edit && wi < watches.length - 1) el.append(e("button", { onclick(_) { array_swap(watches, wi, wi + 1); update_listing() } }, PO.move_down)) + if (edit && wi > 0) el.append(e("button", { onclick(_) { array_swap(watches, wi, wi - 1); update_listing() } }, tr("room_watches.edit.move_up"))) + if (edit && wi < watches.length - 1) el.append(e("button", { onclick(_) { array_swap(watches, wi, wi + 1); update_listing() } }, tr("room_watches.edit.move_down"))) listing.append(el) } @@ -77,14 +77,14 @@ export function ui_room_watches(conn: SignalingConnection): HTMLElement { update_watches() input.value = "" } - }, PO.add), + }, tr("room_watches.edit.add")), e("button", { async onclick() { if (!conn.room) return await add_watch(conn.room) update_watches() } - }, PO.add_current_room) + }, tr("room_watches.edit.add_current_room")) )) } } @@ -98,7 +98,7 @@ export function ui_room_watches(conn: SignalingConnection): HTMLElement { edit = e; } return e("div", { class: "room-watches", role: "dialog", aria_label: "known rooms" }, - e("h2", {}, PO.known_rooms), + e("h2", {}, tr("room_watches")), listing, button_edit = e("button", { icon: "edit", @@ -106,7 +106,7 @@ export function ui_room_watches(conn: SignalingConnection): HTMLElement { set_edit(true) update_listing() } - }, PO.edit), + }, tr("room_watches.edit")), button_finish = e("button", { icon: "check", hidden: true, @@ -116,6 +116,6 @@ export function ui_room_watches(conn: SignalingConnection): HTMLElement { update_watches() update_listing() } - }, PO.finish_edit), + }, tr("room_watches.edit.finish")), ) } diff --git a/client-web/source/user/local.ts b/client-web/source/user/local.ts index 371efff..462edd3 100644 --- a/client-web/source/user/local.ts +++ b/client-web/source/user/local.ts @@ -13,7 +13,7 @@ import { User } from "./mod.ts"; import { create_camera_res, create_mic_res, create_screencast_res } from "../resource/track.ts"; import { LocalResource } from "../resource/mod.ts"; import { PREFS } from "../preferences/mod.ts"; -import { PO } from "../locale/mod.ts"; +import { tr } from "../locale.ts"; export class LocalUser extends User { resources: Map = new Map() @@ -21,7 +21,7 @@ export class LocalUser extends User { constructor(room: Room, id: number) { super(room, id) this.el.classList.add("local") - this.status_el.textContent = PO.local + this.status_el.textContent = tr("res.local") this.name = PREFS.username log("users", `added local user: ${this.display_name}`) this.add_initial_tracks() diff --git a/client-web/source/user/mod.ts b/client-web/source/user/mod.ts index daf3772..90a45b8 100644 --- a/client-web/source/user/mod.ts +++ b/client-web/source/user/mod.ts @@ -6,19 +6,19 @@ /// import { e } from "../helper.ts"; -import { PO } from "../locale/mod.ts"; +import { tr } from "../locale.ts"; import { Room } from "../room.ts"; export class User { private _name?: string set name(v: string | undefined) { this._name = v; this.name_el.textContent = this.display_name; this.el.ariaLabel = "user " + this.display_name } get name() { return this._name } - get display_name() { return this.name ?? PO.unknown_user } + get display_name() { return this.name ?? tr("unknown_user") } name_el = e("span", {}, this.display_name) status_el = e("span", { class: ["connection-status", "status-neutral"] }, "") stats_el = e("pre", {}) - el = e("div", { class: "user", role: "group", aria_label: PO.unknown_user, aria_live: "polite" }) + el = e("div", { class: "user", role: "group", aria_label: tr("unknown_user"), aria_live: "polite" }) constructor(public room: Room, public id: number) { const info_el = e("div", { class: "info" }) diff --git a/client-web/source/user/remote.ts b/client-web/source/user/remote.ts index aa5b1b4..b247511 100644 --- a/client-web/source/user/remote.ts +++ b/client-web/source/user/remote.ts @@ -7,7 +7,7 @@ import { RelayMessage } from "../../../common/packets.d.ts"; import { notify } from "../helper.ts"; -import { PO } from "../locale/mod.ts"; +import { tr } from "../locale.ts"; import { log } from "../logger.ts" import { PREFS } from "../preferences/mod.ts"; import { new_remote_resource, RemoteResource } from "../resource/mod.ts"; @@ -79,7 +79,7 @@ export class RemoteUser extends User { this.pc.close() this.room.remote_users.delete(this.id) this.room.element.removeChild(this.el) - if (PREFS.notify_leave) notify(PO.leave_message(this.display_name).join("")) + if (PREFS.notify_leave) notify(tr("chat.leave_message", { name: this.display_name })) this.room.chat.add_control_message({ leave: this }) } on_relay(message: RelayMessage) { @@ -89,7 +89,7 @@ export class RemoteUser extends User { if (message.answer) this.on_answer(message.answer) if (message.identify) { this.name = message.identify.username - if (PREFS.notify_join) notify(PO.join_message(this.display_name).join("")) + if (PREFS.notify_join) notify(tr("chat.join_message", { name: this.display_name })) this.room.chat.add_control_message({ join: this }) } if (message.preview_response) { @@ -187,13 +187,13 @@ export class RemoteUser extends User { async update_status() { const states: { [key in RTCIceConnectionState]: [string, string] } = { - new: [PO.status_no_conn, "neutral"], - checking: [PO.status_checking, "neutral"], - failed: [PO.status_failed, "fail"], - closed: [PO.status_disconnected, "neutral"], - completed: [PO.status_connected, "good"], - connected: [PO.status_connected, "good"], - disconnected: [PO.status_disconnected, "neutral"] + new: [tr("status.no_conn"), "neutral"], + checking: [tr("status.checking"), "neutral"], + failed: [tr("status.failed"), "fail"], + closed: [tr("status.disconnected"), "neutral"], + completed: [tr("status.connected"), "good"], + connected: [tr("status.connected"), "good"], + disconnected: [tr("status.disconnected"), "neutral"] } this.status_el.classList.value = "" this.status_el.classList.add("connection-status", "status-" + states[this.pc.iceConnectionState][1]) diff --git a/server/src/assets.rs b/server/src/assets.rs index cf6ccb1..7195fbf 100644 --- a/server/src/assets.rs +++ b/server/src/assets.rs @@ -15,8 +15,8 @@ macro_rules! s_file { #[cfg(debug_assertions)] #[macro_export] macro_rules! s_asset_dir { - () => { - warp::fs::dir("../client-web/public/assets") + ($path: literal) => { + warp::fs::dir(concat!("../", $path)) }; } @@ -37,9 +37,9 @@ macro_rules! s_file { #[cfg(not(debug_assertions))] #[macro_export] macro_rules! s_asset_dir { - () => {{ + ($path:literal) => {{ use include_dir::{include_dir, Dir}; - const DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/../client-web/public/assets"); + const DIR: Dir = include_dir!(concat!("$CARGO_MANIFEST_DIR/../", $path)); warp::path::tail().and_then(|t: warp::path::Tail| async move { let path = t.as_str(); let content_type = match &path { @@ -47,6 +47,7 @@ macro_rules! s_asset_dir { _ if path.ends_with(".js") => "application/javascript", _ if path.ends_with(".css") => "text/css", _ if path.ends_with(".svg") => "image/svg+xml", + _ if path.ends_with(".ini") => "text/plain", _ => "application/octet-stream", }; DIR.get_file(path) diff --git a/server/src/main.rs b/server/src/main.rs index 92f9451..e35dd01 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -63,7 +63,8 @@ async fn run() { let favicon: _ = warp::path("favicon.ico").and(s_file!("client-web/public/favicon.ico", "image/avif")); let room: _ = warp::path("room").and(s_file!("client-web/public/app.html", "text/html")); - let assets: _ = warp::path("assets").and(s_asset_dir!()); + let locale: _ = warp::path("locale").and(s_asset_dir!("client-web/locale")); + let assets: _ = warp::path("assets").and(s_asset_dir!("client-web/public/assets")); let sw_script: _ = warp::path("sw.js").and(s_file!( "client-web/public/assets/sw.js", "application/javascript" @@ -94,6 +95,7 @@ async fn run() { .or(client_config) .or(version) .or(assets) + .or(locale) .or(favicon) .or(sw_script) .or(old_format_redirect) -- cgit v1.2.3-70-g09d2