aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--client-web/locale/de.ini91
-rw-r--r--client-web/locale/en.ini91
-rw-r--r--client-web/scripts/apply_translations.ts0
-rw-r--r--client-web/scripts/find_missing_translations.ts29
-rw-r--r--client-web/scripts/gen_param_table.ts27
-rw-r--r--client-web/scripts/reformat_json.ts16
-rw-r--r--client-web/scripts/translate_argos.py31
-rw-r--r--client-web/source/chat.ts18
-rw-r--r--client-web/source/download_stream.ts4
-rw-r--r--client-web/source/helper.ts4
-rw-r--r--client-web/source/index.ts20
-rw-r--r--client-web/source/keybinds.ts4
-rw-r--r--client-web/source/locale.ts29
-rw-r--r--client-web/source/locale/de.ts101
-rw-r--r--client-web/source/locale/en.ts101
-rw-r--r--client-web/source/locale/es.ts102
-rw-r--r--client-web/source/locale/ja.ts102
-rw-r--r--client-web/source/locale/mod.ts89
-rw-r--r--client-web/source/menu.ts26
-rw-r--r--client-web/source/preferences/decl.ts5
-rw-r--r--client-web/source/preferences/mod.ts3
-rw-r--r--client-web/source/preferences/ui.ts14
-rw-r--r--client-web/source/resource/file.ts22
-rw-r--r--client-web/source/resource/track.ts16
-rw-r--r--client-web/source/room_watches.ts16
-rw-r--r--client-web/source/user/local.ts4
-rw-r--r--client-web/source/user/mod.ts6
-rw-r--r--client-web/source/user/remote.ts20
-rw-r--r--server/src/assets.rs9
-rw-r--r--server/src/main.rs4
30 files changed, 309 insertions, 695 deletions
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
--- a/client-web/scripts/apply_translations.ts
+++ /dev/null
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
-/// <reference lib="deno.worker" />
-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 <metamuffin.org>
-*/
-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<string, PrefDecl<unknown>>
-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 <metamuffin.org>
*/
/// <reference lib="dom" />
-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 @@
*/
/// <reference lib="dom" />
-import { PO } from "./locale/mod.ts";
+import { tr } from "./locale.ts";
import { PREFS } from "./preferences/mod.ts";
interface Opts<E> {
@@ -64,7 +64,7 @@ export function image_view(url: string, opts?: Opts<HTMLElement>): 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 @@
/// <reference lib="dom" />
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 <metamuffin.org>
-*/
-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 <metamuffin.org>
-*/
-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 <metamuffin.org>
-*/
-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 <metamuffin.org>
-*/
-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 <metamuffin.org>
*/
-// 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 = <T>(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<T> {
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<string, string> = {}
- 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 @@
/// <reference lib="dom" />
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 @@
/// <reference lib="dom" />
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 @@
*/
/// <reference lib="dom" />
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<string, LocalResource> = 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 @@
/// <reference lib="dom" />
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)