diff --git a/src/components/ui/BouncerDisconnectConfirmModal.tsx b/src/components/ui/BouncerDisconnectConfirmModal.tsx
new file mode 100644
index 00000000..665f7166
--- /dev/null
+++ b/src/components/ui/BouncerDisconnectConfirmModal.tsx
@@ -0,0 +1,130 @@
+import { Trans, useLingui } from "@lingui/react/macro";
+import type React from "react";
+import { useMemo } from "react";
+import { createPortal } from "react-dom";
+import { FaCrown, FaPlug, FaTimes } from "react-icons/fa";
+import { GiGlassShot } from "react-icons/gi";
+import useStore from "../../store";
+import type { Server } from "../../types";
+
+export const BouncerDisconnectConfirmModal: React.FC = () => {
+ const { t } = useLingui();
+ const targetId = useStore((s) => s.ui.disconnectConfirmTarget);
+ const servers = useStore((s) => s.servers);
+ const cancelDeleteServer = useStore((s) => s.cancelDeleteServer);
+ const deleteServer = useStore((s) => s.deleteServer);
+
+ const { parent, children } = useMemo(() => {
+ if (!targetId) return { parent: null as Server | null, children: [] };
+ const p = servers.find((s) => s.id === targetId) ?? null;
+ const c = servers.filter((s) => s.bouncerServerId === targetId);
+ return { parent: p, children: c };
+ }, [targetId, servers]);
+
+ if (!targetId || !parent) return null;
+
+ const confirm = () => deleteServer(targetId);
+
+ return createPortal(
+
+
e.stopPropagation()}
+ >
+
+
+
+
+ Disconnect from soju bouncer?
+
+
+
+
+
+
+
+ You're connected to{" "}
+ {parent.name}.
+
+
+ {children.length > 0 && (
+ <>
+
+ {children.length === 1 ? (
+ This will also close the bound network below.
+ ) : (
+
+ This will also close the {children.length} bound networks
+ below.
+
+ )}
+
+
+ {children.map((child) => (
+
+ ))}
+
+ >
+ )}
+
+
+
+
+
+
+
+
,
+ document.body,
+ );
+};
+
+const NetworkCard: React.FC<{ server: Server }> = ({ server }) => {
+ const label = server.networkName || server.name;
+ return (
+
+
+
+ {label.charAt(0).toUpperCase()}
+
+
+
+
+
+
+
+
{label}
+ {server.host && (
+
+ {server.host}
+
+ )}
+
+
+ );
+};
+
+export default BouncerDisconnectConfirmModal;
diff --git a/src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx b/src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
new file mode 100644
index 00000000..1f3010af
--- /dev/null
+++ b/src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
@@ -0,0 +1,96 @@
+import { Trans, useLingui } from "@lingui/react/macro";
+import type React from "react";
+import { useMemo } from "react";
+import { createPortal } from "react-dom";
+import { FaPlug, FaTimes } from "react-icons/fa";
+import { GiGlassShot } from "react-icons/gi";
+import useStore from "../../store";
+
+export const BouncerNetworkDisconnectConfirmModal: React.FC = () => {
+ const { t } = useLingui();
+ const target = useStore((s) => s.ui.disconnectNetworkConfirmTarget);
+ const servers = useStore((s) => s.servers);
+ const bouncers = useStore((s) => s.bouncers);
+ const cancel = useStore((s) => s.cancelDisconnectNetwork);
+ const confirm = useStore((s) => s.confirmDisconnectNetwork);
+
+ const network = useMemo(() => {
+ if (!target) return null;
+ const child = servers.find((s) => s.id === target.childServerId);
+ const attrs =
+ bouncers[target.bouncerServerId]?.networks[target.netid]?.attributes;
+ const name =
+ attrs?.name || child?.networkName || child?.name || `#${target.netid}`;
+ const host = attrs?.host || child?.host || "";
+ return { name, host };
+ }, [target, servers, bouncers]);
+
+ if (!target || !network) return null;
+
+ return createPortal(
+
+
e.stopPropagation()}
+ >
+
+
+
+
+ Disconnect network?
+
+
+
+
+
+
+
+ Disconnect {network.name}?
+
+
+ {network.host && (
+
{network.host}
+ )}
+
+
+ This removes the network from your soju bouncer. To use it again,
+ you'll need to add it back.
+
+
+
+
+
+
+
+
+
+
,
+ document.body,
+ );
+};
+
+export default BouncerNetworkDisconnectConfirmModal;
diff --git a/src/components/ui/BouncerNetworkForm.tsx b/src/components/ui/BouncerNetworkForm.tsx
new file mode 100644
index 00000000..182b417a
--- /dev/null
+++ b/src/components/ui/BouncerNetworkForm.tsx
@@ -0,0 +1,324 @@
+import { Trans, useLingui } from "@lingui/react/macro";
+import type React from "react";
+import { useEffect, useMemo, useState } from "react";
+import { FaCheck, FaTimes, FaTrash } from "react-icons/fa";
+import { BOUNCER_READ_ONLY_ATTRIBUTES } from "../../lib/bouncerAttrs";
+import { TextInput } from "./TextInput";
+
+export interface BouncerFormValues {
+ name: string;
+ host: string;
+ port: string;
+ tls: boolean;
+ nickname: string;
+ username: string;
+ realname: string;
+ pass: string;
+}
+
+const EMPTY: BouncerFormValues = {
+ name: "",
+ host: "",
+ port: "",
+ tls: true,
+ nickname: "",
+ username: "",
+ realname: "",
+ pass: "",
+};
+
+export function attrsToValues(
+ attrs: Record
,
+): BouncerFormValues {
+ return {
+ name: attrs.name ?? "",
+ host: attrs.host ?? "",
+ port: attrs.port ?? "",
+ tls: attrs.tls !== "0", // default: enabled
+ nickname: attrs.nickname ?? "",
+ username: attrs.username ?? "",
+ realname: attrs.realname ?? "",
+ pass: attrs.pass ?? "",
+ };
+}
+
+// Reduce form state down to only attributes that differ from the
+// originals. Empty strings are sent so server can clear values.
+export function valuesToAttrs(
+ values: BouncerFormValues,
+ original?: Record,
+): Record {
+ const out: Record = {};
+ const setIfChanged = (key: keyof BouncerFormValues, wireKey: string) => {
+ if (BOUNCER_READ_ONLY_ATTRIBUTES.has(wireKey)) return;
+ const v = values[key];
+ const cur = typeof v === "boolean" ? (v ? "1" : "0") : v;
+ if (original) {
+ const origCur = original[wireKey] ?? "";
+ const norm = wireKey === "tls" && origCur === "" ? "1" : origCur;
+ if (norm === cur) return;
+ }
+ out[wireKey] = cur;
+ };
+ setIfChanged("name", "name");
+ setIfChanged("host", "host");
+ setIfChanged("port", "port");
+ setIfChanged("tls", "tls");
+ setIfChanged("nickname", "nickname");
+ setIfChanged("username", "username");
+ setIfChanged("realname", "realname");
+ setIfChanged("pass", "pass");
+ return out;
+}
+
+interface BouncerNetworkFormProps {
+ initial?: Record;
+ errorAttribute?: string;
+ errorMessage?: string;
+ isSaving?: boolean;
+ isDeleting?: boolean;
+ onSave: (attrs: Record) => void;
+ onDelete?: () => void;
+ onCancel: () => void;
+}
+
+const Field: React.FC<{
+ label: React.ReactNode;
+ error?: string;
+ children: React.ReactNode;
+ span?: 1 | 2;
+}> = ({ label, error, children, span = 1 }) => (
+
+);
+
+const inputClass = (hasError: boolean) =>
+ `w-full px-2.5 py-1.5 rounded bg-discord-dark-400 text-discord-text-normal text-sm outline-none transition-colors border ${
+ hasError
+ ? "border-red-500 focus:border-red-400"
+ : "border-transparent focus:border-primary"
+ }`;
+
+export const BouncerNetworkForm: React.FC = ({
+ initial,
+ errorAttribute,
+ errorMessage,
+ isSaving,
+ isDeleting,
+ onSave,
+ onDelete,
+ onCancel,
+}) => {
+ const { t } = useLingui();
+ const [values, setValues] = useState(() =>
+ initial ? attrsToValues(initial) : EMPTY,
+ );
+ const [confirmDelete, setConfirmDelete] = useState(false);
+ useEffect(() => {
+ setValues(initial ? attrsToValues(initial) : EMPTY);
+ setConfirmDelete(false);
+ }, [initial]);
+
+ const isEdit = !!initial;
+ const canSave = useMemo(() => {
+ if (!values.host.trim()) return false;
+ if (isEdit) {
+ const diff = valuesToAttrs(values, initial);
+ return Object.keys(diff).length > 0;
+ }
+ return true;
+ }, [values, initial, isEdit]);
+
+ const submit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!canSave || isSaving) return;
+ onSave(valuesToAttrs(values, initial));
+ };
+
+ const fieldError = (attr: string) =>
+ errorAttribute === attr ? errorMessage : undefined;
+
+ return (
+
+ );
+};
diff --git a/src/components/ui/BouncerNetworksPanel.tsx b/src/components/ui/BouncerNetworksPanel.tsx
new file mode 100644
index 00000000..92b31eb9
--- /dev/null
+++ b/src/components/ui/BouncerNetworksPanel.tsx
@@ -0,0 +1,393 @@
+import { Trans, useLingui } from "@lingui/react/macro";
+import type React from "react";
+import { useEffect, useMemo, useState } from "react";
+import {
+ FaArrowLeft,
+ FaArrowRight,
+ FaExclamationCircle,
+ FaLayerGroup,
+ FaPencilAlt,
+ FaPlay,
+ FaPlus,
+ FaStop,
+} from "react-icons/fa";
+import { v5 as uuidv5 } from "uuid";
+import useStore from "../../store";
+import type { BouncerNetwork } from "../../types";
+import { BouncerNetworkForm } from "./BouncerNetworkForm";
+
+interface BouncerNetworksPanelProps {
+ bouncerServerId: string;
+}
+
+type Mode =
+ | { kind: "list" }
+ | { kind: "add" }
+ | { kind: "edit"; netid: string };
+
+const STATE_COPY: Record<
+ string,
+ { label: string; dotClass: string; pulse?: boolean }
+> = {
+ connected: { label: "Connected", dotClass: "bg-green-400" },
+ connecting: { label: "Connecting…", dotClass: "bg-yellow-400", pulse: true },
+ disconnected: { label: "Disconnected", dotClass: "bg-discord-text-muted" },
+};
+
+// Must mirror CHANNEL_NAMESPACE in src/store/index.ts. The bouncerConnectNetwork
+// action derives child server ids by hashing (parentId, netid) under this
+// namespace; we recompute it here so the row can tell whether a child binding
+// already exists.
+const CHILD_NAMESPACE = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
+
+function rankNetwork(n: BouncerNetwork): number {
+ const s = n.attributes.state;
+ if (s === "connected") return 0;
+ if (s === "connecting") return 1;
+ if (s === "disconnected") return 2;
+ return 3;
+}
+
+export const BouncerNetworksPanel: React.FC = ({
+ bouncerServerId,
+}) => {
+ const { t } = useLingui();
+ const bouncer = useStore((s) => s.bouncers[bouncerServerId]);
+ const server = useStore((s) =>
+ s.servers.find((srv) => srv.id === bouncerServerId),
+ );
+ const servers = useStore((s) => s.servers);
+ const bouncerAddNetwork = useStore((s) => s.bouncerAddNetwork);
+ const bouncerChangeNetwork = useStore((s) => s.bouncerChangeNetwork);
+ const bouncerDelNetwork = useStore((s) => s.bouncerDelNetwork);
+ const bouncerConnectNetwork = useStore((s) => s.bouncerConnectNetwork);
+ const requestDeleteServer = useStore((s) => s.requestDeleteServer);
+ const bouncerListNetworks = useStore((s) => s.bouncerListNetworks);
+ const selectServer = useStore((s) => s.selectServer);
+ const pendingBouncerEdit = useStore((s) => s.ui.pendingBouncerEdit);
+ const consumePendingBouncerEdit = useStore(
+ (s) => s.consumePendingBouncerEdit,
+ );
+
+ const [mode, setMode] = useState({ kind: "list" });
+ const [pendingFor, setPendingFor] = useState(null);
+ const [confirmedSuccessFor, setConfirmedSuccessFor] = useState(
+ null,
+ );
+
+ const networks = useMemo(() => {
+ if (!bouncer) return [];
+ return Object.values(bouncer.networks).sort((a, b) => {
+ const ra = rankNetwork(a);
+ const rb = rankNetwork(b);
+ if (ra !== rb) return ra - rb;
+ const na = a.attributes.name || a.netid;
+ const nb = b.attributes.name || b.netid;
+ return na.localeCompare(nb);
+ });
+ }, [bouncer]);
+
+ // Briefly highlight a row after its ADD/CHANGE/DEL has been acked.
+ useEffect(() => {
+ if (!confirmedSuccessFor) return;
+ const t = setTimeout(() => setConfirmedSuccessFor(null), 1400);
+ return () => clearTimeout(t);
+ }, [confirmedSuccessFor]);
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies: store actions have unstable refs
+ useEffect(() => {
+ if (!pendingBouncerEdit) return;
+ if (pendingBouncerEdit.bouncerServerId !== bouncerServerId) return;
+ setMode({ kind: "edit", netid: pendingBouncerEdit.netid });
+ consumePendingBouncerEdit();
+ }, [pendingBouncerEdit, bouncerServerId]);
+
+ // Close the inline form optimistically after 500ms with no FAIL;
+ // also LISTNETWORKS to cover deployments without bouncer-networks-notify.
+ // biome-ignore lint/correctness/useExhaustiveDependencies: store actions have unstable refs
+ useEffect(() => {
+ if (!bouncer || !pendingFor) return;
+ const timer = setTimeout(() => {
+ if (!bouncer.lastError) {
+ if (pendingFor !== "*") setConfirmedSuccessFor(pendingFor);
+ setMode({ kind: "list" });
+ setPendingFor(null);
+ bouncerListNetworks(bouncerServerId);
+ }
+ }, 500);
+ return () => clearTimeout(timer);
+ }, [bouncer, pendingFor, bouncerServerId]);
+
+ const lastError = bouncer?.lastError;
+ const errorForForm = useMemo(() => {
+ if (!lastError || !pendingFor) return undefined;
+ if (pendingFor === "*" && lastError.subcommand !== "ADDNETWORK")
+ return undefined;
+ if (
+ pendingFor !== "*" &&
+ lastError.netid &&
+ lastError.netid !== pendingFor &&
+ lastError.netid !== "*"
+ )
+ return undefined;
+ return lastError;
+ }, [lastError, pendingFor]);
+
+ const onSubmitAdd = (attrs: Record) => {
+ setPendingFor("*");
+ bouncerAddNetwork(bouncerServerId, attrs);
+ };
+ const onSubmitEdit = (netid: string, attrs: Record) => {
+ setPendingFor(netid);
+ bouncerChangeNetwork(bouncerServerId, netid, attrs);
+ };
+ const onDelete = (netid: string) => {
+ setPendingFor(netid);
+ bouncerDelNetwork(bouncerServerId, netid);
+ };
+ const onConnectOrOpen = async (netid: string) => {
+ const childId = uuidv5(`${bouncerServerId}:${netid}`, CHILD_NAMESPACE);
+ const existing = servers.find((s) => s.id === childId);
+ if (existing) {
+ selectServer(childId, { clearSelection: true });
+ return;
+ }
+ const result = await bouncerConnectNetwork(bouncerServerId, netid);
+ // bouncerConnectNetwork seeds an in-memory Server with the computed
+ // childId before resolving, so this select call lands on the new row.
+ selectServer(childId, { clearSelection: true });
+ return result;
+ };
+
+ const editingNetwork =
+ mode.kind === "edit" ? bouncer?.networks[mode.netid] : undefined;
+
+ return (
+
+
+
+
+ {mode.kind === "list" && (
+
+ {!bouncer?.listed && networks.length === 0 ? (
+
+
+
+ Loading networks from your bouncer…
+
+
+ ) : networks.length === 0 ? (
+
+
+
+
+ Your bouncer doesn't have any networks yet. Add one to get
+ started.
+
+
+
+
+ ) : (
+
+ {networks.map((net) => {
+ const stateKey = net.attributes.state ?? "disconnected";
+ const visual =
+ STATE_COPY[stateKey] ?? STATE_COPY.disconnected;
+ const isHighlighted = confirmedSuccessFor === net.netid;
+ const childId = uuidv5(
+ `${bouncerServerId}:${net.netid}`,
+ CHILD_NAMESPACE,
+ );
+ const childServer = servers.find((s) => s.id === childId);
+ const childOpen = !!childServer;
+ return (
+ -
+
+
+ {visual.pulse && (
+
+ )}
+
+
+
+
+ {net.attributes.name || net.netid}
+
+
+ {visual.label}
+
+
+
+ {net.attributes.host || no host set}
+ {net.attributes.port ? `:${net.attributes.port}` : ""}
+ {net.attributes.nickname
+ ? ` · ${net.attributes.nickname}`
+ : ""}
+
+ {net.attributes.error && (
+
+
+
+ {net.attributes.error}
+
+
+ )}
+
+
+
+ {childServer && (
+
+ )}
+
+ );
+ })}
+
+ )}
+
+ )}
+
+ {mode.kind === "add" && (
+
{
+ setMode({ kind: "list" });
+ setPendingFor(null);
+ }}
+ />
+ )}
+
+ {mode.kind === "edit" && editingNetwork && (
+ onSubmitEdit(editingNetwork.netid, attrs)}
+ onDelete={() => onDelete(editingNetwork.netid)}
+ onCancel={() => {
+ setMode({ kind: "list" });
+ setPendingFor(null);
+ }}
+ />
+ )}
+
+
+ );
+};
+
+export default BouncerNetworksPanel;
diff --git a/src/components/ui/LinkSecurityWarningModal.tsx b/src/components/ui/LinkSecurityWarningModal.tsx
index 3240d6dd..b70ae9b6 100644
--- a/src/components/ui/LinkSecurityWarningModal.tsx
+++ b/src/components/ui/LinkSecurityWarningModal.tsx
@@ -122,7 +122,7 @@ const SingleWarningModal: React.FC = ({
removeWarning();
// Resume connection by sending CAP END
- ircClient.sendRaw(serverId, "CAP END");
+ ircClient.sendCapEnd(serverId);
ircClient.userOnConnect(serverId);
};
diff --git a/src/lib/bouncerAttrs.ts b/src/lib/bouncerAttrs.ts
new file mode 100644
index 00000000..bab6f6df
--- /dev/null
+++ b/src/lib/bouncerAttrs.ts
@@ -0,0 +1,86 @@
+// soju.im/bouncer-networks encodes per-network attributes as a single
+// semicolon-separated token of `key=value` pairs. Values escape with the
+// IRCv3 message-tag rules (\: ; \s SPACE \\ \\ \r CR \n LF), keys are plain.
+//
+// We keep this codec isolated so the IRC handler stays a thin dispatcher
+// and the store/UI can serialise their own ADDNETWORK / CHANGENETWORK
+// payloads via the same primitives.
+
+export function escapeBouncerValue(value: string): string {
+ return value
+ .replace(/\\/g, "\\\\")
+ .replace(/;/g, "\\:")
+ .replace(/ /g, "\\s")
+ .replace(/\r/g, "\\r")
+ .replace(/\n/g, "\\n");
+}
+
+export function unescapeBouncerValue(value: string): string {
+ let out = "";
+ for (let i = 0; i < value.length; i++) {
+ if (value[i] === "\\" && i + 1 < value.length) {
+ const c = value[i + 1];
+ if (c === "\\") out += "\\";
+ else if (c === ":") out += ";";
+ else if (c === "s") out += " ";
+ else if (c === "r") out += "\r";
+ else if (c === "n") out += "\n";
+ // Unknown escape: pass through the second char verbatim per the
+ // permissive interpretation in the message-tag spec.
+ else out += c;
+ i++;
+ } else {
+ out += value[i];
+ }
+ }
+ return out;
+}
+
+export function encodeBouncerAttrs(attrs: Record): string {
+ return Object.entries(attrs)
+ .map(([k, v]) => (v === "" ? k : `${k}=${escapeBouncerValue(v)}`))
+ .join(";");
+}
+
+// An attribute with no `=` is present-with-no-value (e.g. `state` cleared
+// in a notify); we represent that as an empty-string value to keep the
+// returned record's shape stable. Per the spec, an attribute appearing
+// without a value in a notify means it was removed -- callers handle
+// that distinction at the diff layer.
+export function decodeBouncerAttrs(token: string): Record {
+ const out: Record = {};
+ if (!token) return out;
+ for (const part of token.split(";")) {
+ if (!part) continue;
+ const eq = part.indexOf("=");
+ if (eq === -1) {
+ out[part] = "";
+ } else {
+ const k = part.slice(0, eq);
+ const v = unescapeBouncerValue(part.slice(eq + 1));
+ out[k] = v;
+ }
+ }
+ return out;
+}
+
+// Whether a given attribute name is read-only per spec. Clients should
+// not send these in ADDNETWORK / CHANGENETWORK; servers return a
+// READ_ONLY_ATTRIBUTE FAIL if they do.
+export const BOUNCER_READ_ONLY_ATTRIBUTES = new Set(["state", "error"]);
+
+// Attributes the spec defines explicitly. Anything else is implementation
+// defined (still valid wire data, but our UI hides them unless told to
+// surface them).
+export const BOUNCER_STANDARD_ATTRIBUTES = [
+ "name",
+ "state",
+ "host",
+ "port",
+ "tls",
+ "nickname",
+ "username",
+ "realname",
+ "pass",
+ "error",
+] as const;
diff --git a/src/lib/irc/IRCClient.ts b/src/lib/irc/IRCClient.ts
index 68cb98d5..50f3e0ca 100644
--- a/src/lib/irc/IRCClient.ts
+++ b/src/lib/irc/IRCClient.ts
@@ -111,6 +111,24 @@ export interface EventMap {
METADATA_UNSUBOK: BaseIRCEvent & { keys: string[] };
METADATA_SUBS: BaseIRCEvent & { keys: string[] };
METADATA_SYNCLATER: BaseIRCEvent & { target: string; retryAfter?: number };
+ // soju.im/bouncer-networks
+ BOUNCER_NETWORK: BaseIRCEvent & {
+ netid: string;
+ deleted: boolean;
+ attributes: Record;
+ batchTag?: string;
+ };
+ BOUNCER_ADDNETWORK_OK: BaseIRCEvent & { netid: string };
+ BOUNCER_CHANGENETWORK_OK: BaseIRCEvent & { netid: string };
+ BOUNCER_DELNETWORK_OK: BaseIRCEvent & { netid: string };
+ BOUNCER_FAIL: BaseIRCEvent & {
+ code: string;
+ subcommand: string;
+ netid?: string;
+ attribute?: string;
+ context: string[];
+ description: string;
+ };
BATCH_START: BaseIRCEvent & {
batchId: string;
type: string;
@@ -551,6 +569,11 @@ export class IRCClient implements IRCClientContext {
new Map();
private pendingConnections: Map> = new Map();
private pendingCapReqs: Map = new Map(); // Track how many CAP REQ batches are pending ACK
+ // soju.im/bouncer-networks BIND: when a serverId is mapped here, the
+ // next CAP END for that server is preceded by a `BOUNCER BIND `
+ // line so the connection lands inside the named upstream network's
+ // session rather than the bouncer's control channel.
+ private pendingBouncerBind: Map = new Map();
capNegotiationComplete: Map = new Map(); // Track if CAP negotiation is complete
private reconnectionAttempts: Map = new Map(); // Track reconnection attempts per server
reconnectionTimeouts: Map = new Map(); // Track reconnection timeouts per server
@@ -635,6 +658,11 @@ export class IRCClient implements IRCClientContext {
"labeled-response",
"draft/read-marker",
"obsidianirc/cmdslist",
+ // soju.im/bouncer-networks: multi-network bouncer discovery. The
+ // -notify variant gives us live add/change/del pushes without
+ // polling LISTNETWORKS.
+ "soju.im/bouncer-networks",
+ "soju.im/bouncer-networks-notify",
// obbyircd vendor cap. Without REQ'ing it the server won't emit
// the INVITELINK protocol even if it advertises support in CAP LS.
"obby.world/invitation",
@@ -658,7 +686,11 @@ export class IRCClient implements IRCClientContext {
serverId?: string,
oauthBearerEnabled?: boolean,
): Promise {
- const connectionKey = `${host}:${port}`;
+ // Bouncer child connections share host:port with the control
+ // connection (and with each other). Scope the pending-connection
+ // dedup by serverId when one is provided so each child gets its
+ // own promise rather than landing on a sibling's.
+ const connectionKey = serverId ?? `${host}:${port}`;
// Check if there's already a pending connection to this server
const existingConnection = this.pendingConnections.get(connectionKey);
@@ -1198,8 +1230,12 @@ export class IRCClient implements IRCClientContext {
this.sendRaw(serverId, `JOIN ${channelName}`);
- // Only request CHATHISTORY if the server supports it
- if (server.capabilities?.includes("draft/chathistory")) {
+ // soju bouncer control sessions have no real channels — treat as cap-less.
+ const wantsChathistory =
+ !server.isBouncerControl &&
+ !!server.capabilities?.includes("draft/chathistory");
+
+ if (wantsChathistory) {
this.sendRaw(serverId, `CHATHISTORY LATEST ${channelName} * 50`);
}
@@ -1213,23 +1249,23 @@ export class IRCClient implements IRCClientContext {
isMentioned: false,
messages: [],
users: [],
- isLoadingHistory: !!server.capabilities?.includes("draft/chathistory"), // Only loading if we requested history
- hasMoreHistory: !!server.capabilities?.includes("draft/chathistory"), // Assume there's history until proven otherwise
- needsWhoRequest: true, // Need to request WHO after CHATHISTORY completes (or immediately if no CHATHISTORY)
- chathistoryRequested:
- !!server.capabilities?.includes("draft/chathistory"), // Mark that we've requested CHATHISTORY only if supported
+ isLoadingHistory: wantsChathistory,
+ hasMoreHistory: wantsChathistory,
+ needsWhoRequest: true,
+ chathistoryRequested: wantsChathistory,
};
server.channels.push(channel);
- if (server.capabilities?.includes("draft/chathistory")) {
+ if (wantsChathistory) {
this.triggerEvent("CHATHISTORY_LOADING", {
serverId,
channelName,
isLoading: true,
});
} else {
- // No CHATHISTORY support, so the LOADING(false) callback that
- // normally fires WHO will never run. Send it now.
+ // No CHATHISTORY support (or it's the bouncer control session) --
+ // the LOADING(false) callback that normally fires WHO will never
+ // run. Send it now.
this.sendRaw(serverId, `WHO ${channelName} %cuhnfaro`);
channel.needsWhoRequest = false;
}
@@ -1246,6 +1282,7 @@ export class IRCClient implements IRCClientContext {
): void {
const server = this.servers.get(serverId);
if (!server?.capabilities?.includes("draft/chathistory")) return;
+ if (server.isBouncerControl) return;
// Fire isLoading:true so the store sets isLoadingHistory=true for the whole batch.
// The React component uses isLoadingMore to keep messages in DOM (no full-screen spinner)
// and restores scroll position once isLoadingHistory goes false (batch end).
@@ -1579,6 +1616,49 @@ export class IRCClient implements IRCClientContext {
this.sendRaw(serverId, `METADATA ${target} SYNC`);
}
+ // soju.im/bouncer-networks commands. The attribute payload must
+ // already be encoded with bouncerAttrs.encodeBouncerAttrs() to handle
+ // the message-tag-style escapes for `;`, ` `, etc.
+ bouncerListNetworks(serverId: string): void {
+ this.sendRaw(serverId, "BOUNCER LISTNETWORKS");
+ }
+ // BIND must run before CAP END -- the bouncer ties this connection to
+ // the upstream identified by netid for the rest of its lifetime.
+ bouncerBind(serverId: string, netid: string): void {
+ this.sendRaw(serverId, `BOUNCER BIND ${netid}`);
+ }
+ // Mark a server connection as a bouncer-child for the upcoming CAP
+ // negotiation. The next sendCapEnd() will emit `BOUNCER BIND `
+ // immediately before `CAP END`. Call this BEFORE the WS opens (or at
+ // least before CAP LS arrives) so we never miss the bind window.
+ setPendingBouncerBind(serverId: string, netid: string): void {
+ this.pendingBouncerBind.set(serverId, netid);
+ }
+ // Centralised CAP END sender. All call sites should use this instead
+ // of raw sendRaw("CAP END") so the BIND-before-end invariant is
+ // enforced in one place.
+ sendCapEnd(serverId: string): void {
+ const netid = this.pendingBouncerBind.get(serverId);
+ if (netid) {
+ this.pendingBouncerBind.delete(serverId);
+ this.sendRaw(serverId, `BOUNCER BIND ${netid}`);
+ }
+ this.sendRaw(serverId, "CAP END");
+ }
+ bouncerAddNetwork(serverId: string, encodedAttrs: string): void {
+ this.sendRaw(serverId, `BOUNCER ADDNETWORK ${encodedAttrs}`);
+ }
+ bouncerChangeNetwork(
+ serverId: string,
+ netid: string,
+ encodedAttrs: string,
+ ): void {
+ this.sendRaw(serverId, `BOUNCER CHANGENETWORK ${netid} ${encodedAttrs}`);
+ }
+ bouncerDelNetwork(serverId: string, netid: string): void {
+ this.sendRaw(serverId, `BOUNCER DELNETWORK ${netid}`);
+ }
+
/**
* IRCv3 draft/named-modes: send a mode change addressed by long-form
* mode names instead of single letters.
@@ -1964,7 +2044,7 @@ export class IRCClient implements IRCClientContext {
console.log(
`[CAP TIMEOUT] Timeout reached for ${serverId}, ending CAP negotiation`,
);
- this.sendRaw(serverId, "CAP END");
+ this.sendCapEnd(serverId);
this.capNegotiationComplete.set(serverId, true);
this.userOnConnect(serverId);
}
@@ -1979,7 +2059,7 @@ export class IRCClient implements IRCClientContext {
console.log(
`[CAP LS] No capabilities to request for ${serverId}, ending CAP negotiation`,
);
- this.sendRaw(serverId, "CAP END");
+ this.sendCapEnd(serverId);
this.capNegotiationComplete.set(serverId, true);
this.userOnConnect(serverId);
}
@@ -2051,7 +2131,7 @@ export class IRCClient implements IRCClientContext {
);
} else {
// No SASL or SASL not acknowledged - complete CAP negotiation now
- this.sendRaw(serverId, "CAP END");
+ this.sendCapEnd(serverId);
this.capNegotiationComplete.set(serverId, true);
this.userOnConnect(serverId);
}
diff --git a/src/lib/irc/IRCClientContext.ts b/src/lib/irc/IRCClientContext.ts
index ce8418c2..6b281688 100644
--- a/src/lib/irc/IRCClientContext.ts
+++ b/src/lib/irc/IRCClientContext.ts
@@ -35,6 +35,10 @@ export interface IRCClientContext {
// Public methods
sendRaw(serverId: string, command: string): void;
+ // CAP END that first emits a queued `BOUNCER BIND ` if this
+ // serverId was marked as a bouncer child. Centralised so the
+ // bind-before-end invariant lives in one place.
+ sendCapEnd(serverId: string): void;
triggerEvent(event: K, data: EventMap[K]): void;
// Private methods exposed for handlers
diff --git a/src/lib/irc/handlers/bouncer.ts b/src/lib/irc/handlers/bouncer.ts
new file mode 100644
index 00000000..3ff2d430
--- /dev/null
+++ b/src/lib/irc/handlers/bouncer.ts
@@ -0,0 +1,114 @@
+// Dispatches the soju.im/bouncer-networks BOUNCER command and the
+// FAIL BOUNCER standard-replies error variant. See the spec:
+// https://codeberg.org/emersion/soju/src/branch/master/doc/ext/bouncer-networks.md
+//
+// We keep the dispatcher dumb and turn every flavour into a typed event
+// that the store can consume. Attribute decoding lives in bouncerAttrs.
+
+import { decodeBouncerAttrs } from "../../bouncerAttrs";
+import type { IRCClientContext } from "../IRCClientContext";
+
+export function handleBouncer(
+ ctx: IRCClientContext,
+ serverId: string,
+ _source: string,
+ parv: string[],
+ mtags: Record | undefined,
+): void {
+ // parv[0] is the subcommand (server uses uppercase but spec calls it
+ // case-insensitive so be permissive).
+ const subcommand = parv[0]?.toUpperCase();
+ switch (subcommand) {
+ case "NETWORK": {
+ // BOUNCER NETWORK
+ const netid = parv[1];
+ const payload = parv[2] ?? "";
+ if (!netid) return;
+ if (payload === "*") {
+ ctx.triggerEvent("BOUNCER_NETWORK", {
+ serverId,
+ netid,
+ deleted: true,
+ attributes: {},
+ batchTag: mtags?.batch,
+ });
+ return;
+ }
+ ctx.triggerEvent("BOUNCER_NETWORK", {
+ serverId,
+ netid,
+ deleted: false,
+ attributes: decodeBouncerAttrs(payload),
+ batchTag: mtags?.batch,
+ });
+ return;
+ }
+ case "ADDNETWORK": {
+ const netid = parv[1];
+ if (netid) ctx.triggerEvent("BOUNCER_ADDNETWORK_OK", { serverId, netid });
+ return;
+ }
+ case "CHANGENETWORK": {
+ const netid = parv[1];
+ if (netid)
+ ctx.triggerEvent("BOUNCER_CHANGENETWORK_OK", { serverId, netid });
+ return;
+ }
+ case "DELNETWORK": {
+ const netid = parv[1];
+ if (netid) ctx.triggerEvent("BOUNCER_DELNETWORK_OK", { serverId, netid });
+ return;
+ }
+ }
+}
+
+// FAIL BOUNCER [context...] :
+//
+// Spec contexts vary by code:
+// INVALID_NETID FAIL BOUNCER INVALID_NETID :...
+// INVALID_ATTRIBUTE FAIL BOUNCER INVALID_ATTRIBUTE :...
+// READ_ONLY_ATTRIBUTE same shape
+// UNKNOWN_ATTRIBUTE same shape
+// NEED_ATTRIBUTE FAIL BOUNCER NEED_ATTRIBUTE ADDNETWORK :...
+// ACCOUNT_REQUIRED FAIL BOUNCER ACCOUNT_REQUIRED BIND :...
+// REGISTRATION_IS_COMPLETED FAIL BOUNCER REGISTRATION_IS_COMPLETED BIND :...
+// UNKNOWN_COMMAND FAIL BOUNCER UNKNOWN_COMMAND :...
+//
+// We hand all of this to the store as one event; the store / UI decides
+// how to surface it (toast vs inline-on-modal).
+export function handleBouncerFail(
+ ctx: IRCClientContext,
+ serverId: string,
+ _source: string,
+ parv: string[],
+): void {
+ // parv = [ "BOUNCER", code, subcommand, ...context, description ]
+ const [, code, subcommand, ...rest] = parv;
+ if (!code) return;
+ const description = rest.length > 0 ? rest[rest.length - 1] : "";
+ const context = rest.slice(0, -1);
+ // Helpful destructured fields for the common cases.
+ let netid: string | undefined;
+ let attribute: string | undefined;
+ if (code === "INVALID_NETID") {
+ netid = context[0];
+ } else if (
+ code === "INVALID_ATTRIBUTE" ||
+ code === "READ_ONLY_ATTRIBUTE" ||
+ code === "UNKNOWN_ATTRIBUTE"
+ ) {
+ netid = context[0];
+ attribute = context[1];
+ } else if (code === "NEED_ATTRIBUTE") {
+ attribute = context[0];
+ }
+ ctx.triggerEvent("BOUNCER_FAIL", {
+ serverId,
+ code,
+ subcommand: subcommand ?? "",
+ netid,
+ attribute,
+ context,
+ description,
+ });
+}
diff --git a/src/lib/irc/handlers/connection.ts b/src/lib/irc/handlers/connection.ts
index da7cb471..03ac1636 100644
--- a/src/lib/irc/handlers/connection.ts
+++ b/src/lib/irc/handlers/connection.ts
@@ -176,7 +176,7 @@ export function handleCap(
else if (subcommand === "ACK") {
ctx.onCapAck(serverId, caps);
} else if (subcommand === "NAK") {
- ctx.sendRaw(serverId, "CAP END");
+ ctx.sendCapEnd(serverId);
ctx.capNegotiationComplete.set(serverId, true);
} else if (subcommand === "NEW") ctx.onCapNew(serverId, caps);
else if (subcommand === "DEL") ctx.onCapDel(serverId, caps);
@@ -201,7 +201,7 @@ export function handleSaslSuccess(
_source: string,
_parv: string[],
): void {
- ctx.sendRaw(serverId, "CAP END");
+ ctx.sendCapEnd(serverId);
ctx.capNegotiationComplete.set(serverId, true);
ctx.userOnConnect(serverId);
}
@@ -212,7 +212,7 @@ export function handleSaslFailure(
_source: string,
_parv: string[],
): void {
- ctx.sendRaw(serverId, "CAP END");
+ ctx.sendCapEnd(serverId);
ctx.capNegotiationComplete.set(serverId, true);
ctx.userOnConnect(serverId);
}
diff --git a/src/lib/irc/handlers/index.ts b/src/lib/irc/handlers/index.ts
index ddc83311..02b3293e 100644
--- a/src/lib/irc/handlers/index.ts
+++ b/src/lib/irc/handlers/index.ts
@@ -12,6 +12,7 @@ import {
handleVerify,
handleWarn,
} from "./auth";
+import { handleBouncer, handleBouncerFail } from "./bouncer";
import {
handleListChannel,
handleListEnd,
@@ -286,11 +287,18 @@ export const IRC_DISPATCH: Record = {
AUTHENTICATE: (ctx, serverId, source, parv, mtags) =>
handleAuthenticate(ctx, serverId, source, parv, mtags),
- // FAIL METADATA is a distinct protocol — route to the metadata handler
- FAIL: (ctx, serverId, source, parv, mtags, trailing) =>
- parv[0] === "METADATA"
- ? handleMetadataFail(ctx, serverId, source, parv, mtags)
- : handleFail(ctx, serverId, source, parv, mtags, trailing),
+ // soju.im/bouncer-networks: BOUNCER NETWORK / ADDNETWORK / etc.
+ BOUNCER: (ctx, serverId, source, parv, mtags) =>
+ handleBouncer(ctx, serverId, source, parv, mtags),
+ // FAIL has per-command sub-protocols; dispatch on the
+ // command token rather than letting handleFail swallow them.
+ FAIL: (ctx, serverId, source, parv, mtags, trailing) => {
+ if (parv[0] === "METADATA")
+ return handleMetadataFail(ctx, serverId, source, parv, mtags);
+ if (parv[0] === "BOUNCER")
+ return handleBouncerFail(ctx, serverId, source, parv);
+ return handleFail(ctx, serverId, source, parv, mtags, trailing);
+ },
WARN: (ctx, serverId, source, parv, mtags, trailing) =>
handleWarn(ctx, serverId, source, parv, mtags, trailing),
NOTE: (ctx, serverId, source, parv, mtags, trailing) =>
diff --git a/src/locales/cs/messages.mjs b/src/locales/cs/messages.mjs
index 6ee64297..9edb39bf 100644
--- a/src/locales/cs/messages.mjs
+++ b/src/locales/cs/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Neplatný formát vzoru. Použijte formát nick!user@host (jsou povoleny zástupné znaky *)\"],\"+6NQQA\":[\"Obecný podpůrný kanál\"],\"+6NyRG\":[\"Klient\"],\"+K0AvT\":[\"Odpojit\"],\"+cyFdH\":[\"Výchozí zpráva při označení nepřítomnosti\"],\"+mVPqU\":[\"Zobrazovat Markdown formátování ve zprávách\"],\"+vqCJH\":[\"Uživatelské jméno vašeho účtu pro ověření\"],\"+yPBXI\":[\"Vybrat soubor\"],\"+zy2Nq\":[\"Typ\"],\"/09cao\":[\"Nízká bezpečnost připojení (Úroveň \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Uživatelé mimo kanál nemohou odesílat zprávy do něj\"],\"/4C8U0\":[\"Kopírovat vše\"],\"/6BzZF\":[\"Přepnout seznam členů\"],\"/AkXyp\":[\"Potvrdit?\"],\"/TNOPk\":[\"Uživatel je nepřítomen\"],\"/XQgft\":[\"Objevovat\"],\"/cF7Rs\":[\"Hlasitost\"],\"/dqduX\":[\"Další stránka\"],\"/fc3q4\":[\"Veškerý obsah\"],\"/kISDh\":[\"Povolit zvuky upozornění\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Zvuk\"],\"/rfkZe\":[\"Přehrávat zvuky pro zmínky a zprávy\"],\"0/0ZGA\":[\"Maska názvu kanálu\"],\"0D6j7U\":[\"Zjistit více o vlastních pravidlech →\"],\"0XsHcR\":[\"Vyhodit uživatele\"],\"0ZpE//\":[\"Seřadit podle uživatelů\"],\"0bEPwz\":[\"Nastavit nepřítomnost\"],\"0dGkPt\":[\"Rozbalit seznam kanálů\"],\"0gS7M5\":[\"Zobrazované jméno\"],\"0kS+M8\":[\"PříkladSÍŤ\"],\"0rgoY7\":[\"Připojovat se pouze k serverům, které si vyberete\"],\"0wdd7X\":[\"Připojit se\"],\"0wkVYx\":[\"Soukromé zprávy\"],\"111uHX\":[\"Náhled odkazu\"],\"196EG4\":[\"Smazat soukromý chat\"],\"1DSr1i\":[\"Zaregistrovat účet\"],\"1O/24y\":[\"Přepnout seznam kanálů\"],\"1VPJJ2\":[\"Varování o externím odkazu\"],\"1ZC/dv\":[\"Žádné nepřečtené zmínky ani zprávy\"],\"1pO1zi\":[\"Název serveru je povinný\"],\"1uwfzQ\":[\"Zobrazit téma kanálu\"],\"268g7c\":[\"Zadejte zobrazované jméno\"],\"2F9+AZ\":[\"Zatím nebyl zachycen žádný surový IRC provoz. Zkuste se připojit nebo odeslat zprávu.\"],\"2FOFq1\":[\"Operátoři serveru v síti by potenciálně mohli číst vaše zprávy\"],\"2FYpfJ\":[\"Více\"],\"2HF1Y2\":[[\"inviter\"],\" pozval \",[\"target\"],\" k připojení do \",[\"channel\"]],\"2I70QL\":[\"Zobrazit informace o profilu uživatele\"],\"2QYdmE\":[\"Uživatelé:\"],\"2QpEjG\":[\"odešel\"],\"2YE223\":[\"Zpráva #\",[\"0\"],\" (Enter pro nový řádek, Shift+Enter pro odeslání)\"],\"2bimFY\":[\"Použít heslo serveru\"],\"2iTmdZ\":[\"Místní úložiště:\"],\"2odkwe\":[\"Přísný - agresivnější ochrana\"],\"2uDhbA\":[\"Zadejte uživatelské jméno pro pozvání\"],\"2ygf/L\":[\"← Zpět\"],\"2zEgxj\":[\"Hledat GIFy...\"],\"3RdPhl\":[\"Přejmenovat kanál\"],\"3THokf\":[\"Uživatel s hlasem\"],\"3TSz9S\":[\"Minimalizovat\"],\"3jBDvM\":[\"Zobrazovaný název kanálu\"],\"3ryuFU\":[\"Volitelné zprávy o pádu pro zlepšení aplikace\"],\"3uBF/8\":[\"Zavřít prohlížeč\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Zadejte název účtu...\"],\"4/Rr0R\":[\"Pozvat uživatele do aktuálního kanálu\"],\"4EZrJN\":[\"Pravidla\"],\"4JJtW9\":[\"#přetečení\"],\"4NqeT4\":[\"Profil floodingu (+F)\"],\"4RZQRK\":[\"Co teď děláš?\"],\"4hfTrB\":[\"Přezdívka\"],\"4n99LO\":[\"Již v \",[\"0\"]],\"4t6vMV\":[\"Automaticky přepnout na jeden řádek pro krátké zprávy\"],\"4vsHmf\":[\"Čas (min)\"],\"5+INAX\":[\"Zvýrazňovat zprávy, které vás zmiňují\"],\"5R5Pv/\":[\"Jméno operátora\"],\"678PKt\":[\"Název sítě\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Heslo nutné pro vstup do kanálu. Nechte prázdné pro odstranění klíče.\"],\"6HhMs3\":[\"Zpráva při odpojení\"],\"6V3Ea3\":[\"Zkopírováno\"],\"6lGV3K\":[\"Zobrazit méně\"],\"6yFOEi\":[\"Zadejte heslo opera...\"],\"7+IHTZ\":[\"Žádný soubor nevybrán\"],\"73hrRi\":[\"nick!user@host (např. spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Odeslat soukromou zprávu\"],\"7U1W7c\":[\"Velmi uvolněný\"],\"7Y1YQj\":[\"Skutečné jméno:\"],\"7YHArF\":[\"— otevřít v prohlížeči\"],\"7fjnVl\":[\"Hledat uživatele...\"],\"7jL88x\":[\"Smazat tuto zprávu? Tuto akci nelze vrátit zpět.\"],\"7nGhhM\":[\"Na co myslíte?\"],\"7sEpu1\":[\"Členové — \",[\"0\"]],\"7sNhEz\":[\"Uživatelské jméno\"],\"8H0Q+x\":[\"Zjistit více o profilech →\"],\"8Phu0A\":[\"Zobrazovat, když uživatelé mění přezdívky\"],\"8XTG9e\":[\"Zadejte heslo operátora\"],\"8XsV2J\":[\"Zkusit odeslat znovu\"],\"8ZsakT\":[\"Heslo\"],\"8kR84m\":[\"Chystáte se otevřít externí odkaz:\"],\"8lCgih\":[\"Odebrat pravidlo\"],\"8o3dPc\":[\"Přetáhněte soubory pro nahrání\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"připojil se\"],\"few\":[\"připojil se \",[\"joinCount\"],\"×\"],\"many\":[\"připojil se \",[\"joinCount\"],\"×\"],\"other\":[\"připojil se \",[\"joinCount\"],\"×\"]}]],\"9BMLnJ\":[\"Znovu připojit k serveru\"],\"9OEgyT\":[\"Přidat reakci\"],\"9PQ8m2\":[\"G-Line (globální ban)\"],\"9Qs99X\":[\"E-mail:\"],\"9QupBP\":[\"Odebrat vzor\"],\"9bG48P\":[\"Odesílání\"],\"9f5f0u\":[\"Otázky ohledně soukromí? Kontaktujte nás:\"],\"9unqs3\":[\"Nepřítomen:\"],\"9v3hwv\":[\"Nebyly nalezeny žádné servery.\"],\"9zb2WA\":[\"Připojování\"],\"A1taO8\":[\"Hledat\"],\"A2adVi\":[\"Odesílat oznámení o psaní\"],\"A9Rhec\":[\"Název kanálu\"],\"AWOSPo\":[\"Přiblížit\"],\"AXSpEQ\":[\"Operátor při připojení\"],\"AeXO77\":[\"Účet\"],\"AhNP40\":[\"Přetočit\"],\"Ai2U7L\":[\"Hostitel\"],\"AjBQnf\":[\"Změněna přezdívka\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Zrušit odpověď\"],\"ApSx0O\":[\"Nalezeno \",[\"0\"],\" zpráv odpovídajících \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Žádné výsledky nenalezeny\"],\"AyNqAB\":[\"Zobrazit všechny události serveru v chatu\"],\"B/QqGw\":[\"Pryč od klávesnice\"],\"B8AaMI\":[\"Toto pole je povinné\"],\"BA2c49\":[\"Server nepodporuje pokročilé filtrování LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" a \",[\"3\"],\" dalších píší...\"],\"BGul2A\":[\"Máte neuložené změny. Opravdu chcete zavřít bez uložení?\"],\"BIf9fi\":[\"Vaše stavová zpráva\"],\"BPm98R\":[\"Není vybrán žádný server. Nejprve zvolte server z postranního panelu; pozvánkové odkazy se spravují pro každý server zvlášť.\"],\"BZz3md\":[\"Vaše osobní webová stránka\"],\"Bgm/H7\":[\"Povolit zadávání více řádků textu\"],\"BiQIl1\":[\"Připnout tuto soukromou konverzaci\"],\"BlNZZ2\":[\"Klikněte pro přechod na zprávu\"],\"Bowq3c\":[\"Téma kanálu mohou měnit pouze operátoři\"],\"Btozzp\":[\"Platnost tohoto obrázku vypršela\"],\"Bycfjm\":[\"Celkem: \",[\"0\"]],\"C6IBQc\":[\"Kopírovat celý JSON\"],\"C9L9wL\":[\"Sběr dat\"],\"CDq4wC\":[\"Moderovat uživatele\"],\"CHVRxG\":[\"Zpráva @\",[\"0\"],\" (Shift+Enter pro nový řádek)\"],\"CN9zdR\":[\"Jméno a heslo operátora jsou povinné\"],\"CW3sYa\":[\"Přidat reakci \",[\"emoji\"]],\"CaAkqd\":[\"Zobrazit odchody\"],\"CbvaYj\":[\"Ban podle přezdívky\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Vybrat kanál\"],\"CsekCi\":[\"Normální\"],\"D+NlUC\":[\"Systém\"],\"D28t6+\":[\"se připojil a odpojil\"],\"DB8zMK\":[\"Použít\"],\"DBcWHr\":[\"Vlastní soubor zvuku oznámení\"],\"DTy9Xw\":[\"Náhledy médií\"],\"Dj4pSr\":[\"Zvolte bezpečné heslo\"],\"Du+zn+\":[\"Hledám...\"],\"Du2T2f\":[\"Nastavení nenalezeno\"],\"DwsSVQ\":[\"Použít filtry a obnovit\"],\"E3W/zd\":[\"Výchozí přezdívka\"],\"E6nRW7\":[\"Kopírovat URL\"],\"E703RG\":[\"Režimy:\"],\"EAeu1Z\":[\"Odeslat pozvánku\"],\"EFKJQT\":[\"Nastavení\"],\"EGPQBv\":[\"Vlastní pravidla floodingu (+f)\"],\"ELik0r\":[\"Zobrazit úplné zásady ochrany soukromí\"],\"EPbeC2\":[\"Zobrazit nebo upravit téma kanálu\"],\"EQCDNT\":[\"Zadejte uživatelské jméno opera...\"],\"EUvulZ\":[\"Nalezena 1 zpráva odpovídající \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Další obrázek\"],\"EdQY6l\":[\"Žádné\"],\"EnqLYU\":[\"Hledat servery...\"],\"F0OKMc\":[\"Upravit server\"],\"F6Int2\":[\"Povolit zvýraznění\"],\"FDoLyE\":[\"Max. uživatelů\"],\"FUU/hZ\":[\"Kontrolujte, kolik externích médií se načítá v chatu.\"],\"Fdp03t\":[\"zap\"],\"FfPWR0\":[\"Modální okno\"],\"FjkaiT\":[\"Oddálit\"],\"FlqOE9\":[\"Co to znamená:\"],\"FolHNl\":[\"Spravujte svůj účet a ověřování\"],\"Fp2Dif\":[\"Opustit server\"],\"G5KmCc\":[\"GZ-Line (globální Z-Line)\"],\"GDs0lz\":[\"<0>Riziko:0> Citlivé informace (zprávy, soukromé konverzace, přihlašovací údaje) mohou být přístupné správcům sítě nebo útočníkům mezi IRC servery.\"],\"GR+2I3\":[\"Přidat masku pozvánky (např. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Zavřít vyskočená serverová oznámení\"],\"GdhD7H\":[\"Klikněte znovu pro potvrzení\"],\"GlHnXw\":[\"Změna přezdívky se nezdařila: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Náhled:\"],\"GtmO8/\":[\"od\"],\"GtuHUQ\":[\"Přejmenovat tento kanál na serveru. Nový název uvidí všichni uživatelé.\"],\"GuGfFX\":[\"Přepnout hledání\"],\"GxkJXS\":[\"Nahrávám...\"],\"GzbwnK\":[\"Připojil se ke kanálu\"],\"GzsUDB\":[\"Rozšířený profil\"],\"H/PnT8\":[\"Vložit emoji\"],\"H6Izzl\":[\"Váš preferovaný kód barvy\"],\"H9jIv+\":[\"Zobrazit připojení/odchody\"],\"HAKBY9\":[\"Nahrát soubory\"],\"HdE1If\":[\"Kanál\"],\"Hk4AW9\":[\"Vaše preferované zobrazované jméno\"],\"HmHDk7\":[\"Vybrat člena\"],\"HrQzPU\":[\"Kanály na \",[\"networkName\"]],\"I2tXQ5\":[\"Zpráva @\",[\"0\"],\" (Enter pro nový řádek, Shift+Enter pro odeslání)\"],\"I6bw/h\":[\"Zabanovat uživatele\"],\"I92Z+b\":[\"Povolit upozornění\"],\"I9D72S\":[\"Opravdu chcete tuto zprávu smazat? Tuto akci nelze vrátit zpět.\"],\"IA+1wo\":[\"Zobrazovat, když jsou uživatelé vyhozeni z kanálů\"],\"IDwkJx\":[\"IRC operátor\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Uložit změny\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" a \",[\"2\"],\" píší...\"],\"IgrLD/\":[\"Pauza\"],\"Im6JED\":[\"ŠEPOT\"],\"ImOQa9\":[\"Odpovědět\"],\"IoHMnl\":[\"Maximální hodnota je \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Připojování...\"],\"J5T9NW\":[\"Informace o uživateli\"],\"J8Y5+z\":[\"Jejda! Síť se rozdělila! ⚠️\"],\"JBHkBA\":[\"Opustil kanál\"],\"JCwL0Q\":[\"Zadejte důvod (volitelné)\"],\"JFciKP\":[\"Přepnout\"],\"JXGkhG\":[\"Změnit název kanálu (pouze operátoři)\"],\"JcD7qf\":[\"Více akcí\"],\"JdkA+c\":[\"Tajný (+s)\"],\"Jmu12l\":[\"Kanály serveru\"],\"JvQ++s\":[\"Povolit Markdown\"],\"K2jwh/\":[\"Data WHOIS nejsou k dispozici\"],\"KAXSwC\":[\"Hlas\"],\"KDfTdX\":[\"Smazat zprávu\"],\"KKBlUU\":[\"Vložit\"],\"KM0pLb\":[\"Vítejte v kanálu!\"],\"KR6W2h\":[\"Přestat ignorovat uživatele\"],\"KV+Bi1\":[\"Pouze na pozvání (+i)\"],\"KdCtwE\":[\"Kolik sekund sledovat floodingovou aktivitu před resetováním čítačů\"],\"Kkezga\":[\"Heslo serveru\"],\"KsiQ/8\":[\"Uživatelé musí být pozváni k připojení do kanálu\"],\"L+gB/D\":[\"Informace o kanálu\"],\"LC1a7n\":[\"IRC server oznámil, že jeho meziservery mají nízkou úroveň zabezpečení. To znamená, že když jsou vaše zprávy přeposílány mezi IRC servery v síti, nemusí být správně šifrovány nebo SSL/TLS certifikáty nemusí být správně ověřovány.\"],\"LNfLR5\":[\"Zobrazit vykopnutí\"],\"LQb0W/\":[\"Zobrazit všechny události\"],\"LU7/yA\":[\"Alternativní název pro zobrazení v rozhraní. Může obsahovat mezery, emoji a speciální znaky. Skutečný název kanálu (\",[\"channelName\"],\") bude nadále používán pro IRC příkazy.\"],\"LUb9O7\":[\"Je vyžadován platný port serveru\"],\"LV4fT6\":[\"Popis (volitelné, např. \\\"Beta testeři Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Zásady ochrany soukromí\"],\"LcuSDR\":[\"Spravujte informace profilu a metadata\"],\"LqLS9B\":[\"Zobrazit změny přezdívek\"],\"LsDQt2\":[\"Nastavení kanálu\"],\"LtI9AS\":[\"Vlastník\"],\"LuNhhL\":[\"reagoval na tuto zprávu\"],\"M/AZNG\":[\"URL vašeho avatara\"],\"M/WIer\":[\"Odeslat zprávu\"],\"M8er/5\":[\"Název:\"],\"MHk+7g\":[\"Předchozí obrázek\"],\"MRorGe\":[\"Soukromá zpráva uživateli\"],\"MVbSGP\":[\"Časové okno (sekundy)\"],\"MkpcsT\":[\"Vaše zprávy a nastavení jsou uloženy lokálně na vašem zařízení\"],\"N/hDSy\":[\"Označit jako bot - obvykle 'on' nebo prázdné\"],\"N7TQbE\":[\"Pozvat uživatele do \",[\"channelName\"]],\"NCca/o\":[\"Zadejte výchozí přezdívku...\"],\"Nqs6B9\":[\"Zobrazuje veškerá externí média. Libovolná URL může způsobit požadavek na neznámý server.\"],\"Nt+9O7\":[\"Použít WebSocket místo surového TCP\"],\"NxIHzc\":[\"Odpojit uživatele\"],\"O+v/cL\":[\"Procházet všechny kanály na serveru\"],\"ODwSCk\":[\"Odeslat GIF\"],\"OGQ5kK\":[\"Konfigurovat zvuky upozornění a zvýraznění\"],\"OIPt1Z\":[\"Zobrazit nebo skrýt boční panel se seznamem členů\"],\"OKSNq/\":[\"Velmi přísný\"],\"ONWvwQ\":[\"Nahrát\"],\"OVKoQO\":[\"Heslo vašeho účtu pro ověření\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Přinášíme IRC do budoucnosti\"],\"OhCpra\":[\"Nastavit téma…\"],\"OkltoQ\":[\"Zabanovat \",[\"username\"],\" podle přezdívky (zabrání opětovnému připojení se stejným nickem)\"],\"P+t/Te\":[\"Žádné další údaje\"],\"P42Wcc\":[\"Bezpečné\"],\"PD38l0\":[\"Náhled avatara kanálu\"],\"PD9mEt\":[\"Napište zprávu...\"],\"PPqfdA\":[\"Otevřít nastavení konfigurace kanálu\"],\"PSCjfZ\":[\"Téma, které bude zobrazeno pro tento kanál. Téma mohou vidět všichni uživatelé.\"],\"PZCecv\":[\"Náhled PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1×\"],\"few\":[[\"c\"],\"×\"],\"many\":[[\"c\"],\"×\"],\"other\":[[\"c\"],\"×\"]}]],\"PguS2C\":[\"Přidat masku výjimky (např. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Zobrazeno \",[\"displayedChannelsCount\"],\" z \",[\"0\"],\" kanálů\"],\"PqhVlJ\":[\"Zabanovat uživatele (podle masky hostitele)\"],\"Q+chwU\":[\"Uživatelské jméno:\"],\"Q2QY4/\":[\"Smazat tuto pozvánku\"],\"Q6hhn8\":[\"Předvolby\"],\"QF4a34\":[\"Zadejte prosím uživatelské jméno\"],\"QGqSZ2\":[\"Barva a formátování\"],\"QJQd1J\":[\"Upravit profil\"],\"QSzGDE\":[\"Nečinný\"],\"QUlny5\":[\"Vítejte v \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Číst více\"],\"QuSkCF\":[\"Filtrovat kanály...\"],\"QwUrDZ\":[\"změnil téma na: \",[\"topic\"]],\"R0UH07\":[\"Obrázek \",[\"0\"],\" z \",[\"1\"]],\"R7SsBE\":[\"Ztlumit\"],\"R8rf1X\":[\"Klikněte pro nastavení tématu\"],\"RArB3D\":[\"byl vyhozen z \",[\"channelName\"],\" uživatelem \",[\"username\"]],\"RI3cWd\":[\"Objevte svět IRC s ObsidianIRC\"],\"RIfHS5\":[\"Vytvořit nový pozvánkový odkaz\"],\"RMMaN5\":[\"Moderovaný (+m)\"],\"RWw9Lg\":[\"Zavřít okno\"],\"RZ2BuZ\":[\"Registrace účtu \",[\"account\"],\" vyžaduje ověření: \",[\"message\"]],\"RySp6q\":[\"Skrýt komentáře\"],\"SPKQTd\":[\"Přezdívka je povinná\"],\"SPVjfj\":[\"Výchozí bude 'bez důvodu', pokud ponecháte prázdné\"],\"SQKPvQ\":[\"Pozvat uživatele\"],\"SkZcl+\":[\"Vyberte předdefinovaný profil ochrany před floodem. Tyto profily poskytují vyvážená nastavení ochrany pro různé případy použití.\"],\"Slr+3C\":[\"Min. uživatelů\"],\"Spnlre\":[\"Pozval jste \",[\"target\"],\" k připojení do \",[\"channel\"]],\"T/ckN5\":[\"Otevřít v prohlížeči\"],\"T91vKp\":[\"Přehrát\"],\"TV2Wdu\":[\"Zjistěte, jak nakládáme s vašimi daty a chráníme vaše soukromí.\"],\"TgFpwD\":[\"Používám...\"],\"TkzSFB\":[\"Žádné změny\"],\"TtserG\":[\"Zadejte skutečné jméno\"],\"Ttz9J1\":[\"Zadejte heslo...\"],\"Tz0i8g\":[\"Nastavení\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"React\"],\"UE4KO5\":[\"*kanál*\"],\"UETAwW\":[\"Zatím jste nevytvořili žádné pozvánkové odkazy. Použijte formulář výše k vytvoření prvního.\"],\"UGT5vp\":[\"Uložit nastavení\"],\"UV5hLB\":[\"Nenalezeny žádné zákazy\"],\"Uaj3Nd\":[\"Stavové zprávy\"],\"Ue3uny\":[\"Výchozí (bez profilu)\"],\"UkARhe\":[\"Normální - standardní ochrana\"],\"Umn7Cj\":[\"Zatím žádné komentáře. Buďte první!\"],\"UtUIRh\":[[\"0\"],\" starších zpráv\"],\"UwzP+U\":[\"Zabezpečené připojení\"],\"V0/A4O\":[\"Vlastník kanálu\"],\"V4qgxE\":[\"Vytvořeno před (min. zpět)\"],\"V8yTm6\":[\"Vymazat hledání\"],\"VJMMyz\":[\"ObsidianIRC - Přinášíme IRC do budoucnosti\"],\"VJScHU\":[\"Důvod\"],\"VLsmVV\":[\"Ztlumit upozornění\"],\"VbyRUy\":[\"Komentáře\"],\"Vmx0mQ\":[\"Nastaveno:\"],\"VqnIZz\":[\"Zobrazit naše zásady ochrany soukromí a práci s daty\"],\"VrMygG\":[\"Minimální délka je \",[\"0\"]],\"VrnTui\":[\"Vaše zájmena, zobrazená ve vašem profilu\"],\"W8E3qn\":[\"Ověřený účet\"],\"WAakm9\":[\"Smazat kanál\"],\"WFxTHC\":[\"Přidat masku banu (např. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Hostitel serveru je povinný\"],\"WRYdXW\":[\"Pozice zvuku\"],\"WUOH5B\":[\"Ignorovat uživatele\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Zobrazit 1 další položku\"],\"few\":[\"Zobrazit \",[\"1\"],\" další položky\"],\"many\":[\"Zobrazit \",[\"1\"],\" dalších položek\"],\"other\":[\"Zobrazit \",[\"1\"],\" dalších položek\"]}]],\"WYxRzo\":[\"Vytvářejte a spravujte své pozvánkové odkazy\"],\"Wd38W1\":[\"Ponechte pole kanálu prázdné pro obecnou pozvánku do sítě. Popis slouží pouze pro vaše záznamy — viditelný je jen pro vás v tomto seznamu.\"],\"Weq9zb\":[\"Obecné\"],\"Wfj7Sk\":[\"Ztlumit nebo zapnout zvuky upozornění\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Profil uživatele\"],\"X6S3lt\":[\"Hledat nastavení, kanály, servery...\"],\"XEHan5\":[\"Přesto pokračovat\"],\"XI1+wb\":[\"Neplatný formát\"],\"XIXeuC\":[\"Zpráva @\",[\"0\"]],\"XMS+k4\":[\"Začít soukromou zprávu\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Odepnout soukromou konverzaci\"],\"Xm/s+u\":[\"Zobrazení\"],\"Xp2n93\":[\"Zobrazuje média z důvěryhodného file hostu vašeho serveru. Nejsou prováděny žádné požadavky na externí služby.\"],\"XvjC4F\":[\"Ukládám...\"],\"Y/qryO\":[\"Nebyly nalezeni žádní uživatelé odpovídající vašemu vyhledávání\"],\"YAqRpI\":[\"Registrace účtu \",[\"account\"],\" proběhla úspěšně: \",[\"message\"]],\"YEfzvP\":[\"Chráněné téma (+t)\"],\"YQOn6a\":[\"Sbalit seznam členů\"],\"YRCoE9\":[\"Operátor kanálu\"],\"YURQaF\":[\"Zobrazit profil\"],\"YdBSvr\":[\"Ovládat zobrazení médií a externího obsahu\"],\"Yj6U3V\":[\"Bez centrálního serveru:\"],\"YjvpGx\":[\"Zájmena\"],\"YqH4l4\":[\"Bez klíče\"],\"YyUPpV\":[\"Účet:\"],\"ZJSWfw\":[\"Zpráva zobrazená při odpojení od serveru\"],\"ZR1dJ4\":[\"Pozvánky\"],\"ZdWg0V\":[\"Otevřít v prohlížeči\"],\"ZhRBbl\":[\"Hledat zprávy…\"],\"Zmcu3y\":[\"Pokročilé filtry\"],\"a2/8e5\":[\"Téma nastaveno po (min)\"],\"aHKcKc\":[\"Předchozí stránka\"],\"aJTbXX\":[\"Heslo operátora\"],\"aQryQv\":[\"Vzor již existuje\"],\"aW9pLN\":[\"Maximální počet uživatelů povolených v kanálu. Nechte prázdné pro žádný limit.\"],\"ah4fmZ\":[\"Zobrazuje také náhledy z YouTube, Vimeo, SoundCloud a podobných známých služeb.\"],\"aifXak\":[\"V tomto kanálu nejsou žádná média\"],\"ap2zBz\":[\"Uvolněný\"],\"az8lvo\":[\"Vypnuto\"],\"azXSNo\":[\"Rozbalit seznam členů\"],\"azdliB\":[\"Přihlásit se k účtu\"],\"b26wlF\":[\"ona/její\"],\"bD/+Ei\":[\"Přísný\"],\"bQ6BJn\":[\"Nakonfigurujte podrobná pravidla ochrany proti floodingu. Každé pravidlo určuje, jaký typ aktivity sledovat a jakou akci provést při překročení prahů.\"],\"beV7+y\":[\"Uživatel obdrží pozvánku k připojení do \",[\"channelName\"],\".\"],\"bk84cH\":[\"Zpráva o nepřítomnosti\"],\"bkHdLj\":[\"Přidat IRC server\"],\"bmQLn5\":[\"Přidat pravidlo\"],\"bwRvnp\":[\"Akce\"],\"c8+EVZ\":[\"Ověřený účet\"],\"cGYUlD\":[\"Nejsou načteny žádné náhledy médií.\"],\"cLF98o\":[\"Zobrazit komentáře (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Žádní uživatelé nejsou k dispozici\"],\"cSgpoS\":[\"Připnout soukromou konverzaci\"],\"cde3ce\":[\"Zpráva <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Kopírovat formátovaný výstup\"],\"cl/A5J\":[\"Vítejte v \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Smazat\"],\"coPLXT\":[\"Neukládáme vaši IRC komunikaci na našich serverech\"],\"crYH/6\":[\"Přehrávač SoundCloud\"],\"d3sis4\":[\"Přidat server\"],\"d9aN5k\":[\"Odebrat \",[\"username\"],\" z kanálu\"],\"dEgA5A\":[\"Zrušit\"],\"dGi1We\":[\"Odepnout tuto soukromou konverzaci\"],\"dJVuyC\":[\"opustil \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"do\"],\"dXqxlh\":[\"<0>⚠️ Bezpečnostní riziko!0> Toto připojení může být zranitelné vůči odposlechu nebo útokům man-in-the-middle.\"],\"da9Q/R\":[\"Změněny módy kanálu\"],\"dhJN3N\":[\"Zobrazit komentáře\"],\"dj2xTE\":[\"Odmítnout oznámení\"],\"dpCzmC\":[\"Nastavení ochrany proti floodingu\"],\"e9dQpT\":[\"Chcete otevřít tento odkaz v nové záložce?\"],\"ePK91l\":[\"Upravit\"],\"eYBDuB\":[\"Nahrajte obrázek nebo zadejte URL s volitelnou substitucí \",[\"size\"],\" pro dynamické velikosti\"],\"edBbee\":[\"Zabanovat \",[\"username\"],\" podle masky hostitele (zabrání opětovnému připojení ze stejné IP/hostitele)\"],\"ekfzWq\":[\"Nastavení uživatele\"],\"elPDWs\":[\"Přizpůsobte si IRC klienta\"],\"eu2osY\":[\"<0>💡 Doporučení:0> Pokračujte pouze pokud důvěřujete tomuto serveru a rozumíte rizikům. Vyhněte se sdílení citlivých informací nebo hesel přes toto připojení.\"],\"euEhbr\":[\"Klikněte pro připojení k \",[\"channel\"]],\"ez3vLd\":[\"Povolit víceřádkové zadávání\"],\"f0J5Ki\":[\"Komunikace mezi servery může používat nešifrovaná připojení\"],\"f9BHJk\":[\"Varovat uživatele\"],\"fDOLLd\":[\"Nebyly nalezeny žádné kanály.\"],\"ffzDkB\":[\"Anonymní analytika:\"],\"fq1GF9\":[\"Zobrazit při odpojení uživatelů ze serveru\"],\"gEF57C\":[\"Tento server podporuje pouze jeden typ připojení\"],\"gJuLUI\":[\"Seznam ignorovaných\"],\"gNzMrk\":[\"Aktuální avatar\"],\"gjPWyO\":[\"Zadejte přezdívku...\"],\"gz6UQ3\":[\"Maximalizovat\"],\"h6razj\":[\"Maska vyloučení názvu kanálu\"],\"hG6jnw\":[\"Téma není nastaveno\"],\"hG89Ed\":[\"Obrázek\"],\"hYgDIe\":[\"Vytvořit\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"např. 100:1440\"],\"he3ygx\":[\"Kopírovat\"],\"hehnjM\":[\"Množství\"],\"hzdLuQ\":[\"Mluvit mohou pouze uživatelé s hlasem nebo vyšší hodností\"],\"i0qMbr\":[\"Domů\"],\"iDNBZe\":[\"Oznámení\"],\"iH8pgl\":[\"Zpět\"],\"iL9SZg\":[\"Zabanovat uživatele (podle přezdívky)\"],\"iNt+3c\":[\"Zpět na obrázek\"],\"iQvi+a\":[\"Neupozorňovat mě na nízkou bezpečnost připojení pro tento server\"],\"iSLIjg\":[\"Připojit\"],\"iWXkHH\":[\"Polooperátor\"],\"iZeTtp\":[\"Hostitel serveru\"],\"idD8Ev\":[\"Uloženo\"],\"iivqkW\":[\"Přihlášen\"],\"ij+Elv\":[\"Náhled obrázku\"],\"ilIWp7\":[\"Přepnout oznámení\"],\"iuaqvB\":[\"Použijte * pro zástupné znaky. Příklady: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Ban podle masky hostitele\"],\"jA4uoI\":[\"Téma:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Důvod (volitelné)\"],\"jUV7CU\":[\"Nahrát avatar\"],\"jW5Uwh\":[\"Kontrolujte načítání externích médií. Vypnuto / Bezpečné / Důvěryhodné zdroje / Veškerý obsah.\"],\"jXzms5\":[\"Možnosti přílohy\"],\"jZlrte\":[\"Barva\"],\"jfC/xh\":[\"Kontakt\"],\"jywMpv\":[\"#nový-název-kanálu\"],\"k112DD\":[\"Načíst starší zprávy\"],\"k3ID0F\":[\"Filtrovat členy…\"],\"k65gsE\":[\"Podrobný přehled\"],\"k7Zgob\":[\"Zrušit připojení\"],\"kAVx5h\":[\"Nenalezeny žádné pozvánky\"],\"kCLEPU\":[\"Připojeno k\"],\"kF5LKb\":[\"Ignorované vzory:\"],\"kGeOx/\":[\"Připojit se k \",[\"0\"]],\"kITKr8\":[\"Načítám režimy kanálu...\"],\"kPpPsw\":[\"Jste IRC operátor\"],\"kWJmRL\":[\"Vy\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Kopírovat JSON\"],\"krViRy\":[\"Klikněte pro kopírování jako JSON\"],\"ks71ra\":[\"Výjimky\"],\"kw4lRv\":[\"Polooperátor kanálu\"],\"kxgIRq\":[\"Vyberte nebo přidejte kanál pro začátek.\"],\"ky6dWe\":[\"Náhled avatara\"],\"l+GxCv\":[\"Načítám kanály...\"],\"l+IUVW\":[\"Ověření účtu \",[\"account\"],\" proběhlo úspěšně: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"znovu se připojil\"],\"few\":[\"znovu se připojil \",[\"reconnectCount\"],\"×\"],\"many\":[\"znovu se připojil \",[\"reconnectCount\"],\"×\"],\"other\":[\"znovu se připojil \",[\"reconnectCount\"],\"×\"]}]],\"l1l8sj\":[\"před \",[\"0\"],\" dny\"],\"l5NhnV\":[\"#kanál (volitelné)\"],\"l5jmzx\":[[\"0\"],\" a \",[\"1\"],\" píší...\"],\"lCF0wC\":[\"Obnovit\"],\"lHy8N5\":[\"Načítám více kanálů...\"],\"lasgrr\":[\"použito\"],\"lbpf14\":[\"Připojit se k \",[\"value\"]],\"lfFsZ4\":[\"Kanály\"],\"lkNdiH\":[\"Název účtu\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Nahrát obrázek\"],\"loQxaJ\":[\"Jsem zpět\"],\"lvfaxv\":[\"DOMŮ\"],\"m16xKo\":[\"Přidat\"],\"m8flAk\":[\"Náhled (ještě nenahrán)\"],\"mEPxTp\":[\"<0>⚠️ Buďte opatrní!0> Otevírejte pouze odkazy z důvěryhodných zdrojů. Škodlivé odkazy mohou ohrozit vaši bezpečnost nebo soukromí.\"],\"mHGdhG\":[\"Informace o serveru\"],\"mHS8lb\":[\"Zpráva #\",[\"0\"]],\"mMYBD9\":[\"Široký - širší rozsah ochrany\"],\"mTGsPd\":[\"Téma kanálu\"],\"mU8j6O\":[\"Žádné externí zprávy (+n)\"],\"mZp8FL\":[\"Automatický návrat na jeden řádek\"],\"mdQu8G\":[\"VašePřezdívka\"],\"miSSBQ\":[\"Komentáře (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Uživatel je ověřen\"],\"mwtcGl\":[\"Zavřít komentáře\"],\"mzI/c+\":[\"Stáhnout\"],\"n3fGRk\":[\"nastaveno \",[\"0\"]],\"nE9jsU\":[\"Uvolněný - méně agresivní ochrana\"],\"nNflMD\":[\"Opustit kanál\"],\"nPXkBi\":[\"Načítám data WHOIS...\"],\"nQnxxF\":[\"Zpráva #\",[\"0\"],\" (Shift+Enter pro nový řádek)\"],\"nWMRxa\":[\"Odepnout\"],\"nkC032\":[\"Žádný profil floodingu\"],\"o69z4d\":[\"Odeslat varovnou zprávu uživateli \",[\"username\"]],\"o9ylQi\":[\"Hledejte GIFy pro začátek\"],\"oFGkER\":[\"Oznámení serveru\"],\"oOi11l\":[\"Přejít dolů\"],\"oPYIL5\":[\"síť\"],\"oQEzQR\":[\"Nová DM\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Útoky man-in-the-middle na serverová připojení jsou možné\"],\"oeqmmJ\":[\"Důvěryhodné zdroje\"],\"optX0N\":[\"před \",[\"0\"],\" h\"],\"ovBPCi\":[\"Výchozí\"],\"p0Z69r\":[\"Vzor nemůže být prázdný\"],\"p1KgtK\":[\"Nepodařilo se načíst zvuk\"],\"p59pEv\":[\"Další podrobnosti\"],\"p7sRI6\":[\"Informovat ostatní, když píšete\"],\"pBm1od\":[\"Tajný kanál\"],\"pNmiXx\":[\"Vaše výchozí přezdívka pro všechny servery\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Heslo účtu\"],\"peNE68\":[\"Trvalý\"],\"plhHQt\":[\"Žádná data\"],\"pm6+q5\":[\"Bezpečnostní upozornění\"],\"pn5qSs\":[\"Další informace\"],\"q0cR4S\":[\"je nyní znám jako **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanál se nebude zobrazovat v příkazech LIST nebo NAMES\"],\"qLpTm/\":[\"Odebrat reakci \",[\"emoji\"]],\"qVkGWK\":[\"Připnout\"],\"qY8wNa\":[\"Domovská stránka\"],\"qb0xJ7\":[\"Použijte zástupné znaky: * odpovídá libovolné sekvenci, ? odpovídá libovolnému jednomu znaku. Příklady: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Klíč kanálu (+k)\"],\"qtoOYG\":[\"Bez omezení\"],\"r1W2AS\":[\"Obrázek z file hostu\"],\"rIPR2O\":[\"Téma nastaveno před (min)\"],\"rMMSYo\":[\"Maximální délka je \",[\"0\"]],\"rWtzQe\":[\"Síť se rozdělila a znovu připojila. ✅\"],\"rYG2u6\":[\"Prosím čekejte...\"],\"rdUucN\":[\"Náhled\"],\"rjGI/Q\":[\"Soukromí\"],\"rk8iDX\":[\"Načítám GIFy...\"],\"rn6SBY\":[\"Zrušit ztlumení\"],\"s/UKqq\":[\"Byl vykopnut z kanálu\"],\"s8cATI\":[\"se připojil k \",[\"channelName\"]],\"sCO9ue\":[\"Připojení k <0>\",[\"serverName\"],\"0> má následující bezpečnostní problémy:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"je nyní znám jako **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" vás pozval k připojení do \",[\"channel\"]],\"sby+1/\":[\"Klikněte pro kopírování\"],\"sfN25C\":[\"Vaše skutečné nebo celé jméno\"],\"sliuzR\":[\"Otevřít odkaz\"],\"sqrO9R\":[\"Vlastní zmínky\"],\"sr6RdJ\":[\"Víceřádkové na Shift+Enter\"],\"swrCpB\":[\"Kanál byl přejmenován z \",[\"oldName\"],\" na \",[\"newName\"],\" uživatelem \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Pokročilé\"],\"t/YqKh\":[\"Odebrat\"],\"t47eHD\":[\"Váš jedinečný identifikátor na tomto serveru\"],\"tAkAh0\":[\"URL s volitelnou substitucí \",[\"size\"],\" pro dynamické velikosti. Příklad: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Zobrazit nebo skrýt boční panel se seznamem kanálů\"],\"tfDRzk\":[\"Uložit\"],\"tiBsJk\":[\"opustil \",[\"channelName\"]],\"tt4/UD\":[\"se odpojil (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Přezdívka {nick} je již používána, zkouším s {newNick}\"],\"u0a8B4\":[\"Ověřit jako IRC operátor pro administrativní přístup\"],\"u0rWFU\":[\"Vytvořeno po (min. zpět)\"],\"u72w3t\":[\"Uživatelé a vzory k ignorování\"],\"u7jc2L\":[\"se odpojil\"],\"uAQUqI\":[\"Stav\"],\"uB85T3\":[\"Uložení selhalo: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC servery:\"],\"ukyW4o\":[\"Vaše pozvánkové odkazy\"],\"usSSr/\":[\"Úroveň přiblížení\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Použijte Shift+Enter pro nový řádek (Enter odešle)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Bez tématu\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Jazyk\"],\"vaHYxN\":[\"Skutečné jméno\"],\"vhjbKr\":[\"Nepřítomen\"],\"w4NYox\":[\"klient \",[\"title\"]],\"w8xQRx\":[\"Neplatná hodnota\"],\"wFjjxZ\":[\"byl vyhozen z \",[\"channelName\"],\" uživatelem \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nenalezeny žádné výjimky zákazu\"],\"wPrGnM\":[\"Správce kanálu\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Zobrazovat, když uživatelé vstupují nebo opouštějí kanály\"],\"whqZ9r\":[\"Další slova nebo fráze ke zvýraznění\"],\"wm7RV4\":[\"Zvuk oznámení\"],\"wz/Yoq\":[\"Vaše zprávy mohou být zachyceny při přeposílání mezi servery\"],\"x3+y8b\":[\"Tolik lidí se zaregistrovalo přes tento odkaz\"],\"xCJdfg\":[\"Vymazat\"],\"xOTzt5\":[\"právě teď\"],\"xUHRTR\":[\"Automaticky ověřit jako operátor při připojení\"],\"xWHwwQ\":[\"Bany\"],\"xYilR2\":[\"Média\"],\"xbi8D6\":[\"Tento server nepodporuje pozvánkové odkazy (capability<0>obby.world/invitation0>není inzerována). Můžete normálně chatovat; tento panel je určen pro sítě poháněné obbyircd.\"],\"xceQrO\":[\"Jsou podporovány pouze zabezpečené websocket připojení\"],\"xdtXa+\":[\"název-kanálu\"],\"xfXC7q\":[\"Textové kanály\"],\"xlCYOE\":[\"Načítám více zpráv...\"],\"xlhswE\":[\"Minimální hodnota je \",[\"0\"]],\"xq97Ci\":[\"Přidat slovo nebo frázi...\"],\"xuRqRq\":[\"Limit klientů (+l)\"],\"xwF+7J\":[[\"0\"],\" píše...\"],\"y1eoq1\":[\"Kopírovat odkaz\"],\"yNeucF\":[\"Tento server nepodporuje rozšířená metadata profilu (rozšíření IRCv3 METADATA). Další pole jako avatar, zobrazované jméno a stav nejsou k dispozici.\"],\"yPlrca\":[\"Avatar kanálu\"],\"yQE2r9\":[\"Načítání\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Uživatelské jméno operátora\"],\"yYOzWD\":[\"logy\"],\"yfx9Re\":[\"Heslo IRC operátora\"],\"ygCKqB\":[\"Zastavit\"],\"ymDxJx\":[\"Uživatelské jméno IRC operátora\"],\"yrpRsQ\":[\"Seřadit podle názvu\"],\"yz7wBu\":[\"Zavřít\"],\"zJw+jA\":[\"nastavuje režim: \",[\"0\"]],\"zbymaY\":[\"před \",[\"0\"],\" min\"],\"zebeLu\":[\"Zadejte uživatelské jméno operátora\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Neplatný formát vzoru. Použijte formát nick!user@host (jsou povoleny zástupné znaky *)\"],\"+6NQQA\":[\"Obecný podpůrný kanál\"],\"+6NyRG\":[\"Klient\"],\"+K0AvT\":[\"Odpojit\"],\"+cyFdH\":[\"Výchozí zpráva při označení nepřítomnosti\"],\"+mVPqU\":[\"Zobrazovat Markdown formátování ve zprávách\"],\"+vqCJH\":[\"Uživatelské jméno vašeho účtu pro ověření\"],\"+yPBXI\":[\"Vybrat soubor\"],\"+zy2Nq\":[\"Typ\"],\"/09cao\":[\"Nízká bezpečnost připojení (Úroveň \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Uživatelé mimo kanál nemohou odesílat zprávy do něj\"],\"/4C8U0\":[\"KopÃrovat vÅ¡e\"],\"/6BzZF\":[\"Přepnout seznam členů\"],\"/AkXyp\":[\"Potvrdit?\"],\"/TNOPk\":[\"Uživatel je nepřítomen\"],\"/XQgft\":[\"Objevovat\"],\"/cF7Rs\":[\"Hlasitost\"],\"/dqduX\":[\"Další stránka\"],\"/fc3q4\":[\"Veškerý obsah\"],\"/kISDh\":[\"Povolit zvuky upozornění\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Zvuk\"],\"/rfkZe\":[\"Přehrávat zvuky pro zmínky a zprávy\"],\"0/0ZGA\":[\"Maska názvu kanálu\"],\"0D6j7U\":[\"Zjistit více o vlastních pravidlech →\"],\"0XsHcR\":[\"Vyhodit uživatele\"],\"0ZpE//\":[\"Seřadit podle uživatelů\"],\"0bEPwz\":[\"Nastavit nepřítomnost\"],\"0dGkPt\":[\"Rozbalit seznam kanálů\"],\"0gS7M5\":[\"Zobrazované jméno\"],\"0kS+M8\":[\"PříkladSÍŤ\"],\"0rgoY7\":[\"Připojovat se pouze k serverům, které si vyberete\"],\"0wdd7X\":[\"Připojit se\"],\"0wkVYx\":[\"Soukromé zprávy\"],\"111uHX\":[\"Náhled odkazu\"],\"196EG4\":[\"Smazat soukromý chat\"],\"1DSr1i\":[\"Zaregistrovat účet\"],\"1O/24y\":[\"Přepnout seznam kanálů\"],\"1TNIig\":[\"Otevřít\"],\"1VPJJ2\":[\"Varování o externím odkazu\"],\"1ZC/dv\":[\"Žádné nepřečtené zmínky ani zprávy\"],\"1pO1zi\":[\"Název serveru je povinný\"],\"1uwfzQ\":[\"Zobrazit téma kanálu\"],\"268g7c\":[\"Zadejte zobrazované jméno\"],\"2CEOW6\":[\"Síť navázaná přes soju bouncer\"],\"2F9+AZ\":[\"ZatÃm nebyl zachycen žádný surový IRC provoz. Zkuste se pÅipojit nebo odeslat zprávu.\"],\"2FOFq1\":[\"Operátoři serveru v síti by potenciálně mohli číst vaše zprávy\"],\"2FYpfJ\":[\"Více\"],\"2HF1Y2\":[[\"inviter\"],\" pozval \",[\"target\"],\" k připojení do \",[\"channel\"]],\"2I70QL\":[\"Zobrazit informace o profilu uživatele\"],\"2QYdmE\":[\"Uživatelé:\"],\"2QpEjG\":[\"odešel\"],\"2YE223\":[\"Zpráva #\",[\"0\"],\" (Enter pro nový řádek, Shift+Enter pro odeslání)\"],\"2bimFY\":[\"Použít heslo serveru\"],\"2iTmdZ\":[\"Místní úložiště:\"],\"2odkwe\":[\"Přísný - agresivnější ochrana\"],\"2uDhbA\":[\"Zadejte uživatelské jméno pro pozvání\"],\"2ygf/L\":[\"← Zpět\"],\"2zEgxj\":[\"Hledat GIFy...\"],\"3RdPhl\":[\"Přejmenovat kanál\"],\"3THokf\":[\"Uživatel s hlasem\"],\"3TSz9S\":[\"Minimalizovat\"],\"3jBDvM\":[\"Zobrazovaný název kanálu\"],\"3ryuFU\":[\"Volitelné zprávy o pádu pro zlepšení aplikace\"],\"3uBF/8\":[\"Zavřít prohlížeč\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Zadejte název účtu...\"],\"4/Rr0R\":[\"Pozvat uživatele do aktuálního kanálu\"],\"4EZrJN\":[\"Pravidla\"],\"4JJtW9\":[\"#přetečení\"],\"4NqeT4\":[\"Profil floodingu (+F)\"],\"4RZQRK\":[\"Co teď děláš?\"],\"4hfTrB\":[\"Přezdívka\"],\"4n99LO\":[\"Již v \",[\"0\"]],\"4t6vMV\":[\"Automaticky přepnout na jeden řádek pro krátké zprávy\"],\"4vsHmf\":[\"Čas (min)\"],\"4x/Axu\":[\"Váš bouncer zatím nemá žádné sítě. Přidejte jednu pro začátek.\"],\"5+INAX\":[\"Zvýrazňovat zprávy, které vás zmiňují\"],\"5R5Pv/\":[\"Jméno operátora\"],\"678PKt\":[\"Název sítě\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Heslo nutné pro vstup do kanálu. Nechte prázdné pro odstranění klíče.\"],\"6HhMs3\":[\"Zpráva při odpojení\"],\"6V3Ea3\":[\"Zkopírováno\"],\"6lGV3K\":[\"Zobrazit méně\"],\"6yFOEi\":[\"Zadejte heslo opera...\"],\"7+IHTZ\":[\"Žádný soubor nevybrán\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (např. spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Odeslat soukromou zprávu\"],\"7U1W7c\":[\"Velmi uvolněný\"],\"7Y1YQj\":[\"Skutečné jméno:\"],\"7YHArF\":[\"— otevřít v prohlížeči\"],\"7fjnVl\":[\"Hledat uživatele...\"],\"7jL88x\":[\"Smazat tuto zprávu? Tuto akci nelze vrátit zpět.\"],\"7nGhhM\":[\"Na co myslíte?\"],\"7sEpu1\":[\"Členové — \",[\"0\"]],\"7sNhEz\":[\"Uživatelské jméno\"],\"8H0Q+x\":[\"Zjistit více o profilech →\"],\"8Phu0A\":[\"Zobrazovat, když uživatelé mění přezdívky\"],\"8XTG9e\":[\"Zadejte heslo operátora\"],\"8XsV2J\":[\"Zkusit odeslat znovu\"],\"8ZsakT\":[\"Heslo\"],\"8kR84m\":[\"Chystáte se otevřít externí odkaz:\"],\"8lCgih\":[\"Odebrat pravidlo\"],\"8o3dPc\":[\"PÅetáhnÄte soubory pro nahránÃ\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"připojil se\"],\"few\":[\"připojil se \",[\"joinCount\"],\"×\"],\"many\":[\"připojil se \",[\"joinCount\"],\"×\"],\"other\":[\"připojil se \",[\"joinCount\"],\"×\"]}]],\"9BMLnJ\":[\"Znovu připojit k serveru\"],\"9OEgyT\":[\"Přidat reakci\"],\"9PQ8m2\":[\"G-Line (globální ban)\"],\"9Qs99X\":[\"E-mail:\"],\"9QupBP\":[\"Odebrat vzor\"],\"9W7tl5\":[\"(beze změny)\"],\"9bG48P\":[\"Odesílání\"],\"9f5f0u\":[\"Otázky ohledně soukromí? Kontaktujte nás:\"],\"9iweoP\":[\"Sítě na \",[\"0\"]],\"9unqs3\":[\"Nepřítomen:\"],\"9v3hwv\":[\"Nebyly nalezeny žádné servery.\"],\"9zb2WA\":[\"Připojování\"],\"A1taO8\":[\"Hledat\"],\"A2adVi\":[\"Odesílat oznámení o psaní\"],\"A9Rhec\":[\"Název kanálu\"],\"AWOSPo\":[\"Přiblížit\"],\"AXSpEQ\":[\"Operátor při připojení\"],\"AdKRCX\":[\"Jste připojeni k <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Účet\"],\"AhNP40\":[\"Přetočit\"],\"Ai2U7L\":[\"Hostitel\"],\"AjBQnf\":[\"Změněna přezdívka\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Zrušit odpověď\"],\"ApSx0O\":[\"Nalezeno \",[\"0\"],\" zpráv odpovídajících \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Žádné výsledky nenalezeny\"],\"AyNqAB\":[\"Zobrazit všechny události serveru v chatu\"],\"B/QqGw\":[\"Pryč od klávesnice\"],\"B0sB2k\":[\"Prostý text\"],\"B8AaMI\":[\"Toto pole je povinné\"],\"BA2c49\":[\"Server nepodporuje pokročilé filtrování LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" a \",[\"3\"],\" dalších píší...\"],\"BGul2A\":[\"Máte neuložené změny. Opravdu chcete zavřít bez uložení?\"],\"BIf9fi\":[\"Vaše stavová zpráva\"],\"BOJWfb\":[\"Odpojit od soju bounceru?\"],\"BPm98R\":[\"Nenà vybrán žádný server. Nejprve zvolte server z postrannÃho panelu; pozvánkové odkazy se spravujà pro každý server zvlášť.\"],\"BZz3md\":[\"Vaše osobní webová stránka\"],\"Bgm/H7\":[\"Povolit zadávání více řádků textu\"],\"BiQIl1\":[\"Připnout tuto soukromou konverzaci\"],\"BlNZZ2\":[\"Klikněte pro přechod na zprávu\"],\"Bowq3c\":[\"Téma kanálu mohou měnit pouze operátoři\"],\"Btozzp\":[\"Platnost tohoto obrázku vypršela\"],\"Bycfjm\":[\"Celkem: \",[\"0\"]],\"C6IBQc\":[\"Kopírovat celý JSON\"],\"C9L9wL\":[\"Sběr dat\"],\"CDq4wC\":[\"Moderovat uživatele\"],\"CHVRxG\":[\"Zpráva @\",[\"0\"],\" (Shift+Enter pro nový řádek)\"],\"CN9zdR\":[\"Jméno a heslo operátora jsou povinné\"],\"CW3sYa\":[\"Přidat reakci \",[\"emoji\"]],\"CaAkqd\":[\"Zobrazit odchody\"],\"CbvaYj\":[\"Ban podle přezdívky\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Vybrat kanál\"],\"CsekCi\":[\"Normální\"],\"D+NlUC\":[\"Systém\"],\"D28t6+\":[\"se připojil a odpojil\"],\"DB8zMK\":[\"Použít\"],\"DBcWHr\":[\"Vlastní soubor zvuku oznámení\"],\"DTy9Xw\":[\"Náhledy médií\"],\"Dj4pSr\":[\"Zvolte bezpečné heslo\"],\"Du+zn+\":[\"Hledám...\"],\"Du2T2f\":[\"Nastavení nenalezeno\"],\"DwsSVQ\":[\"Použít filtry a obnovit\"],\"E3W/zd\":[\"Výchozí přezdívka\"],\"E6nRW7\":[\"Kopírovat URL\"],\"E703RG\":[\"Režimy:\"],\"EAeu1Z\":[\"Odeslat pozvánku\"],\"EFKJQT\":[\"Nastavení\"],\"EGPQBv\":[\"Vlastní pravidla floodingu (+f)\"],\"ELik0r\":[\"Zobrazit úplné zásady ochrany soukromí\"],\"EPbeC2\":[\"Zobrazit nebo upravit téma kanálu\"],\"EQCDNT\":[\"Zadejte uživatelské jméno opera...\"],\"EUvulZ\":[\"Nalezena 1 zpráva odpovídající \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Další obrázek\"],\"EdQY6l\":[\"Žádné\"],\"EnqLYU\":[\"Hledat servery...\"],\"F0OKMc\":[\"Upravit server\"],\"F6Int2\":[\"Povolit zvýraznění\"],\"FDoLyE\":[\"Max. uživatelů\"],\"FUU/hZ\":[\"Kontrolujte, kolik externích médií se načítá v chatu.\"],\"Fdp03t\":[\"zap\"],\"FfPWR0\":[\"Modální okno\"],\"FjkaiT\":[\"Oddálit\"],\"FlqOE9\":[\"Co to znamená:\"],\"FolHNl\":[\"Spravujte svůj účet a ověřování\"],\"Fp2Dif\":[\"Opustit server\"],\"G5KmCc\":[\"GZ-Line (globální Z-Line)\"],\"GDs0lz\":[\"<0>Riziko:0> Citlivé informace (zprávy, soukromé konverzace, přihlašovací údaje) mohou být přístupné správcům sítě nebo útočníkům mezi IRC servery.\"],\"GR+2I3\":[\"Přidat masku pozvánky (např. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Zavřít vyskočená serverová oznámení\"],\"GdhD7H\":[\"KliknÄte znovu pro potvrzenÃ\"],\"GjRZex\":[\"Odpojit síť?\"],\"GlHnXw\":[\"Změna přezdívky se nezdařila: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Náhled:\"],\"GtmO8/\":[\"od\"],\"GtuHUQ\":[\"Přejmenovat tento kanál na serveru. Nový název uvidí všichni uživatelé.\"],\"GuGfFX\":[\"Přepnout hledání\"],\"GxkJXS\":[\"Nahrávám...\"],\"GzbwnK\":[\"Připojil se ke kanálu\"],\"GzsUDB\":[\"Rozšířený profil\"],\"H/PnT8\":[\"Vložit emoji\"],\"H6Izzl\":[\"Váš preferovaný kód barvy\"],\"H9jIv+\":[\"Zobrazit připojení/odchody\"],\"HAKBY9\":[\"Nahrát soubory\"],\"HdE1If\":[\"Kanál\"],\"Hk4AW9\":[\"Vaše preferované zobrazované jméno\"],\"HmHDk7\":[\"Vybrat člena\"],\"HrQzPU\":[\"Kanály na \",[\"networkName\"]],\"I2tXQ5\":[\"Zpráva @\",[\"0\"],\" (Enter pro nový řádek, Shift+Enter pro odeslání)\"],\"I6bw/h\":[\"Zabanovat uživatele\"],\"I92Z+b\":[\"Povolit upozornění\"],\"I9D72S\":[\"Opravdu chcete tuto zprávu smazat? Tuto akci nelze vrátit zpět.\"],\"IA+1wo\":[\"Zobrazovat, když jsou uživatelé vyhozeni z kanálů\"],\"IDwkJx\":[\"IRC operátor\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Uložit změny\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" a \",[\"2\"],\" píší...\"],\"IgrLD/\":[\"Pauza\"],\"Im6JED\":[\"ŠEPOT\"],\"ImOQa9\":[\"Odpovědět\"],\"IoHMnl\":[\"Maximální hodnota je \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Připojování...\"],\"J5T9NW\":[\"Informace o uživateli\"],\"J8Y5+z\":[\"Jejda! Síť se rozdělila! ⚠️\"],\"JBHkBA\":[\"Opustil kanál\"],\"JCwL0Q\":[\"Zadejte důvod (volitelné)\"],\"JFciKP\":[\"Přepnout\"],\"JXGkhG\":[\"Změnit název kanálu (pouze operátoři)\"],\"JcD7qf\":[\"Více akcí\"],\"JdkA+c\":[\"Tajný (+s)\"],\"Jmu12l\":[\"Kanály serveru\"],\"JoQY+E\":[\"Tím se síť odebere z vašeho soju bounceru. Abyste ji mohli znovu použít, budete ji muset přidat zpět.\"],\"JvQ++s\":[\"Povolit Markdown\"],\"K2jwh/\":[\"Data WHOIS nejsou k dispozici\"],\"KAXSwC\":[\"Hlas\"],\"KDfTdX\":[\"Smazat zprávu\"],\"KKBlUU\":[\"Vložit\"],\"KM0pLb\":[\"Vítejte v kanálu!\"],\"KR6W2h\":[\"Přestat ignorovat uživatele\"],\"KV+Bi1\":[\"Pouze na pozvání (+i)\"],\"KdCtwE\":[\"Kolik sekund sledovat floodingovou aktivitu před resetováním čítačů\"],\"Kkezga\":[\"Heslo serveru\"],\"KsiQ/8\":[\"Uživatelé musí být pozváni k připojení do kanálu\"],\"L+gB/D\":[\"Informace o kanálu\"],\"LC1a7n\":[\"IRC server oznámil, že jeho meziservery mají nízkou úroveň zabezpečení. To znamená, že když jsou vaše zprávy přeposílány mezi IRC servery v síti, nemusí být správně šifrovány nebo SSL/TLS certifikáty nemusí být správně ověřovány.\"],\"LEwpeL\":[\"soju bouncer (řízení)\"],\"LNfLR5\":[\"Zobrazit vykopnutí\"],\"LP+1Z7\":[\"Přidat síť\"],\"LQb0W/\":[\"Zobrazit všechny události\"],\"LU7/yA\":[\"Alternativní název pro zobrazení v rozhraní. Může obsahovat mezery, emoji a speciální znaky. Skutečný název kanálu (\",[\"channelName\"],\") bude nadále používán pro IRC příkazy.\"],\"LUb9O7\":[\"Je vyžadován platný port serveru\"],\"LV4fT6\":[\"Popis (volitelné, napÅ. \\\"Beta testeÅi Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Zásady ochrany soukromí\"],\"LcuSDR\":[\"Spravujte informace profilu a metadata\"],\"LqLS9B\":[\"Zobrazit změny přezdívek\"],\"LsDQt2\":[\"Nastavení kanálu\"],\"LtI9AS\":[\"Vlastník\"],\"LuNhhL\":[\"reagoval na tuto zprávu\"],\"M/AZNG\":[\"URL vašeho avatara\"],\"M/WIer\":[\"Odeslat zprávu\"],\"M8er/5\":[\"Název:\"],\"MHk+7g\":[\"Předchozí obrázek\"],\"MRorGe\":[\"Soukromá zpráva uživateli\"],\"MVbSGP\":[\"Časové okno (sekundy)\"],\"MkpcsT\":[\"Vaše zprávy a nastavení jsou uloženy lokálně na vašem zařízení\"],\"MzPdC2\":[\"Heslo serveru (PASS)\"],\"N/hDSy\":[\"Označit jako bot - obvykle 'on' nebo prázdné\"],\"N6j2JH\":[\"Upravit \",[\"0\"]],\"N7TQbE\":[\"Pozvat uživatele do \",[\"channelName\"]],\"NCca/o\":[\"Zadejte výchozí přezdívku...\"],\"Nqs6B9\":[\"Zobrazuje veškerá externí média. Libovolná URL může způsobit požadavek na neznámý server.\"],\"Nt+9O7\":[\"Použít WebSocket místo surového TCP\"],\"NxIHzc\":[\"Odpojit uživatele\"],\"O+v/cL\":[\"Procházet všechny kanály na serveru\"],\"OCGpR4\":[\"(zdědit)\"],\"ODwSCk\":[\"Odeslat GIF\"],\"OGQ5kK\":[\"Konfigurovat zvuky upozornění a zvýraznění\"],\"OIPt1Z\":[\"Zobrazit nebo skrýt boční panel se seznamem členů\"],\"OKSNq/\":[\"Velmi přísný\"],\"ONWvwQ\":[\"Nahrát\"],\"OVKoQO\":[\"Heslo vašeho účtu pro ověření\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Přinášíme IRC do budoucnosti\"],\"OhCpra\":[\"Nastavit téma…\"],\"OkltoQ\":[\"Zabanovat \",[\"username\"],\" podle přezdívky (zabrání opětovnému připojení se stejným nickem)\"],\"P+t/Te\":[\"Žádné další údaje\"],\"P42Wcc\":[\"Bezpečné\"],\"PD38l0\":[\"Náhled avatara kanálu\"],\"PD9mEt\":[\"Napište zprávu...\"],\"PPqfdA\":[\"Otevřít nastavení konfigurace kanálu\"],\"PSCjfZ\":[\"Téma, které bude zobrazeno pro tento kanál. Téma mohou vidět všichni uživatelé.\"],\"PZCecv\":[\"Náhled PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1×\"],\"few\":[[\"c\"],\"×\"],\"many\":[[\"c\"],\"×\"],\"other\":[[\"c\"],\"×\"]}]],\"PguS2C\":[\"Přidat masku výjimky (např. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Zobrazeno \",[\"displayedChannelsCount\"],\" z \",[\"0\"],\" kanálů\"],\"PqhVlJ\":[\"Zabanovat uživatele (podle masky hostitele)\"],\"Q+chwU\":[\"Uživatelské jméno:\"],\"Q2QY4/\":[\"Smazat tuto pozvánku\"],\"Q3v9Wc\":[\"Ano, smazat\"],\"Q6hhn8\":[\"Předvolby\"],\"QF4a34\":[\"Zadejte prosím uživatelské jméno\"],\"QGqSZ2\":[\"Barva a formátování\"],\"QJQd1J\":[\"Upravit profil\"],\"QSzGDE\":[\"Nečinný\"],\"QUlny5\":[\"Vítejte v \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Číst více\"],\"QuSkCF\":[\"Filtrovat kanály...\"],\"QwUrDZ\":[\"změnil téma na: \",[\"topic\"]],\"R0UH07\":[\"Obrázek \",[\"0\"],\" z \",[\"1\"]],\"R7SsBE\":[\"Ztlumit\"],\"R8rf1X\":[\"Klikněte pro nastavení tématu\"],\"RArB3D\":[\"byl vyhozen z \",[\"channelName\"],\" uživatelem \",[\"username\"]],\"RI3cWd\":[\"Objevte svět IRC s ObsidianIRC\"],\"RIfHS5\":[\"VytvoÅit nový pozvánkový odkaz\"],\"RMMaN5\":[\"Moderovaný (+m)\"],\"RWw9Lg\":[\"Zavřít okno\"],\"RZ2BuZ\":[\"Registrace účtu \",[\"account\"],\" vyžaduje ověření: \",[\"message\"]],\"RySp6q\":[\"Skrýt komentáře\"],\"S5Togi\":[\"Načítání sítí z vašeho bounceru…\"],\"SPKQTd\":[\"Přezdívka je povinná\"],\"SPVjfj\":[\"Výchozí bude 'bez důvodu', pokud ponecháte prázdné\"],\"SQKPvQ\":[\"Pozvat uživatele\"],\"STmlpb\":[\"Zpět na seznam sítí\"],\"SkZcl+\":[\"Vyberte předdefinovaný profil ochrany před floodem. Tyto profily poskytují vyvážená nastavení ochrany pro různé případy použití.\"],\"Slr+3C\":[\"Min. uživatelů\"],\"Spnlre\":[\"Pozval jste \",[\"target\"],\" k připojení do \",[\"channel\"]],\"T/ckN5\":[\"Otevřít v prohlížeči\"],\"T91vKp\":[\"Přehrát\"],\"TV2Wdu\":[\"Zjistěte, jak nakládáme s vašimi daty a chráníme vaše soukromí.\"],\"TgFpwD\":[\"Používám...\"],\"TkzSFB\":[\"Žádné změny\"],\"TtserG\":[\"Zadejte skutečné jméno\"],\"Ttz9J1\":[\"Zadejte heslo...\"],\"Tz0i8g\":[\"Nastavení\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"React\"],\"UE4KO5\":[\"*kanál*\"],\"UETAwW\":[\"ZatÃm jste nevytvoÅili žádné pozvánkové odkazy. Použijte formuláŠvýše k vytvoÅenà prvnÃho.\"],\"UGT5vp\":[\"Uložit nastavení\"],\"UV5hLB\":[\"Nenalezeny žádné zákazy\"],\"Uaj3Nd\":[\"Stavové zprávy\"],\"Ue3uny\":[\"Výchozí (bez profilu)\"],\"UkARhe\":[\"Normální - standardní ochrana\"],\"Umn7Cj\":[\"Zatím žádné komentáře. Buďte první!\"],\"UtUIRh\":[[\"0\"],\" starších zpráv\"],\"UwzP+U\":[\"Zabezpečené připojení\"],\"V0/A4O\":[\"Vlastník kanálu\"],\"V0zZWc\":[\"Také se zavře níže uvedená navázaná síť.\"],\"V4qgxE\":[\"Vytvořeno před (min. zpět)\"],\"V8yTm6\":[\"Vymazat hledání\"],\"VJMMyz\":[\"ObsidianIRC - Přinášíme IRC do budoucnosti\"],\"VJScHU\":[\"Důvod\"],\"VLsmVV\":[\"Ztlumit upozornění\"],\"VbyRUy\":[\"Komentáře\"],\"Vmx0mQ\":[\"Nastaveno:\"],\"VqnIZz\":[\"Zobrazit naše zásady ochrany soukromí a práci s daty\"],\"VrMygG\":[\"Minimální délka je \",[\"0\"]],\"VrnTui\":[\"Vaše zájmena, zobrazená ve vašem profilu\"],\"W8E3qn\":[\"Ověřený účet\"],\"WAakm9\":[\"Smazat kanál\"],\"WFxTHC\":[\"Přidat masku banu (např. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Hostitel serveru je povinný\"],\"WRYdXW\":[\"Pozice zvuku\"],\"WUOH5B\":[\"Ignorovat uživatele\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Zobrazit 1 další položku\"],\"few\":[\"Zobrazit \",[\"1\"],\" další položky\"],\"many\":[\"Zobrazit \",[\"1\"],\" dalších položek\"],\"other\":[\"Zobrazit \",[\"1\"],\" dalších položek\"]}]],\"WYxRzo\":[\"VytváÅejte a spravujte své pozvánkové odkazy\"],\"Wd38W1\":[\"Ponechte pole kanálu prázdné pro obecnou pozvánku do sÃtÄ. Popis sloužà pouze pro vaÅ¡e záznamy â viditelný je jen pro vás v tomto seznamu.\"],\"Weq9zb\":[\"Obecné\"],\"Wfj7Sk\":[\"Ztlumit nebo zapnout zvuky upozornění\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Profil uživatele\"],\"X6S3lt\":[\"Hledat nastavení, kanály, servery...\"],\"XEHan5\":[\"Přesto pokračovat\"],\"XI1+wb\":[\"Neplatný formát\"],\"XIXeuC\":[\"Zpráva @\",[\"0\"]],\"XMS+k4\":[\"Začít soukromou zprávu\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Odepnout soukromou konverzaci\"],\"Xm/s+u\":[\"Zobrazení\"],\"Xp2n93\":[\"Zobrazuje média z důvěryhodného file hostu vašeho serveru. Nejsou prováděny žádné požadavky na externí služby.\"],\"XvjC4F\":[\"Ukládám...\"],\"Y/qryO\":[\"Nebyly nalezeni žádní uživatelé odpovídající vašemu vyhledávání\"],\"YAqRpI\":[\"Registrace účtu \",[\"account\"],\" proběhla úspěšně: \",[\"message\"]],\"YEfzvP\":[\"Chráněné téma (+t)\"],\"YQOn6a\":[\"Sbalit seznam členů\"],\"YRCoE9\":[\"Operátor kanálu\"],\"YURQaF\":[\"Zobrazit profil\"],\"YdBSvr\":[\"Ovládat zobrazení médií a externího obsahu\"],\"Yj6U3V\":[\"Bez centrálního serveru:\"],\"YjvpGx\":[\"Zájmena\"],\"YqH4l4\":[\"Bez klíče\"],\"YyUPpV\":[\"Účet:\"],\"ZJSWfw\":[\"Zpráva zobrazená při odpojení od serveru\"],\"ZR1dJ4\":[\"Pozvánky\"],\"ZdWg0V\":[\"Otevřít v prohlížeči\"],\"ZhRBbl\":[\"Hledat zprávy…\"],\"Zmcu3y\":[\"Pokročilé filtry\"],\"a0bHay\":[\"Odpojit <0>\",[\"0\"],\"0>?\"],\"a2/8e5\":[\"Téma nastaveno po (min)\"],\"aHKcKc\":[\"Předchozí stránka\"],\"aJTbXX\":[\"Heslo operátora\"],\"aQryQv\":[\"Vzor již existuje\"],\"aW9pLN\":[\"Maximální počet uživatelů povolených v kanálu. Nechte prázdné pro žádný limit.\"],\"ah4fmZ\":[\"Zobrazuje také náhledy z YouTube, Vimeo, SoundCloud a podobných známých služeb.\"],\"aifXak\":[\"V tomto kanálu nejsou žádná média\"],\"ap2zBz\":[\"Uvolněný\"],\"az8lvo\":[\"Vypnuto\"],\"azXSNo\":[\"Rozbalit seznam členů\"],\"azdliB\":[\"Přihlásit se k účtu\"],\"b26wlF\":[\"ona/její\"],\"bD/+Ei\":[\"Přísný\"],\"bQ6BJn\":[\"Nakonfigurujte podrobná pravidla ochrany proti floodingu. Každé pravidlo určuje, jaký typ aktivity sledovat a jakou akci provést při překročení prahů.\"],\"beV7+y\":[\"Uživatel obdrží pozvánku k připojení do \",[\"channelName\"],\".\"],\"bk84cH\":[\"Zpráva o nepřítomnosti\"],\"bkHdLj\":[\"Přidat IRC server\"],\"bmQLn5\":[\"Přidat pravidlo\"],\"bv4cFj\":[\"Přenos\"],\"bwRvnp\":[\"Akce\"],\"c8+EVZ\":[\"Ověřený účet\"],\"cGYUlD\":[\"Nejsou načteny žádné náhledy médií.\"],\"cLF98o\":[\"Zobrazit komentáře (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Žádní uživatelé nejsou k dispozici\"],\"cSgpoS\":[\"Připnout soukromou konverzaci\"],\"cXeEKu\":[\"Také se zavřou \",[\"0\"],\" níže uvedené navázané sítě.\"],\"cde3ce\":[\"Zpráva <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Kopírovat formátovaný výstup\"],\"cl/A5J\":[\"Vítejte v \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Smazat\"],\"coPLXT\":[\"Neukládáme vaši IRC komunikaci na našich serverech\"],\"crYH/6\":[\"Přehrávač SoundCloud\"],\"cv5DQb\":[\"není nastaven hostitel\"],\"d3sis4\":[\"Přidat server\"],\"d9aN5k\":[\"Odebrat \",[\"username\"],\" z kanálu\"],\"dEgA5A\":[\"Zrušit\"],\"dGi1We\":[\"Odepnout tuto soukromou konverzaci\"],\"dJVuyC\":[\"opustil \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"do\"],\"dXqxlh\":[\"<0>⚠️ Bezpečnostní riziko!0> Toto připojení může být zranitelné vůči odposlechu nebo útokům man-in-the-middle.\"],\"da9Q/R\":[\"Změněny módy kanálu\"],\"dhJN3N\":[\"Zobrazit komentáře\"],\"dj2xTE\":[\"Odmítnout oznámení\"],\"dpCzmC\":[\"Nastavení ochrany proti floodingu\"],\"e9dQpT\":[\"Chcete otevřít tento odkaz v nové záložce?\"],\"ePK91l\":[\"Upravit\"],\"eYBDuB\":[\"Nahrajte obrázek nebo zadejte URL s volitelnou substitucí \",[\"size\"],\" pro dynamické velikosti\"],\"edBbee\":[\"Zabanovat \",[\"username\"],\" podle masky hostitele (zabrání opětovnému připojení ze stejné IP/hostitele)\"],\"ekfzWq\":[\"Nastavení uživatele\"],\"elPDWs\":[\"Přizpůsobte si IRC klienta\"],\"eu2osY\":[\"<0>💡 Doporučení:0> Pokračujte pouze pokud důvěřujete tomuto serveru a rozumíte rizikům. Vyhněte se sdílení citlivých informací nebo hesel přes toto připojení.\"],\"euEhbr\":[\"Klikněte pro připojení k \",[\"channel\"]],\"ez3vLd\":[\"Povolit víceřádkové zadávání\"],\"f0J5Ki\":[\"Komunikace mezi servery může používat nešifrovaná připojení\"],\"f9BHJk\":[\"Varovat uživatele\"],\"fDOLLd\":[\"Nebyly nalezeny žádné kanály.\"],\"ffzDkB\":[\"Anonymní analytika:\"],\"fq1GF9\":[\"Zobrazit při odpojení uživatelů ze serveru\"],\"gCldcN\":[\"Změnit barvu zvýraznění\"],\"gEF57C\":[\"Tento server podporuje pouze jeden typ připojení\"],\"gJuLUI\":[\"Seznam ignorovaných\"],\"gNzMrk\":[\"Aktuální avatar\"],\"gjPWyO\":[\"Zadejte přezdívku...\"],\"gz6UQ3\":[\"Maximalizovat\"],\"h6/IMX\":[\"Přidejte svou první síť\"],\"h6razj\":[\"Maska vyloučení názvu kanálu\"],\"hG6jnw\":[\"Téma není nastaveno\"],\"hG89Ed\":[\"Obrázek\"],\"hYgDIe\":[\"VytvoÅit\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"např. 100:1440\"],\"he3ygx\":[\"KopÃrovat\"],\"hehnjM\":[\"Množství\"],\"hzdLuQ\":[\"Mluvit mohou pouze uživatelé s hlasem nebo vyšší hodností\"],\"i0qMbr\":[\"Domů\"],\"iDNBZe\":[\"Oznámení\"],\"iH8pgl\":[\"Zpět\"],\"iL9SZg\":[\"Zabanovat uživatele (podle přezdívky)\"],\"iNt+3c\":[\"Zpět na obrázek\"],\"iQvi+a\":[\"Neupozorňovat mě na nízkou bezpečnost připojení pro tento server\"],\"iSLIjg\":[\"Připojit\"],\"iWXkHH\":[\"Polooperátor\"],\"iZeTtp\":[\"Hostitel serveru\"],\"idD8Ev\":[\"Uloženo\"],\"iivqkW\":[\"Přihlášen\"],\"ij+Elv\":[\"Náhled obrázku\"],\"ilIWp7\":[\"Přepnout oznámení\"],\"iuaqvB\":[\"Použijte * pro zástupné znaky. Příklady: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Ban podle masky hostitele\"],\"jA4uoI\":[\"Téma:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Důvod (volitelné)\"],\"jUV7CU\":[\"Nahrát avatar\"],\"jW5Uwh\":[\"Kontrolujte načítání externích médií. Vypnuto / Bezpečné / Důvěryhodné zdroje / Veškerý obsah.\"],\"jXzms5\":[\"Možnosti přílohy\"],\"jZlrte\":[\"Barva\"],\"jfC/xh\":[\"Kontakt\"],\"jywMpv\":[\"#nový-název-kanálu\"],\"k112DD\":[\"Načíst starší zprávy\"],\"k3ID0F\":[\"Filtrovat členy…\"],\"k65gsE\":[\"Podrobný přehled\"],\"k7Zgob\":[\"Zrušit připojení\"],\"kAVx5h\":[\"Nenalezeny žádné pozvánky\"],\"kCLEPU\":[\"Připojeno k\"],\"kF5LKb\":[\"Ignorované vzory:\"],\"kGeOx/\":[\"Připojit se k \",[\"0\"]],\"kITKr8\":[\"Načítám režimy kanálu...\"],\"kPpPsw\":[\"Jste IRC operátor\"],\"kWJmRL\":[\"Vy\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Kopírovat JSON\"],\"krViRy\":[\"Klikněte pro kopírování jako JSON\"],\"ks71ra\":[\"Výjimky\"],\"kw4lRv\":[\"Polooperátor kanálu\"],\"kxgIRq\":[\"Vyberte nebo přidejte kanál pro začátek.\"],\"ky6dWe\":[\"Náhled avatara\"],\"l+GxCv\":[\"Načítám kanály...\"],\"l+IUVW\":[\"Ověření účtu \",[\"account\"],\" proběhlo úspěšně: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"znovu se připojil\"],\"few\":[\"znovu se připojil \",[\"reconnectCount\"],\"×\"],\"many\":[\"znovu se připojil \",[\"reconnectCount\"],\"×\"],\"other\":[\"znovu se připojil \",[\"reconnectCount\"],\"×\"]}]],\"l1l8sj\":[\"pÅed \",[\"0\"],\" dny\"],\"l5NhnV\":[\"#kanál (volitelné)\"],\"l5jmzx\":[[\"0\"],\" a \",[\"1\"],\" píší...\"],\"lCF0wC\":[\"Obnovit\"],\"lHy8N5\":[\"Načítám více kanálů...\"],\"lasgrr\":[\"použito\"],\"lbpf14\":[\"Připojit se k \",[\"value\"]],\"lfFsZ4\":[\"Kanály\"],\"lkNdiH\":[\"Název účtu\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Nahrát obrázek\"],\"loQxaJ\":[\"Jsem zpět\"],\"lvfaxv\":[\"DOMŮ\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Přidat\"],\"m8flAk\":[\"Náhled (ještě nenahrán)\"],\"mEPxTp\":[\"<0>⚠️ Buďte opatrní!0> Otevírejte pouze odkazy z důvěryhodných zdrojů. Škodlivé odkazy mohou ohrozit vaši bezpečnost nebo soukromí.\"],\"mHGdhG\":[\"Informace o serveru\"],\"mHS8lb\":[\"Zpráva #\",[\"0\"]],\"mMYBD9\":[\"Široký - širší rozsah ochrany\"],\"mTGsPd\":[\"Téma kanálu\"],\"mU8j6O\":[\"Žádné externí zprávy (+n)\"],\"mZp8FL\":[\"Automatický návrat na jeden řádek\"],\"mdQu8G\":[\"VašePřezdívka\"],\"miSSBQ\":[\"Komentáře (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Uživatel je ověřen\"],\"mwtcGl\":[\"Zavřít komentáře\"],\"myL0MR\":[\"Smazat tuto síť?\"],\"mzI/c+\":[\"Stáhnout\"],\"n3fGRk\":[\"nastaveno \",[\"0\"]],\"n5+j9l\":[\"např. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Uvolněný - méně agresivní ochrana\"],\"nNflMD\":[\"Opustit kanál\"],\"nPXkBi\":[\"Načítám data WHOIS...\"],\"nQnxxF\":[\"Zpráva #\",[\"0\"],\" (Shift+Enter pro nový řádek)\"],\"nWMRxa\":[\"Odepnout\"],\"nkC032\":[\"Žádný profil floodingu\"],\"o69z4d\":[\"Odeslat varovnou zprávu uživateli \",[\"username\"]],\"o9ylQi\":[\"Hledejte GIFy pro začátek\"],\"oFGkER\":[\"Oznámení serveru\"],\"oOi11l\":[\"Přejít dolů\"],\"oPYIL5\":[\"sÃÅ¥\"],\"oQEzQR\":[\"Nová DM\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Útoky man-in-the-middle na serverová připojení jsou možné\"],\"oeqmmJ\":[\"Důvěryhodné zdroje\"],\"optX0N\":[\"pÅed \",[\"0\"],\" h\"],\"ovBPCi\":[\"Výchozí\"],\"p0Z69r\":[\"Vzor nemůže být prázdný\"],\"p1KgtK\":[\"Nepodařilo se načíst zvuk\"],\"p59pEv\":[\"Další podrobnosti\"],\"p7sRI6\":[\"Informovat ostatní, když píšete\"],\"pBm1od\":[\"Tajný kanál\"],\"pNmiXx\":[\"Vaše výchozí přezdívka pro všechny servery\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Heslo účtu\"],\"peNE68\":[\"Trvalý\"],\"plhHQt\":[\"Žádná data\"],\"pm6+q5\":[\"Bezpečnostní upozornění\"],\"pn5qSs\":[\"Další informace\"],\"q0cR4S\":[\"je nyní znám jako **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanál se nebude zobrazovat v příkazech LIST nebo NAMES\"],\"qLpTm/\":[\"Odebrat reakci \",[\"emoji\"]],\"qVkGWK\":[\"Připnout\"],\"qY8wNa\":[\"Domovská stránka\"],\"qb0xJ7\":[\"Použijte zástupné znaky: * odpovídá libovolné sekvenci, ? odpovídá libovolnému jednomu znaku. Příklady: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Klíč kanálu (+k)\"],\"qtoOYG\":[\"Bez omezení\"],\"r1W2AS\":[\"Obrázek z file hostu\"],\"rIPR2O\":[\"Téma nastaveno před (min)\"],\"rMMSYo\":[\"Maximální délka je \",[\"0\"]],\"rWtzQe\":[\"Síť se rozdělila a znovu připojila. ✅\"],\"rYG2u6\":[\"Prosím čekejte...\"],\"rdUucN\":[\"Náhled\"],\"rjGI/Q\":[\"Soukromí\"],\"rk8iDX\":[\"Načítám GIFy...\"],\"rn6SBY\":[\"Zrušit ztlumení\"],\"s/UKqq\":[\"Byl vykopnut z kanálu\"],\"s7oqXR\":[\"Vyberte síť\"],\"s8cATI\":[\"se připojil k \",[\"channelName\"]],\"sCO9ue\":[\"Připojení k <0>\",[\"serverName\"],\"0> má následující bezpečnostní problémy:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"je nyní znám jako **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" vás pozval k připojení do \",[\"channel\"]],\"sUBSbK\":[\"Zatím žádné nadřazené sítě.\"],\"sby+1/\":[\"Klikněte pro kopírování\"],\"sfN25C\":[\"Vaše skutečné nebo celé jméno\"],\"sliuzR\":[\"Otevřít odkaz\"],\"sqrO9R\":[\"Vlastní zmínky\"],\"sr6RdJ\":[\"Víceřádkové na Shift+Enter\"],\"swrCpB\":[\"Kanál byl přejmenován z \",[\"oldName\"],\" na \",[\"newName\"],\" uživatelem \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Pokročilé\"],\"t/YqKh\":[\"Odebrat\"],\"t47eHD\":[\"Váš jedinečný identifikátor na tomto serveru\"],\"tAkAh0\":[\"URL s volitelnou substitucí \",[\"size\"],\" pro dynamické velikosti. Příklad: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Zobrazit nebo skrýt boční panel se seznamem kanálů\"],\"tfDRzk\":[\"Uložit\"],\"tiBsJk\":[\"opustil \",[\"channelName\"]],\"tt4/UD\":[\"se odpojil (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Přezdívka {nick} je již používána, zkouším s {newNick}\"],\"u0a8B4\":[\"Ověřit jako IRC operátor pro administrativní přístup\"],\"u0rWFU\":[\"Vytvořeno po (min. zpět)\"],\"u72w3t\":[\"Uživatelé a vzory k ignorování\"],\"u7jc2L\":[\"se odpojil\"],\"uAQUqI\":[\"Stav\"],\"uB85T3\":[\"Uložení selhalo: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC servery:\"],\"ukyW4o\":[\"VaÅ¡e pozvánkové odkazy\"],\"usSSr/\":[\"Úroveň přiblížení\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Použijte Shift+Enter pro nový řádek (Enter odešle)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Bez tématu\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Jazyk\"],\"vaHYxN\":[\"Skutečné jméno\"],\"vhjbKr\":[\"Nepřítomen\"],\"w/nogd\":[[\"0\"],\" sítí\",[\"1\"],\" — vyberte jednu pro připojení\"],\"w4NYox\":[\"klient \",[\"title\"]],\"w8xQRx\":[\"Neplatná hodnota\"],\"wFjjxZ\":[\"byl vyhozen z \",[\"channelName\"],\" uživatelem \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nenalezeny žádné výjimky zákazu\"],\"wPrGnM\":[\"Správce kanálu\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Zobrazovat, když uživatelé vstupují nebo opouštějí kanály\"],\"whqZ9r\":[\"Další slova nebo fráze ke zvýraznění\"],\"wm7RV4\":[\"Zvuk oznámení\"],\"wz/Yoq\":[\"Vaše zprávy mohou být zachyceny při přeposílání mezi servery\"],\"x3+y8b\":[\"Tolik lidà se zaregistrovalo pÅes tento odkaz\"],\"xCJdfg\":[\"Vymazat\"],\"xOTzt5\":[\"právÄ teÄ\"],\"xUHRTR\":[\"Automaticky ověřit jako operátor při připojení\"],\"xWHwwQ\":[\"Bany\"],\"xYilR2\":[\"Média\"],\"xbi8D6\":[\"Tento server nepodporuje pozvánkové odkazy (capability<0>obby.world/invitation0>nenà inzerována). Můžete normálnÄ chatovat; tento panel je urÄen pro sÃtÄ pohánÄné obbyircd.\"],\"xceQrO\":[\"Jsou podporovány pouze zabezpečené websocket připojení\"],\"xdtXa+\":[\"název-kanálu\"],\"xfXC7q\":[\"Textové kanály\"],\"xlCYOE\":[\"Načítám více zpráv...\"],\"xlhswE\":[\"Minimální hodnota je \",[\"0\"]],\"xq97Ci\":[\"Přidat slovo nebo frázi...\"],\"xuRqRq\":[\"Limit klientů (+l)\"],\"xwF+7J\":[[\"0\"],\" píše...\"],\"y1eoq1\":[\"KopÃrovat odkaz\"],\"yJztBY\":[\"Smazat síť\"],\"yNeucF\":[\"Tento server nepodporuje rozšířená metadata profilu (rozšíření IRCv3 METADATA). Další pole jako avatar, zobrazované jméno a stav nejsou k dispozici.\"],\"yPlrca\":[\"Avatar kanálu\"],\"yQE2r9\":[\"Načítání\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Uživatelské jméno operátora\"],\"yYOzWD\":[\"logy\"],\"yfx9Re\":[\"Heslo IRC operátora\"],\"ygCKqB\":[\"Zastavit\"],\"ymDxJx\":[\"Uživatelské jméno IRC operátora\"],\"yrpRsQ\":[\"Seřadit podle názvu\"],\"yz7wBu\":[\"Zavřít\"],\"zJw+jA\":[\"nastavuje režim: \",[\"0\"]],\"zbymaY\":[\"pÅed \",[\"0\"],\" min\"],\"zebeLu\":[\"Zadejte uživatelské jméno operátora\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/cs/messages.po b/src/locales/cs/messages.po
index 6065ce60..155013a5 100644
--- a/src/locales/cs/messages.po
+++ b/src/locales/cs/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - Přinášíme IRC do budoucnosti"
msgid "— open in viewer"
msgstr "— otevřít v prohlížeči"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(zdědit)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(beze změny)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} a {1} píší..."
msgid "{0} is typing..."
msgstr "{0} píše..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} sítí{1} — vyberte jednu pro připojení"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -69,17 +85,17 @@ msgstr "{0}, {1}, {2} a {3} dalších píší..."
#. placeholder {0}: Math.floor(secs / 86400)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}d ago"
-msgstr "před {0} dny"
+msgstr "pÅed {0} dny"
#. placeholder {0}: Math.floor(secs / 3600)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}h ago"
-msgstr "před {0} h"
+msgstr "pÅed {0} h"
#. placeholder {0}: Math.floor(secs / 60)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}m ago"
-msgstr "před {0} min"
+msgstr "pÅed {0} min"
#: src/lib/eventGrouping.ts
msgid "{c, plural, one {1 time} other {{c} times}}"
@@ -115,7 +131,7 @@ msgstr "*spam*"
#: src/components/ui/InvitationsPanel.tsx
msgid "#channel (optional)"
-msgstr "#kanál (volitelné)"
+msgstr "#kanál (volitelné)"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "#new-channel-name"
@@ -205,6 +221,12 @@ msgstr "Přidat masku pozvánky (např. nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "Přidat IRC server"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Přidat síť"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Přidat pravidlo"
msgid "Add Server"
msgstr "Přidat server"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Přidejte svou první síť"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Další podrobnosti"
@@ -377,6 +403,10 @@ msgstr "Zpět"
msgid "Back to image"
msgstr "Zpět na obrázek"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Zpět na seznam sítí"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "Zabanovat {username} podle masky hostitele (zabrání opětovnému připojení ze stejné IP/hostitele)"
@@ -424,6 +454,10 @@ msgstr "Procházet všechny kanály na serveru"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Zrušit připojení"
msgid "Cancel reply"
msgstr "Zrušit odpověď"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Změnit barvu zvýraznění"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Změnit název kanálu (pouze operátoři)"
@@ -564,7 +603,7 @@ msgstr "Vymazat hledání"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "Klikněte znovu pro potvrzení"
+msgstr "KliknÄte znovu pro potvrzenÃ"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "Limit klientů (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -669,6 +710,7 @@ msgid "Confirm?"
msgstr "Potvrdit?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Připojit"
@@ -711,11 +753,11 @@ msgstr "Zkopírováno"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy"
-msgstr "Kopírovat"
+msgstr "KopÃrovat"
#: src/components/ui/RawLogViewer.tsx
msgid "Copy all"
-msgstr "Kopírovat vše"
+msgstr "KopÃrovat vÅ¡e"
#: src/components/message/JsonLogMessage.tsx
msgid "Copy entire JSON"
@@ -731,7 +773,7 @@ msgstr "Kopírovat JSON"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy link"
-msgstr "Kopírovat odkaz"
+msgstr "KopÃrovat odkaz"
#: src/components/ui/ExternalLinkWarningModal.tsx
msgid "Copy URL"
@@ -739,15 +781,15 @@ msgstr "Kopírovat URL"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create"
-msgstr "Vytvořit"
+msgstr "VytvoÅit"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "Vytvořit nový pozvánkový odkaz"
+msgstr "VytvoÅit nový pozvánkový odkaz"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "Vytvářejte a spravujte své pozvánkové odkazy"
+msgstr "VytváÅejte a spravujte své pozvánkové odkazy"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,27 +856,52 @@ msgstr "Smazat kanál"
msgid "Delete message"
msgstr "Smazat zprávu"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Smazat síť"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Smazat soukromý chat"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "Smazat tuto pozvánku"
+msgstr "Smazat tuto pozvánku"
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "Smazat tuto zprávu? Tuto akci nelze vrátit zpět."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Smazat tuto síť?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "Popis (volitelné, např. \"Beta testeři Q3\")"
+msgstr "Popis (volitelné, napÅ. \"Beta testeÅi Q3\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Odpojit"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "Odpojit <0>{0}0>?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Odpojit od soju bounceru?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Odpojit síť?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Objevovat"
@@ -891,20 +958,31 @@ msgstr "Stáhnout"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "Přetáhněte soubory pro nahrání"
+msgstr "PÅetáhnÄte soubory pro nahránÃ"
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "např. <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "např. 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Upravit"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "Upravit {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Upravit profil"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "DOMŮ"
msgid "Homepage"
msgstr "Domovská stránka"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Hostitel"
@@ -1285,7 +1364,7 @@ msgstr "Připojil se ke kanálu"
#: src/components/ui/InvitationsPanel.tsx
msgid "just now"
-msgstr "právě teď"
+msgstr "právÄ teÄ"
#: src/components/ui/ModerationModal.tsx
#: src/components/ui/UserContextMenu.tsx
@@ -1323,7 +1402,7 @@ msgstr "Opustit kanál"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Ponechte pole kanálu prázdné pro obecnou pozvánku do sítě. Popis slouží pouze pro vaše záznamy — viditelný je jen pro vás v tomto seznamu."
+msgstr "Ponechte pole kanálu prázdné pro obecnou pozvánku do sÃtÄ. Popis sloužà pouze pro vaÅ¡e záznamy â viditelný je jen pro vás v tomto seznamu."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "Opustil kanál"
msgid "Let others know when you are typing"
msgstr "Informovat ostatní, když píšete"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Náhled odkazu"
@@ -1375,6 +1458,10 @@ msgstr "Načítám GIFy..."
msgid "Loading more channels..."
msgstr "Načítám více kanálů..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Načítání sítí z vašeho bounceru…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "Načítám data WHOIS..."
@@ -1563,12 +1650,23 @@ msgstr "Název:"
#: src/components/ui/InvitationsPanel.tsx
msgid "network"
-msgstr "síť"
+msgstr "sÃÅ¥"
+
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Síť navázaná přes soju bouncer"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Název sítě"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Sítě na {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Nová DM"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (např. spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Žádný soubor nevybrán"
msgid "No flood profile"
msgstr "Žádný profil floodingu"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "není nastaven hostitel"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Nenalezeny žádné pozvánky"
@@ -1672,7 +1775,7 @@ msgstr "Nejsou načteny žádné náhledy médií."
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "Zatím nebyl zachycen žádný surový IRC provoz. Zkuste se připojit nebo odeslat zprávu."
+msgstr "ZatÃm nebyl zachycen žádný surový IRC provoz. Zkuste se pÅipojit nebo odeslat zprávu."
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "Žádné výsledky nenalezeny"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "Není vybrán žádný server. Nejprve zvolte server z postranního panelu; pozvánkové odkazy se spravují pro každý server zvlášť."
+msgstr "Nenà vybrán žádný server. Nejprve zvolte server z postrannÃho panelu; pozvánkové odkazy se spravujà pro každý server zvlášť."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "Téma není nastaveno"
msgid "No unread mentions or messages"
msgstr "Žádné nepřečtené zmínky ani zprávy"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Zatím žádné nadřazené sítě."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Žádní uživatelé nejsou k dispozici"
@@ -1784,6 +1891,10 @@ msgstr "Jejda! Síť se rozdělila! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Otevřít"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Otevřít nastavení konfigurace kanálu"
@@ -1887,6 +1998,10 @@ msgstr "Připnout soukromou konverzaci"
msgid "Pin this private message conversation"
msgstr "Připnout tuto soukromou konverzaci"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Prostý text"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "Soukromá zpráva uživateli"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Port"
@@ -2006,6 +2122,7 @@ msgstr "reagoval na tuto zprávu"
msgid "Read more"
msgstr "Číst více"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Důvod"
msgid "Reason (optional)"
msgstr "Důvod (volitelné)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Znovu připojit k serveru"
@@ -2095,6 +2213,7 @@ msgstr "Pravidla"
msgid "Safe"
msgstr "Bezpečné"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Přetočit"
msgid "Select a channel"
msgstr "Vybrat kanál"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Vyberte síť"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Vybrat člena"
@@ -2276,6 +2399,10 @@ msgstr "Operátoři serveru v síti by potenciálně mohli číst vaše zprávy"
msgid "Server Password"
msgstr "Heslo serveru"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Heslo serveru (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Komunikace mezi servery může používat nešifrovaná připojení"
@@ -2380,6 +2507,12 @@ msgstr "Přihlášen"
msgid "Software:"
msgstr "Software:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "soju bouncer (řízení)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Seřadit podle názvu"
@@ -2457,7 +2590,11 @@ msgstr "Platnost tohoto obrázku vypršela"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "Tolik lidí se zaregistrovalo přes tento odkaz"
+msgstr "Tolik lidà se zaregistrovalo pÅes tento odkaz"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Tím se síť odebere z vašeho soju bounceru. Abyste ji mohli znovu použít, budete ji muset přidat zpět."
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "Tento server nepodporuje rozšířená metadata profilu (rozšíření I
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Tento server nepodporuje pozvánkové odkazy (capability<0>obby.world/invitation0>není inzerována). Můžete normálně chatovat; tento panel je určen pro sítě poháněné obbyircd."
+msgstr "Tento server nepodporuje pozvánkové odkazy (capability<0>obby.world/invitation0>nenà inzerována). Můžete normálnÄ chatovat; tento panel je urÄen pro sÃtÄ pohánÄné obbyircd."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Tento server podporuje pouze jeden typ připojení"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Také se zavřou {0} níže uvedené navázané sítě."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Také se zavře níže uvedená navázaná síť."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Čas (min)"
@@ -2479,6 +2625,10 @@ msgstr "Čas (min)"
msgid "Time Window (seconds)"
msgstr "Časové okno (sekundy)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Téma:"
msgid "Total: {0}"
msgstr "Celkem: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Přenos"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Důvěryhodné zdroje"
@@ -2615,7 +2769,7 @@ msgstr "Použijte zástupné znaky: * odpovídá libovolné sekvenci, ? odpovíd
#: src/components/ui/InvitationsPanel.tsx
msgid "used"
-msgstr "použito"
+msgstr "použito"
#: src/components/message/JsonLogMessage.tsx
#: src/components/ui/UserProfileModal.tsx
@@ -2641,6 +2795,7 @@ msgstr "Profil uživatele"
msgid "User Settings"
msgstr "Nastavení uživatele"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Široký - širší rozsah ochrany"
msgid "Will default to 'no reason' if left empty"
msgstr "Výchozí bude 'bez důvodu', pokud ponecháte prázdné"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Ano, smazat"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "Máte neuložené změny. Opravdu chcete zavřít bez uložení?"
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "Zatím jste nevytvořili žádné pozvánkové odkazy. Použijte formulář výše k vytvoření prvního."
+msgstr "ZatÃm jste nevytvoÅili žádné pozvánkové odkazy. Použijte formuláŠvýše k vytvoÅenà prvnÃho."
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "Pozval jste {target} k připojení do {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Jste připojeni k <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Heslo vašeho účtu pro ověření"
@@ -2822,13 +2986,17 @@ msgstr "Heslo vašeho účtu pro ověření"
msgid "Your account username for authentication"
msgstr "Uživatelské jméno vašeho účtu pro ověření"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Váš bouncer zatím nemá žádné sítě. Přidejte jednu pro začátek."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Vaše výchozí přezdívka pro všechny servery"
#: src/components/ui/InvitationsPanel.tsx
msgid "Your invite links"
-msgstr "Vaše pozvánkové odkazy"
+msgstr "Vaše pozvánkové odkazy"
#: src/components/ui/UserSettings.tsx
msgid "Your messages and settings are stored locally on your device"
diff --git a/src/locales/de/messages.mjs b/src/locales/de/messages.mjs
index 0e7ec21c..80f124e5 100644
--- a/src/locales/de/messages.mjs
+++ b/src/locales/de/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Ungültiges Musterformat. Verwenden Sie nick!user@host (Platzhalter * erlaubt)\"],\"+6NQQA\":[\"Allgemeiner Support-Kanal\"],\"+6NyRG\":[\"Client\"],\"+K0AvT\":[\"Trennen\"],\"+cyFdH\":[\"Standardnachricht beim Als-abwesend-markieren\"],\"+mVPqU\":[\"Markdown-Formatierung in Nachrichten rendern\"],\"+vqCJH\":[\"Ihr Kontobenutzername zur Authentifizierung\"],\"+yPBXI\":[\"Datei auswählen\"],\"+zy2Nq\":[\"Typ\"],\"/09cao\":[\"Geringe Verbindungssicherheit (Stufe \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Externe Benutzer können keine Nachrichten senden\"],\"/4C8U0\":[\"Alles kopieren\"],\"/6BzZF\":[\"Mitgliederliste umschalten\"],\"/AkXyp\":[\"Bestätigen?\"],\"/TNOPk\":[\"Benutzer ist abwesend\"],\"/XQgft\":[\"Entdecken\"],\"/cF7Rs\":[\"Lautstärke\"],\"/dqduX\":[\"Nächste Seite\"],\"/fc3q4\":[\"Alle Inhalte\"],\"/kISDh\":[\"Benachrichtigungstöne aktivieren\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Töne bei Erwähnungen und Nachrichten abspielen\"],\"0/0ZGA\":[\"Kanalname-Maske\"],\"0D6j7U\":[\"Mehr über benutzerdefinierte Regeln erfahren →\"],\"0XsHcR\":[\"Benutzer rauswerfen\"],\"0ZpE//\":[\"Nach Benutzern sortieren\"],\"0bEPwz\":[\"Als abwesend setzen\"],\"0dGkPt\":[\"Kanalliste ausklappen\"],\"0gS7M5\":[\"Anzeigename\"],\"0kS+M8\":[\"BeispielNET\"],\"0rgoY7\":[\"Nur mit ausgewählten Servern verbinden\"],\"0wdd7X\":[\"Beitreten\"],\"0wkVYx\":[\"Privatnachrichten\"],\"111uHX\":[\"Link-Vorschau\"],\"196EG4\":[\"Privatnachricht löschen\"],\"1DSr1i\":[\"Konto registrieren\"],\"1O/24y\":[\"Kanalliste umschalten\"],\"1VPJJ2\":[\"Warnung: Externer Link\"],\"1ZC/dv\":[\"Keine ungelesenen Erwähnungen oder Nachrichten\"],\"1pO1zi\":[\"Servername ist erforderlich\"],\"1uwfzQ\":[\"Kanalthema anzeigen\"],\"268g7c\":[\"Anzeigenamen eingeben\"],\"2F9+AZ\":[\"Noch kein roher IRC-Verkehr erfasst. Versuche dich zu verbinden oder eine Nachricht zu senden.\"],\"2FOFq1\":[\"Server-Operatoren im Netzwerk könnten deine Nachrichten lesen\"],\"2FYpfJ\":[\"Mehr\"],\"2HF1Y2\":[[\"inviter\"],\" hat \",[\"target\"],\" eingeladen, \",[\"channel\"],\" beizutreten\"],\"2I70QL\":[\"Benutzerprofilinformationen anzeigen\"],\"2QYdmE\":[\"Benutzer:\"],\"2QpEjG\":[\"hat verlassen\"],\"2YE223\":[\"Nachricht an #\",[\"0\"],\" (Enter für neue Zeile, Shift+Enter zum Senden)\"],\"2bimFY\":[\"Server-Passwort verwenden\"],\"2iTmdZ\":[\"Lokaler Speicher:\"],\"2odkwe\":[\"Streng – Aggressiverer Schutz\"],\"2uDhbA\":[\"Benutzername zum Einladen eingeben\"],\"2ygf/L\":[\"← Zurück\"],\"2zEgxj\":[\"GIFs suchen...\"],\"3RdPhl\":[\"Kanal umbenennen\"],\"3THokf\":[\"Benutzer mit Sprachrecht\"],\"3TSz9S\":[\"Minimieren\"],\"3jBDvM\":[\"Kanal-Anzeigename\"],\"3ryuFU\":[\"Optionale Absturzberichte zur App-Verbesserung\"],\"3uBF/8\":[\"Ansicht schließen\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Kontoname eingeben...\"],\"4/Rr0R\":[\"Benutzer in den aktuellen Kanal einladen\"],\"4EZrJN\":[\"Regeln\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Flood-Profil (+F)\"],\"4RZQRK\":[\"Was machst du gerade?\"],\"4hfTrB\":[\"Nickname\"],\"4n99LO\":[\"Bereits in \",[\"0\"]],\"4t6vMV\":[\"Kurze Nachrichten automatisch einzeilig darstellen\"],\"4vsHmf\":[\"Zeit (Min)\"],\"5+INAX\":[\"Nachrichten hervorheben, die Sie erwähnen\"],\"5R5Pv/\":[\"Oper Name\"],\"678PKt\":[\"Netzwerkname\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Passwort zum Beitreten erforderlich. Leer lassen, um den Schlüssel zu entfernen.\"],\"6HhMs3\":[\"Abgangsnachricht\"],\"6V3Ea3\":[\"Kopiert\"],\"6lGV3K\":[\"Weniger anzeigen\"],\"6yFOEi\":[\"Oper-Passwort eingeben...\"],\"7+IHTZ\":[\"Keine Datei ausgewählt\"],\"73hrRi\":[\"nick!user@host (z.B. spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Privatnachricht senden\"],\"7U1W7c\":[\"Sehr locker\"],\"7Y1YQj\":[\"Echter Name:\"],\"7YHArF\":[\"— im Viewer öffnen\"],\"7fjnVl\":[\"Benutzer suchen...\"],\"7jL88x\":[\"Diese Nachricht löschen? Dies kann nicht rückgängig gemacht werden.\"],\"7nGhhM\":[\"Was denkst du gerade?\"],\"7sEpu1\":[\"Mitglieder — \",[\"0\"]],\"7sNhEz\":[\"Benutzername\"],\"8H0Q+x\":[\"Mehr über Profile erfahren →\"],\"8Phu0A\":[\"Anzeigen, wenn Benutzer ihren Nickname ändern\"],\"8XTG9e\":[\"oper-Passwort eingeben\"],\"8XsV2J\":[\"Erneut senden\"],\"8ZsakT\":[\"Passwort\"],\"8kR84m\":[\"Du bist dabei, einen externen Link zu öffnen:\"],\"8lCgih\":[\"Regel entfernen\"],\"8o3dPc\":[\"Dateien hier ablegen zum Hochladen\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"beigetreten\"],\"other\":[[\"joinCount\"],\"-mal beigetreten\"]}]],\"9BMLnJ\":[\"Erneut mit Server verbinden\"],\"9OEgyT\":[\"Reaktion hinzufügen\"],\"9PQ8m2\":[\"G-Line (globaler Ban)\"],\"9Qs99X\":[\"E-Mail:\"],\"9QupBP\":[\"Muster entfernen\"],\"9bG48P\":[\"Wird gesendet\"],\"9f5f0u\":[\"Fragen zum Datenschutz? Kontaktieren Sie uns:\"],\"9unqs3\":[\"Abwesend:\"],\"9v3hwv\":[\"Keine Server gefunden.\"],\"9zb2WA\":[\"Verbinden...\"],\"A1taO8\":[\"Suchen\"],\"A2adVi\":[\"Tipp-Benachrichtigungen senden\"],\"A9Rhec\":[\"Kanalname\"],\"AWOSPo\":[\"Vergrößern\"],\"AXSpEQ\":[\"Oper beim Verbinden\"],\"AeXO77\":[\"Konto\"],\"AhNP40\":[\"Vor-/Zurückspulen\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Nickname geändert\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Antwort abbrechen\"],\"ApSx0O\":[[\"0\"],\" Nachrichten gefunden, die zu \\\"\",[\"searchQuery\"],\"\\\" passen\"],\"AxPAXW\":[\"Keine Ergebnisse gefunden\"],\"AyNqAB\":[\"Alle Serverereignisse im Chat anzeigen\"],\"B/QqGw\":[\"Nicht am Rechner\"],\"B8AaMI\":[\"Dieses Feld ist erforderlich\"],\"BA2c49\":[\"Server unterstützt keine erweiterte LIST-Filterung\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" und \",[\"3\"],\" weitere tippen...\"],\"BGul2A\":[\"Du hast ungespeicherte Änderungen. Möchtest du wirklich schließen, ohne zu speichern?\"],\"BIf9fi\":[\"Ihre Statusnachricht\"],\"BPm98R\":[\"Kein Server ausgewählt. Wähle zuerst einen Server in der Seitenleiste; Einladungslinks werden pro Server verwaltet.\"],\"BZz3md\":[\"Ihre persönliche Website\"],\"Bgm/H7\":[\"Mehrzeilige Texteingabe erlauben\"],\"BiQIl1\":[\"Dieses Privatgespräch anheften\"],\"BlNZZ2\":[\"Klicken, um zur Nachricht zu springen\"],\"Bowq3c\":[\"Nur Operatoren können das Kanalthema ändern\"],\"Btozzp\":[\"Dieses Bild ist abgelaufen\"],\"Bycfjm\":[\"Gesamt: \",[\"0\"]],\"C6IBQc\":[\"Gesamtes JSON kopieren\"],\"C9L9wL\":[\"Datenerfassung\"],\"CDq4wC\":[\"Benutzer moderieren\"],\"CHVRxG\":[\"Nachricht an @\",[\"0\"],\" (Shift+Enter für neue Zeile)\"],\"CN9zdR\":[\"Oper-Name und Passwort sind erforderlich\"],\"CW3sYa\":[\"Reaktion \",[\"emoji\"],\" hinzufügen\"],\"CaAkqd\":[\"Verbindungstrennungen anzeigen\"],\"CbvaYj\":[\"Nach Nickname sperren\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Kanal auswählen\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"System\"],\"D28t6+\":[\"ist beigetreten und gegangen\"],\"DB8zMK\":[\"Anwenden\"],\"DBcWHr\":[\"Benutzerdefinierte Benachrichtigungstondatei\"],\"DTy9Xw\":[\"Medienvorschauen\"],\"Dj4pSr\":[\"Sicheres Passwort wählen\"],\"Du+zn+\":[\"Suche...\"],\"Du2T2f\":[\"Einstellung nicht gefunden\"],\"DwsSVQ\":[\"Filter anwenden & Aktualisieren\"],\"E3W/zd\":[\"Standard-Nickname\"],\"E6nRW7\":[\"URL kopieren\"],\"E703RG\":[\"Modi:\"],\"EAeu1Z\":[\"Einladung senden\"],\"EFKJQT\":[\"Einstellung\"],\"EGPQBv\":[\"Benutzerdefinierte Flood-Regeln (+f)\"],\"ELik0r\":[\"Vollständige Datenschutzrichtlinie anzeigen\"],\"EPbeC2\":[\"Kanalthema anzeigen oder bearbeiten\"],\"EQCDNT\":[\"Oper-Benutzernamen eingeben...\"],\"EUvulZ\":[\"1 Nachricht gefunden, die zu \\\"\",[\"searchQuery\"],\"\\\" passt\"],\"EatZYJ\":[\"Nächstes Bild\"],\"EdQY6l\":[\"Keine\"],\"EnqLYU\":[\"Server suchen...\"],\"F0OKMc\":[\"Server bearbeiten\"],\"F6Int2\":[\"Hervorhebungen aktivieren\"],\"FDoLyE\":[\"Max. Benutzer\"],\"FUU/hZ\":[\"Steuert, wie viele externe Medien im Chat geladen werden.\"],\"Fdp03t\":[\"an\"],\"FfPWR0\":[\"Modal\"],\"FjkaiT\":[\"Verkleinern\"],\"FlqOE9\":[\"Was das bedeutet:\"],\"FolHNl\":[\"Konto und Authentifizierung verwalten\"],\"Fp2Dif\":[\"Den Server verlassen\"],\"G5KmCc\":[\"GZ-Line (globale Z-Line)\"],\"GDs0lz\":[\"<0>Risiko:0> Sensible Informationen (Nachrichten, private Gespräche, Authentifizierungsdaten) könnten Netzwerkadministratoren oder Angreifern zwischen IRC-Servern zugänglich sein.\"],\"GR+2I3\":[\"Einladungs-Maske hinzufügen (z.B. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Server-Hinweise schließen\"],\"GdhD7H\":[\"Erneut klicken zum Bestätigen\"],\"GlHnXw\":[\"Nicknamewechsel fehlgeschlagen: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Vorschau:\"],\"GtmO8/\":[\"von\"],\"GtuHUQ\":[\"Diesen Kanal auf dem Server umbenennen. Alle Benutzer sehen den neuen Namen.\"],\"GuGfFX\":[\"Suche umschalten\"],\"GxkJXS\":[\"Wird hochgeladen...\"],\"GzbwnK\":[\"Dem Kanal beigetreten\"],\"GzsUDB\":[\"Erweitertes Profil\"],\"H/PnT8\":[\"Emoji einfügen\"],\"H6Izzl\":[\"Ihr bevorzugter Farbcode\"],\"H9jIv+\":[\"Beitritte/Abgänge anzeigen\"],\"HAKBY9\":[\"Dateien hochladen\"],\"HdE1If\":[\"Kanal\"],\"Hk4AW9\":[\"Ihr bevorzugter Anzeigename\"],\"HmHDk7\":[\"Mitglied auswählen\"],\"HrQzPU\":[\"Kanäle auf \",[\"networkName\"]],\"I2tXQ5\":[\"Nachricht an @\",[\"0\"],\" (Enter für neue Zeile, Shift+Enter zum Senden)\"],\"I6bw/h\":[\"Benutzer sperren\"],\"I92Z+b\":[\"Benachrichtigungen aktivieren\"],\"I9D72S\":[\"Bist du sicher, dass du diese Nachricht löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.\"],\"IA+1wo\":[\"Anzeigen, wenn Benutzer aus Kanälen gekickt werden\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Änderungen speichern\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" und \",[\"2\"],\" tippen...\"],\"IgrLD/\":[\"Pause\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Antworten\"],\"IoHMnl\":[\"Maximalwert ist \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Verbinde...\"],\"J5T9NW\":[\"Benutzerinformationen\"],\"J8Y5+z\":[\"Ups! Netz-Split! ⚠️\"],\"JBHkBA\":[\"Den Kanal verlassen\"],\"JCwL0Q\":[\"Grund eingeben (optional)\"],\"JFciKP\":[\"Umschalten\"],\"JXGkhG\":[\"Kanalnamen ändern (nur Operatoren)\"],\"JcD7qf\":[\"Weitere Aktionen\"],\"JdkA+c\":[\"Geheim (+s)\"],\"Jmu12l\":[\"Serverkanäle\"],\"JvQ++s\":[\"Markdown aktivieren\"],\"K2jwh/\":[\"Keine WHOIS-Daten verfügbar\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Nachricht löschen\"],\"KKBlUU\":[\"Einbetten\"],\"KM0pLb\":[\"Willkommen im Kanal!\"],\"KR6W2h\":[\"Benutzer nicht mehr ignorieren\"],\"KV+Bi1\":[\"Nur auf Einladung (+i)\"],\"KdCtwE\":[\"Wie viele Sekunden Flood-Aktivität überwacht wird, bevor die Zähler zurückgesetzt werden\"],\"Kkezga\":[\"Server-Passwort\"],\"KsiQ/8\":[\"Benutzer müssen eingeladen werden\"],\"L+gB/D\":[\"Kanalinformationen\"],\"LC1a7n\":[\"Der IRC-Server hat gemeldet, dass seine Server-zu-Server-Verbindungen ein niedriges Sicherheitsniveau aufweisen. Das bedeutet, dass deine Nachrichten beim Weiterleiten zwischen IRC-Servern im Netzwerk möglicherweise nicht ordnungsgemäß verschlüsselt sind oder die SSL/TLS-Zertifikate nicht korrekt validiert werden.\"],\"LNfLR5\":[\"Kicks anzeigen\"],\"LQb0W/\":[\"Alle Ereignisse anzeigen\"],\"LU7/yA\":[\"Alternativer Anzeigename. Kann Leerzeichen, Emojis und Sonderzeichen enthalten. Der echte Kanalname (\",[\"channelName\"],\") wird weiterhin für IRC-Befehle verwendet.\"],\"LUb9O7\":[\"Ein gültiger Server-Port ist erforderlich\"],\"LV4fT6\":[\"Beschreibung (optional, z.B. \\\"Beta-Tester Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Datenschutzrichtlinie\"],\"LcuSDR\":[\"Profilinformationen und Metadaten verwalten\"],\"LqLS9B\":[\"Nickwechsel anzeigen\"],\"LsDQt2\":[\"Kanaleinstellungen\"],\"LtI9AS\":[\"Eigentümer\"],\"LuNhhL\":[\"hat auf diese Nachricht reagiert\"],\"M/AZNG\":[\"URL zu Ihrem Avatar-Bild\"],\"M/WIer\":[\"Nachricht senden\"],\"M8er/5\":[\"Name:\"],\"MHk+7g\":[\"Vorheriges Bild\"],\"MRorGe\":[\"Benutzer anschreiben\"],\"MVbSGP\":[\"Zeitfenster (Sekunden)\"],\"MkpcsT\":[\"Ihre Nachrichten und Einstellungen werden lokal gespeichert\"],\"N/hDSy\":[\"Als Bot markieren – normalerweise 'on' oder leer\"],\"N7TQbE\":[\"Benutzer zu \",[\"channelName\"],\" einladen\"],\"NCca/o\":[\"Standard-Spitznamen eingeben...\"],\"Nqs6B9\":[\"Zeigt alle externen Medien. Jede URL kann eine Anfrage an einen unbekannten Server auslösen.\"],\"Nt+9O7\":[\"WebSocket statt rohem TCP verwenden\"],\"NxIHzc\":[\"Benutzer trennen\"],\"O+v/cL\":[\"Alle Kanäle auf dem Server durchsuchen\"],\"ODwSCk\":[\"GIF senden\"],\"OGQ5kK\":[\"Benachrichtigungstöne und Hervorhebungen konfigurieren\"],\"OIPt1Z\":[\"Seitenleiste der Mitgliederliste ein- oder ausblenden\"],\"OKSNq/\":[\"Sehr streng\"],\"ONWvwQ\":[\"Hochladen\"],\"OVKoQO\":[\"Ihr Kontopasswort zur Authentifizierung\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - IRC in die Zukunft bringen\"],\"OhCpra\":[\"Thema setzen…\"],\"OkltoQ\":[[\"username\"],\" per Nickname sperren (verhindert erneutes Beitreten mit demselben Nick)\"],\"P+t/Te\":[\"Keine weiteren Daten\"],\"P42Wcc\":[\"Sicher\"],\"PD38l0\":[\"Kanal-Avatar-Vorschau\"],\"PD9mEt\":[\"Nachricht eingeben...\"],\"PPqfdA\":[\"Kanaleinstellungen öffnen\"],\"PSCjfZ\":[\"Das Thema für diesen Kanal. Alle Benutzer können es sehen.\"],\"PZCecv\":[\"PDF-Vorschau\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 Mal\"],\"other\":[[\"c\"],\" Mal\"]}]],\"PguS2C\":[\"Ausnahme-Maske hinzufügen (z.B. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[[\"displayedChannelsCount\"],\" von \",[\"0\"],\" Kanälen angezeigt\"],\"PqhVlJ\":[\"Benutzer sperren (per Hostmask)\"],\"Q+chwU\":[\"Benutzername:\"],\"Q2QY4/\":[\"Diese Einladung löschen\"],\"Q6hhn8\":[\"Einstellungen\"],\"QF4a34\":[\"Bitte gib einen Benutzernamen ein\"],\"QGqSZ2\":[\"Farbe & Formatierung\"],\"QJQd1J\":[\"Profil bearbeiten\"],\"QSzGDE\":[\"Inaktiv\"],\"QUlny5\":[\"Willkommen bei \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Mehr lesen\"],\"QuSkCF\":[\"Kanäle filtern...\"],\"QwUrDZ\":[\"hat das Thema geändert zu: \",[\"topic\"]],\"R0UH07\":[\"Bild \",[\"0\"],\" von \",[\"1\"]],\"R7SsBE\":[\"Stumm schalten\"],\"R8rf1X\":[\"Klicken, um das Thema zu setzen\"],\"RArB3D\":[\"wurde von \",[\"username\"],\" aus \",[\"channelName\"],\" gekickt\"],\"RI3cWd\":[\"Entdecke die Welt von IRC mit ObsidianIRC\"],\"RIfHS5\":[\"Neuen Einladungslink erstellen\"],\"RMMaN5\":[\"Moderiert (+m)\"],\"RWw9Lg\":[\"Fenster schließen\"],\"RZ2BuZ\":[\"Kontoregistrierung für \",[\"account\"],\" erfordert Verifizierung: \",[\"message\"]],\"RySp6q\":[\"Kommentare ausblenden\"],\"SPKQTd\":[\"Nickname ist erforderlich\"],\"SPVjfj\":[\"Standardmäßig 'kein Grund', wenn leer gelassen\"],\"SQKPvQ\":[\"Benutzer einladen\"],\"SkZcl+\":[\"Wähle ein vordefiniertes Flood-Schutzprofil. Diese Profile bieten ausgewogene Schutzeinstellungen für verschiedene Anwendungsfälle.\"],\"Slr+3C\":[\"Min. Benutzer\"],\"Spnlre\":[\"Du hast \",[\"target\"],\" eingeladen, \",[\"channel\"],\" beizutreten\"],\"T/ckN5\":[\"Im Viewer öffnen\"],\"T91vKp\":[\"Abspielen\"],\"TV2Wdu\":[\"Erfahren Sie, wie wir Ihre Daten verwalten und Ihre Privatsphäre schützen.\"],\"TgFpwD\":[\"Wird angewendet...\"],\"TkzSFB\":[\"Keine Änderungen\"],\"TtserG\":[\"Echten Namen eingeben\"],\"Ttz9J1\":[\"Passwort eingeben...\"],\"Tz0i8g\":[\"Einstellungen\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reagieren\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Du hast noch keine Einladungslinks erstellt. Verwende das Formular oben, um deinen ersten zu erstellen.\"],\"UGT5vp\":[\"Einstellungen speichern\"],\"UV5hLB\":[\"Keine Sperren gefunden\"],\"Uaj3Nd\":[\"Statusnachrichten\"],\"Ue3uny\":[\"Standard (kein Profil)\"],\"UkARhe\":[\"Normal – Standardschutz\"],\"Umn7Cj\":[\"Noch keine Kommentare. Sei der Erste!\"],\"UtUIRh\":[[\"0\"],\" ältere Nachrichten\"],\"UwzP+U\":[\"Sichere Verbindung\"],\"V0/A4O\":[\"Kanalbesitzer\"],\"V4qgxE\":[\"Erstellt vor (Min.)\"],\"V8yTm6\":[\"Suche löschen\"],\"VJMMyz\":[\"ObsidianIRC - IRC in die Zukunft bringen\"],\"VJScHU\":[\"Grund\"],\"VLsmVV\":[\"Benachrichtigungen stummschalten\"],\"VbyRUy\":[\"Kommentare\"],\"Vmx0mQ\":[\"Gesetzt von:\"],\"VqnIZz\":[\"Datenschutzrichtlinie und Datenpraktiken anzeigen\"],\"VrMygG\":[\"Mindestlänge ist \",[\"0\"]],\"VrnTui\":[\"Ihre Pronomen, im Profil angezeigt\"],\"W8E3qn\":[\"Authentifiziertes Konto\"],\"WAakm9\":[\"Kanal löschen\"],\"WFxTHC\":[\"Bann-Maske hinzufügen (z.B. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Server-Host ist erforderlich\"],\"WRYdXW\":[\"Audioposition\"],\"WUOH5B\":[\"Benutzer ignorieren\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"1 weiteres Element anzeigen\"],\"other\":[[\"1\"],\" weitere Elemente anzeigen\"]}]],\"WYxRzo\":[\"Einladungslinks erstellen und verwalten\"],\"Wd38W1\":[\"Lass das Kanalfeld leer für eine allgemeine Netzwerkeinladung. Die Beschreibung ist nur für deine Aufzeichnungen — sie ist nur für dich in dieser Liste sichtbar.\"],\"Weq9zb\":[\"Allgemein\"],\"Wfj7Sk\":[\"Benachrichtigungstöne stummschalten oder aktivieren\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Benutzerprofil\"],\"X6S3lt\":[\"Einstellungen, Kanäle, Server suchen...\"],\"XEHan5\":[\"Trotzdem fortfahren\"],\"XI1+wb\":[\"Ungültiges Format\"],\"XIXeuC\":[\"Nachricht an @\",[\"0\"]],\"XMS+k4\":[\"Privatnachricht starten\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Privatnachricht loslösen\"],\"Xm/s+u\":[\"Anzeige\"],\"Xp2n93\":[\"Zeigt Medien vom vertrauenswürdigen Datei-Host deines Servers. Es werden keine Anfragen an externe Dienste gestellt.\"],\"XvjC4F\":[\"Wird gespeichert...\"],\"Y/qryO\":[\"Keine Benutzer gefunden, die deiner Suche entsprechen\"],\"YAqRpI\":[\"Kontoregistrierung für \",[\"account\"],\" erfolgreich: \",[\"message\"]],\"YEfzvP\":[\"Geschütztes Thema (+t)\"],\"YQOn6a\":[\"Mitgliederliste einklappen\"],\"YRCoE9\":[\"Kanal-Operator\"],\"YURQaF\":[\"Profil anzeigen\"],\"YdBSvr\":[\"Medienanzeige und externe Inhalte steuern\"],\"Yj6U3V\":[\"Kein zentraler Server:\"],\"YjvpGx\":[\"Pronomen\"],\"YqH4l4\":[\"Kein Schlüssel\"],\"YyUPpV\":[\"Konto:\"],\"ZJSWfw\":[\"Nachricht beim Trennen vom Server\"],\"ZR1dJ4\":[\"Einladungen\"],\"ZdWg0V\":[\"Im Browser öffnen\"],\"ZhRBbl\":[\"Nachrichten suchen…\"],\"Zmcu3y\":[\"Erweiterte Filter\"],\"a2/8e5\":[\"Thema gesetzt nach (Min.)\"],\"aHKcKc\":[\"Vorherige Seite\"],\"aJTbXX\":[\"Oper Password\"],\"aQryQv\":[\"Muster existiert bereits\"],\"aW9pLN\":[\"Maximale Anzahl der zugelassenen Benutzer. Leer lassen für kein Limit.\"],\"ah4fmZ\":[\"Zeigt auch Vorschauen von YouTube, Vimeo, SoundCloud und ähnlichen bekannten Diensten.\"],\"aifXak\":[\"Keine Medien in diesem Kanal\"],\"ap2zBz\":[\"Locker\"],\"az8lvo\":[\"Aus\"],\"azXSNo\":[\"Mitgliederliste ausklappen\"],\"azdliB\":[\"Bei einem Konto anmelden\"],\"b26wlF\":[\"sie/ihr\"],\"bD/+Ei\":[\"Streng\"],\"bQ6BJn\":[\"Detaillierte Flood-Schutzregeln konfigurieren. Jede Regel legt fest, welche Aktivitäten überwacht werden sollen und welche Maßnahmen bei Überschreitung der Schwellenwerte ergriffen werden.\"],\"beV7+y\":[\"Der Benutzer erhält eine Einladung, \",[\"channelName\"],\" beizutreten.\"],\"bk84cH\":[\"Abwesenheitsnachricht\"],\"bkHdLj\":[\"IRC-Server hinzufügen\"],\"bmQLn5\":[\"Regel hinzufügen\"],\"bwRvnp\":[\"Aktion\"],\"c8+EVZ\":[\"Verifiziertes Konto\"],\"cGYUlD\":[\"Es werden keine Medienvorschauen geladen.\"],\"cLF98o\":[\"Kommentare anzeigen (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Keine Benutzer verfügbar\"],\"cSgpoS\":[\"Privatnachricht anheften\"],\"cde3ce\":[\"Nachricht an <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Formatierte Ausgabe kopieren\"],\"cl/A5J\":[\"Willkommen bei \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Löschen\"],\"coPLXT\":[\"Wir speichern Ihre IRC-Kommunikation nicht auf unseren Servern\"],\"crYH/6\":[\"SoundCloud-Player\"],\"d3sis4\":[\"Server hinzufügen\"],\"d9aN5k\":[[\"username\"],\" aus dem Kanal entfernen\"],\"dEgA5A\":[\"Abbrechen\"],\"dGi1We\":[\"Dieses Privatgespräch loslösen\"],\"dJVuyC\":[\"hat \",[\"channelName\"],\" verlassen (\",[\"reason\"],\")\"],\"dMtLDE\":[\"an\"],\"dXqxlh\":[\"<0>⚠️ Sicherheitsrisiko!0> Diese Verbindung könnte anfällig für Abhören oder Man-in-the-Middle-Angriffe sein.\"],\"da9Q/R\":[\"Kanalmodi geändert\"],\"dhJN3N\":[\"Kommentare anzeigen\"],\"dj2xTE\":[\"Benachrichtigung schließen\"],\"dpCzmC\":[\"Flood-Schutz-Einstellungen\"],\"e9dQpT\":[\"Möchtest du diesen Link in einem neuen Tab öffnen?\"],\"ePK91l\":[\"Bearbeiten\"],\"eYBDuB\":[\"Bild hochladen oder URL mit optionaler \",[\"size\"],\"-Substitution angeben\"],\"edBbee\":[[\"username\"],\" per hostmask sperren (verhindert erneutes Beitreten von derselben IP/Host)\"],\"ekfzWq\":[\"Benutzereinstellungen\"],\"elPDWs\":[\"IRC-Client-Erfahrung anpassen\"],\"eu2osY\":[\"<0>💡 Empfehlung:0> Fahre nur fort, wenn du diesem Server vertraust und die Risiken kennst. Teile keine sensiblen Informationen oder Passwörter über diese Verbindung.\"],\"euEhbr\":[\"Klicke, um \",[\"channel\"],\" beizutreten\"],\"ez3vLd\":[\"Mehrzeilige Eingabe aktivieren\"],\"f0J5Ki\":[\"Die Server-zu-Server-Kommunikation verwendet möglicherweise unverschlüsselte Verbindungen\"],\"f9BHJk\":[\"Benutzer warnen\"],\"fDOLLd\":[\"Keine Kanäle gefunden.\"],\"ffzDkB\":[\"Anonyme Analysen:\"],\"fq1GF9\":[\"Anzeigen, wenn Benutzer die Verbindung trennen\"],\"gEF57C\":[\"Dieser Server unterstützt nur einen Verbindungstyp\"],\"gJuLUI\":[\"Ignorierliste\"],\"gNzMrk\":[\"Aktueller Avatar\"],\"gjPWyO\":[\"Spitznamen eingeben...\"],\"gz6UQ3\":[\"Maximieren\"],\"h6razj\":[\"Kanalname-Maske ausschließen\"],\"hG6jnw\":[\"Kein Thema gesetzt\"],\"hG89Ed\":[\"Bild\"],\"hYgDIe\":[\"Erstellen\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"z.B. 100:1440\"],\"he3ygx\":[\"Kopieren\"],\"hehnjM\":[\"Anzahl\"],\"hzdLuQ\":[\"Nur Benutzer mit Voice oder höher können sprechen\"],\"i0qMbr\":[\"Startseite\"],\"iDNBZe\":[\"Benachrichtigungen\"],\"iH8pgl\":[\"Zurück\"],\"iL9SZg\":[\"Benutzer sperren (per Nickname)\"],\"iNt+3c\":[\"Zurück zum Bild\"],\"iQvi+a\":[\"Nicht mehr vor geringer Verbindungssicherheit für diesen Server warnen\"],\"iSLIjg\":[\"Verbinden\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Server-Host\"],\"idD8Ev\":[\"Gespeichert\"],\"iivqkW\":[\"Angemeldet seit\"],\"ij+Elv\":[\"Bildvorschau\"],\"ilIWp7\":[\"Benachrichtigungen umschalten\"],\"iuaqvB\":[\"* als Platzhalter verwenden. Beispiele: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Nach Hostmaske sperren\"],\"jA4uoI\":[\"Thema:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Grund (optional)\"],\"jUV7CU\":[\"Avatar hochladen\"],\"jW5Uwh\":[\"Steuert, wie viele externe Medien geladen werden. Aus / Sicher / Vertrauenswürdige / Alle Inhalte.\"],\"jXzms5\":[\"Anhangsoptionen\"],\"jZlrte\":[\"Farbe\"],\"jfC/xh\":[\"Kontakt\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Ältere Nachrichten laden\"],\"k3ID0F\":[\"Mitglieder filtern…\"],\"k65gsE\":[\"Vertieft ansehen\"],\"k7Zgob\":[\"Verbindung abbrechen\"],\"kAVx5h\":[\"Keine Einladungen gefunden\"],\"kCLEPU\":[\"Verbunden mit\"],\"kF5LKb\":[\"Ignorierte Muster:\"],\"kGeOx/\":[[\"0\"],\" beitreten\"],\"kITKr8\":[\"Kanal-Modi werden geladen...\"],\"kPpPsw\":[\"Du bist ein IRC Operator\"],\"kWJmRL\":[\"Du\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"JSON kopieren\"],\"krViRy\":[\"Klicken zum Kopieren als JSON\"],\"ks71ra\":[\"Ausnahmen\"],\"kw4lRv\":[\"Kanal-Halboperator\"],\"kxgIRq\":[\"Kanal auswählen oder hinzufügen, um zu beginnen.\"],\"ky6dWe\":[\"Avatar-Vorschau\"],\"l+GxCv\":[\"Kanäle werden geladen...\"],\"l+IUVW\":[\"Kontoverifizierung für \",[\"account\"],\" erfolgreich: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"erneut verbunden\"],\"other\":[[\"reconnectCount\"],\"-mal erneut verbunden\"]}]],\"l1l8sj\":[\"vor \",[\"0\"],\" T.\"],\"l5NhnV\":[\"#kanal (optional)\"],\"l5jmzx\":[[\"0\"],\" und \",[\"1\"],\" tippen...\"],\"lCF0wC\":[\"Aktualisieren\"],\"lHy8N5\":[\"Weitere Kanäle werden geladen...\"],\"lasgrr\":[\"verwendet\"],\"lbpf14\":[[\"value\"],\" beitreten\"],\"lfFsZ4\":[\"Kanäle\"],\"lkNdiH\":[\"Kontoname\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Bild hochladen\"],\"loQxaJ\":[\"Ich bin zurück\"],\"lvfaxv\":[\"STARTSEITE\"],\"m16xKo\":[\"Hinzufügen\"],\"m8flAk\":[\"Vorschau (noch nicht hochgeladen)\"],\"mEPxTp\":[\"<0>⚠️ Vorsicht!0> Öffne nur Links aus vertrauenswürdigen Quellen. Bösartige Links können deine Sicherheit oder Privatsphäre gefährden.\"],\"mHGdhG\":[\"Serverinformationen\"],\"mHS8lb\":[\"Nachricht an #\",[\"0\"]],\"mMYBD9\":[\"Weit – Breiterer Schutzbereich\"],\"mTGsPd\":[\"Kanalthema\"],\"mU8j6O\":[\"Keine externen Nachrichten (+n)\"],\"mZp8FL\":[\"Automatisch auf einzeilig wechseln\"],\"mdQu8G\":[\"DeinNickname\"],\"miSSBQ\":[\"Kommentare (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Benutzer ist authentifiziert\"],\"mwtcGl\":[\"Kommentare schließen\"],\"mzI/c+\":[\"Herunterladen\"],\"n3fGRk\":[\"gesetzt von \",[\"0\"]],\"nE9jsU\":[\"Entspannt – Weniger aggressiver Schutz\"],\"nNflMD\":[\"Kanal verlassen\"],\"nPXkBi\":[\"WHOIS-Daten werden geladen...\"],\"nQnxxF\":[\"Nachricht an #\",[\"0\"],\" (Shift+Enter für neue Zeile)\"],\"nWMRxa\":[\"Loslösen\"],\"nkC032\":[\"Kein Flood-Profil\"],\"o69z4d\":[\"Warnmeldung an \",[\"username\"],\" senden\"],\"o9ylQi\":[\"GIFs suchen, um zu beginnen\"],\"oFGkER\":[\"Server-Hinweise\"],\"oOi11l\":[\"Nach unten scrollen\"],\"oPYIL5\":[\"Netzwerk\"],\"oQEzQR\":[\"Neue Direktnachricht\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Man-in-the-Middle-Angriffe auf Server-Verbindungen sind möglich\"],\"oeqmmJ\":[\"Vertrauenswürdige Quellen\"],\"optX0N\":[\"vor \",[\"0\"],\" Std.\"],\"ovBPCi\":[\"Standard\"],\"p0Z69r\":[\"Muster darf nicht leer sein\"],\"p1KgtK\":[\"Audio konnte nicht geladen werden\"],\"p59pEv\":[\"Weitere Details\"],\"p7sRI6\":[\"Anderen mitteilen, wenn Sie tippen\"],\"pBm1od\":[\"Geheimer Kanal\"],\"pNmiXx\":[\"Ihr Standard-Nickname für alle Server\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Kontopasswort\"],\"peNE68\":[\"Dauerhaft\"],\"plhHQt\":[\"Keine Daten\"],\"pm6+q5\":[\"Sicherheitswarnung\"],\"pn5qSs\":[\"Weitere Informationen\"],\"q0cR4S\":[\"ist jetzt bekannt als **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanal erscheint nicht in LIST- oder NAMES-Befehlen\"],\"qLpTm/\":[\"Reaktion \",[\"emoji\"],\" entfernen\"],\"qVkGWK\":[\"Anheften\"],\"qY8wNa\":[\"Homepage\"],\"qb0xJ7\":[\"Platzhalter: * beliebige Zeichen, ? ein einzelnes Zeichen. Beispiele: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Kanalschlüssel (+k)\"],\"qtoOYG\":[\"Kein Limit\"],\"r1W2AS\":[\"Dateiserver-Bild\"],\"rIPR2O\":[\"Thema gesetzt vor (Min.)\"],\"rMMSYo\":[\"Maximale Länge ist \",[\"0\"]],\"rWtzQe\":[\"Das Netzwerk hat sich geteilt und wieder verbunden. ✅\"],\"rYG2u6\":[\"Bitte warten...\"],\"rdUucN\":[\"Vorschau\"],\"rjGI/Q\":[\"Datenschutz\"],\"rk8iDX\":[\"GIFs werden geladen...\"],\"rn6SBY\":[\"Ton einschalten\"],\"s/UKqq\":[\"Wurde aus dem Kanal geworfen\"],\"s8cATI\":[\"ist \",[\"channelName\"],\" beigetreten\"],\"sCO9ue\":[\"Die Verbindung zu <0>\",[\"serverName\"],\"0> hat folgende Sicherheitsbedenken:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"ist jetzt bekannt als **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" hat dich eingeladen, \",[\"channel\"],\" beizutreten\"],\"sby+1/\":[\"Zum Kopieren klicken\"],\"sfN25C\":[\"Ihr echter oder vollständiger Name\"],\"sliuzR\":[\"Link öffnen\"],\"sqrO9R\":[\"Benutzerdefinierte Erwähnungen\"],\"sr6RdJ\":[\"Mehrzeilig mit Shift+Enter\"],\"swrCpB\":[\"Der Kanal wurde von \",[\"oldName\"],\" in \",[\"newName\"],\" umbenannt von \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Erweitert\"],\"t/YqKh\":[\"Entfernen\"],\"t47eHD\":[\"Ihr eindeutiger Bezeichner auf diesem Server\"],\"tAkAh0\":[\"URL mit optionaler \",[\"size\"],\"-Substitution. Beispiel: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Seitenleiste der Kanalliste ein- oder ausblenden\"],\"tfDRzk\":[\"Speichern\"],\"tiBsJk\":[\"hat \",[\"channelName\"],\" verlassen\"],\"tt4/UD\":[\"hat sich abgemeldet (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Nickname {nick} bereits vergeben, versuche es mit {newNick}\"],\"u0a8B4\":[\"Als IRC-Operator für Verwaltungszugriff authentifizieren\"],\"u0rWFU\":[\"Erstellt nach (Min.)\"],\"u72w3t\":[\"Zu ignorierende Benutzer und Muster\"],\"u7jc2L\":[\"hat sich abgemeldet\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Speichern fehlgeschlagen: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC-Server:\"],\"ukyW4o\":[\"Deine Einladungslinks\"],\"usSSr/\":[\"Zoomstufe\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Shift+Enter für neue Zeilen (Enter sendet)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Kein Thema\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Sprache\"],\"vaHYxN\":[\"Echter Name\"],\"vhjbKr\":[\"Abwesend\"],\"w4NYox\":[[\"title\"],\" Client\"],\"w8xQRx\":[\"Ungültiger Wert\"],\"wFjjxZ\":[\"wurde von \",[\"username\"],\" aus \",[\"channelName\"],\" gekickt (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Keine Bann-Ausnahmen gefunden\"],\"wPrGnM\":[\"Kanal-Administrator\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Anzeigen, wenn Benutzer Kanäle betreten oder verlassen\"],\"whqZ9r\":[\"Weitere Wörter oder Phrasen zum Hervorheben\"],\"wm7RV4\":[\"Benachrichtigungston\"],\"wz/Yoq\":[\"Deine Nachrichten könnten abgefangen werden, wenn sie zwischen Servern weitergeleitet werden\"],\"x3+y8b\":[\"So viele Personen haben sich über diesen Link registriert\"],\"xCJdfg\":[\"Leeren\"],\"xOTzt5\":[\"gerade eben\"],\"xUHRTR\":[\"Beim Verbinden automatisch als Operator authentifizieren\"],\"xWHwwQ\":[\"Sperren\"],\"xYilR2\":[\"Medien\"],\"xbi8D6\":[\"Dieser Server unterstützt keine Einladungslinks (die<0>obby.world/invitation0>-Capability wird nicht angekündigt). Du kannst trotzdem normal chatten; dieses Panel ist für Netzwerke mit obbyircd.\"],\"xceQrO\":[\"Nur sichere Websockets werden unterstützt\"],\"xdtXa+\":[\"Kanalname\"],\"xfXC7q\":[\"Textkanäle\"],\"xlCYOE\":[\"Weitere Nachrichten werden geladen...\"],\"xlhswE\":[\"Mindestwert ist \",[\"0\"]],\"xq97Ci\":[\"Wort oder Phrase hinzufügen...\"],\"xuRqRq\":[\"Client-Limit (+l)\"],\"xwF+7J\":[[\"0\"],\" tippt...\"],\"y1eoq1\":[\"Link kopieren\"],\"yNeucF\":[\"Dieser Server unterstützt keine erweiterten Profilmetadaten (IRCv3 METADATA). Felder wie Avatar, Anzeigename und Status sind nicht verfügbar.\"],\"yPlrca\":[\"Kanal-Avatar\"],\"yQE2r9\":[\"Laden\"],\"ySU+JY\":[\"deine@email.de\"],\"yTX1Rt\":[\"Oper-Benutzername\"],\"yYOzWD\":[\"Protokolle\"],\"yfx9Re\":[\"IRC-Operatorpasswort\"],\"ygCKqB\":[\"Stopp\"],\"ymDxJx\":[\"IRC-Operatorbenutzername\"],\"yrpRsQ\":[\"Nach Name sortieren\"],\"yz7wBu\":[\"Schließen\"],\"zJw+jA\":[\"setzt Modus: \",[\"0\"]],\"zbymaY\":[\"vor \",[\"0\"],\" Min.\"],\"zebeLu\":[\"oper-Benutzername eingeben\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Ungültiges Musterformat. Verwenden Sie nick!user@host (Platzhalter * erlaubt)\"],\"+6NQQA\":[\"Allgemeiner Support-Kanal\"],\"+6NyRG\":[\"Client\"],\"+K0AvT\":[\"Trennen\"],\"+cyFdH\":[\"Standardnachricht beim Als-abwesend-markieren\"],\"+mVPqU\":[\"Markdown-Formatierung in Nachrichten rendern\"],\"+vqCJH\":[\"Ihr Kontobenutzername zur Authentifizierung\"],\"+yPBXI\":[\"Datei auswählen\"],\"+zy2Nq\":[\"Typ\"],\"/09cao\":[\"Geringe Verbindungssicherheit (Stufe \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Externe Benutzer können keine Nachrichten senden\"],\"/4C8U0\":[\"Alles kopieren\"],\"/6BzZF\":[\"Mitgliederliste umschalten\"],\"/AkXyp\":[\"Bestätigen?\"],\"/TNOPk\":[\"Benutzer ist abwesend\"],\"/XQgft\":[\"Entdecken\"],\"/cF7Rs\":[\"Lautstärke\"],\"/dqduX\":[\"Nächste Seite\"],\"/fc3q4\":[\"Alle Inhalte\"],\"/kISDh\":[\"Benachrichtigungstöne aktivieren\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Töne bei Erwähnungen und Nachrichten abspielen\"],\"0/0ZGA\":[\"Kanalname-Maske\"],\"0D6j7U\":[\"Mehr über benutzerdefinierte Regeln erfahren →\"],\"0XsHcR\":[\"Benutzer rauswerfen\"],\"0ZpE//\":[\"Nach Benutzern sortieren\"],\"0bEPwz\":[\"Als abwesend setzen\"],\"0dGkPt\":[\"Kanalliste ausklappen\"],\"0gS7M5\":[\"Anzeigename\"],\"0kS+M8\":[\"BeispielNET\"],\"0rgoY7\":[\"Nur mit ausgewählten Servern verbinden\"],\"0wdd7X\":[\"Beitreten\"],\"0wkVYx\":[\"Privatnachrichten\"],\"111uHX\":[\"Link-Vorschau\"],\"196EG4\":[\"Privatnachricht löschen\"],\"1DSr1i\":[\"Konto registrieren\"],\"1O/24y\":[\"Kanalliste umschalten\"],\"1TNIig\":[\"Öffnen\"],\"1VPJJ2\":[\"Warnung: Externer Link\"],\"1ZC/dv\":[\"Keine ungelesenen Erwähnungen oder Nachrichten\"],\"1pO1zi\":[\"Servername ist erforderlich\"],\"1uwfzQ\":[\"Kanalthema anzeigen\"],\"268g7c\":[\"Anzeigenamen eingeben\"],\"2CEOW6\":[\"Netzwerk gebunden über soju Bouncer\"],\"2F9+AZ\":[\"Noch kein roher IRC-Verkehr erfasst. Versuche dich zu verbinden oder eine Nachricht zu senden.\"],\"2FOFq1\":[\"Server-Operatoren im Netzwerk könnten deine Nachrichten lesen\"],\"2FYpfJ\":[\"Mehr\"],\"2HF1Y2\":[[\"inviter\"],\" hat \",[\"target\"],\" eingeladen, \",[\"channel\"],\" beizutreten\"],\"2I70QL\":[\"Benutzerprofilinformationen anzeigen\"],\"2QYdmE\":[\"Benutzer:\"],\"2QpEjG\":[\"hat verlassen\"],\"2YE223\":[\"Nachricht an #\",[\"0\"],\" (Enter für neue Zeile, Shift+Enter zum Senden)\"],\"2bimFY\":[\"Server-Passwort verwenden\"],\"2iTmdZ\":[\"Lokaler Speicher:\"],\"2odkwe\":[\"Streng – Aggressiverer Schutz\"],\"2uDhbA\":[\"Benutzername zum Einladen eingeben\"],\"2ygf/L\":[\"← Zurück\"],\"2zEgxj\":[\"GIFs suchen...\"],\"3RdPhl\":[\"Kanal umbenennen\"],\"3THokf\":[\"Benutzer mit Sprachrecht\"],\"3TSz9S\":[\"Minimieren\"],\"3jBDvM\":[\"Kanal-Anzeigename\"],\"3ryuFU\":[\"Optionale Absturzberichte zur App-Verbesserung\"],\"3uBF/8\":[\"Ansicht schließen\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Kontoname eingeben...\"],\"4/Rr0R\":[\"Benutzer in den aktuellen Kanal einladen\"],\"4EZrJN\":[\"Regeln\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Flood-Profil (+F)\"],\"4RZQRK\":[\"Was machst du gerade?\"],\"4hfTrB\":[\"Nickname\"],\"4n99LO\":[\"Bereits in \",[\"0\"]],\"4t6vMV\":[\"Kurze Nachrichten automatisch einzeilig darstellen\"],\"4vsHmf\":[\"Zeit (Min)\"],\"4x/Axu\":[\"Dein Bouncer hat noch keine Netzwerke. Füge eines hinzu, um zu starten.\"],\"5+INAX\":[\"Nachrichten hervorheben, die Sie erwähnen\"],\"5R5Pv/\":[\"Oper Name\"],\"678PKt\":[\"Netzwerkname\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Passwort zum Beitreten erforderlich. Leer lassen, um den Schlüssel zu entfernen.\"],\"6HhMs3\":[\"Abgangsnachricht\"],\"6V3Ea3\":[\"Kopiert\"],\"6lGV3K\":[\"Weniger anzeigen\"],\"6yFOEi\":[\"Oper-Passwort eingeben...\"],\"7+IHTZ\":[\"Keine Datei ausgewählt\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (z.B. spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Privatnachricht senden\"],\"7U1W7c\":[\"Sehr locker\"],\"7Y1YQj\":[\"Echter Name:\"],\"7YHArF\":[\"— im Viewer öffnen\"],\"7fjnVl\":[\"Benutzer suchen...\"],\"7jL88x\":[\"Diese Nachricht löschen? Dies kann nicht rückgängig gemacht werden.\"],\"7nGhhM\":[\"Was denkst du gerade?\"],\"7sEpu1\":[\"Mitglieder — \",[\"0\"]],\"7sNhEz\":[\"Benutzername\"],\"8H0Q+x\":[\"Mehr über Profile erfahren →\"],\"8Phu0A\":[\"Anzeigen, wenn Benutzer ihren Nickname ändern\"],\"8XTG9e\":[\"oper-Passwort eingeben\"],\"8XsV2J\":[\"Erneut senden\"],\"8ZsakT\":[\"Passwort\"],\"8kR84m\":[\"Du bist dabei, einen externen Link zu öffnen:\"],\"8lCgih\":[\"Regel entfernen\"],\"8o3dPc\":[\"Dateien hier ablegen zum Hochladen\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"beigetreten\"],\"other\":[[\"joinCount\"],\"-mal beigetreten\"]}]],\"9BMLnJ\":[\"Erneut mit Server verbinden\"],\"9OEgyT\":[\"Reaktion hinzufügen\"],\"9PQ8m2\":[\"G-Line (globaler Ban)\"],\"9Qs99X\":[\"E-Mail:\"],\"9QupBP\":[\"Muster entfernen\"],\"9W7tl5\":[\"(unverändert)\"],\"9bG48P\":[\"Wird gesendet\"],\"9f5f0u\":[\"Fragen zum Datenschutz? Kontaktieren Sie uns:\"],\"9iweoP\":[\"Netzwerke auf \",[\"0\"]],\"9unqs3\":[\"Abwesend:\"],\"9v3hwv\":[\"Keine Server gefunden.\"],\"9zb2WA\":[\"Verbinden...\"],\"A1taO8\":[\"Suchen\"],\"A2adVi\":[\"Tipp-Benachrichtigungen senden\"],\"A9Rhec\":[\"Kanalname\"],\"AWOSPo\":[\"Vergrößern\"],\"AXSpEQ\":[\"Oper beim Verbinden\"],\"AdKRCX\":[\"Du bist mit <0>\",[\"0\"],\"0> verbunden.\"],\"AeXO77\":[\"Konto\"],\"AhNP40\":[\"Vor-/Zurückspulen\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Nickname geändert\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Antwort abbrechen\"],\"ApSx0O\":[[\"0\"],\" Nachrichten gefunden, die zu \\\"\",[\"searchQuery\"],\"\\\" passen\"],\"AxPAXW\":[\"Keine Ergebnisse gefunden\"],\"AyNqAB\":[\"Alle Serverereignisse im Chat anzeigen\"],\"B/QqGw\":[\"Nicht am Rechner\"],\"B0sB2k\":[\"Klartext\"],\"B8AaMI\":[\"Dieses Feld ist erforderlich\"],\"BA2c49\":[\"Server unterstützt keine erweiterte LIST-Filterung\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" und \",[\"3\"],\" weitere tippen...\"],\"BGul2A\":[\"Du hast ungespeicherte Änderungen. Möchtest du wirklich schließen, ohne zu speichern?\"],\"BIf9fi\":[\"Ihre Statusnachricht\"],\"BOJWfb\":[\"Vom soju-Bouncer trennen?\"],\"BPm98R\":[\"Kein Server ausgewählt. Wähle zuerst einen Server in der Seitenleiste; Einladungslinks werden pro Server verwaltet.\"],\"BZz3md\":[\"Ihre persönliche Website\"],\"Bgm/H7\":[\"Mehrzeilige Texteingabe erlauben\"],\"BiQIl1\":[\"Dieses Privatgespräch anheften\"],\"BlNZZ2\":[\"Klicken, um zur Nachricht zu springen\"],\"Bowq3c\":[\"Nur Operatoren können das Kanalthema ändern\"],\"Btozzp\":[\"Dieses Bild ist abgelaufen\"],\"Bycfjm\":[\"Gesamt: \",[\"0\"]],\"C6IBQc\":[\"Gesamtes JSON kopieren\"],\"C9L9wL\":[\"Datenerfassung\"],\"CDq4wC\":[\"Benutzer moderieren\"],\"CHVRxG\":[\"Nachricht an @\",[\"0\"],\" (Shift+Enter für neue Zeile)\"],\"CN9zdR\":[\"Oper-Name und Passwort sind erforderlich\"],\"CW3sYa\":[\"Reaktion \",[\"emoji\"],\" hinzufügen\"],\"CaAkqd\":[\"Verbindungstrennungen anzeigen\"],\"CbvaYj\":[\"Nach Nickname sperren\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Kanal auswählen\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"System\"],\"D28t6+\":[\"ist beigetreten und gegangen\"],\"DB8zMK\":[\"Anwenden\"],\"DBcWHr\":[\"Benutzerdefinierte Benachrichtigungstondatei\"],\"DTy9Xw\":[\"Medienvorschauen\"],\"Dj4pSr\":[\"Sicheres Passwort wählen\"],\"Du+zn+\":[\"Suche...\"],\"Du2T2f\":[\"Einstellung nicht gefunden\"],\"DwsSVQ\":[\"Filter anwenden & Aktualisieren\"],\"E3W/zd\":[\"Standard-Nickname\"],\"E6nRW7\":[\"URL kopieren\"],\"E703RG\":[\"Modi:\"],\"EAeu1Z\":[\"Einladung senden\"],\"EFKJQT\":[\"Einstellung\"],\"EGPQBv\":[\"Benutzerdefinierte Flood-Regeln (+f)\"],\"ELik0r\":[\"Vollständige Datenschutzrichtlinie anzeigen\"],\"EPbeC2\":[\"Kanalthema anzeigen oder bearbeiten\"],\"EQCDNT\":[\"Oper-Benutzernamen eingeben...\"],\"EUvulZ\":[\"1 Nachricht gefunden, die zu \\\"\",[\"searchQuery\"],\"\\\" passt\"],\"EatZYJ\":[\"Nächstes Bild\"],\"EdQY6l\":[\"Keine\"],\"EnqLYU\":[\"Server suchen...\"],\"F0OKMc\":[\"Server bearbeiten\"],\"F6Int2\":[\"Hervorhebungen aktivieren\"],\"FDoLyE\":[\"Max. Benutzer\"],\"FUU/hZ\":[\"Steuert, wie viele externe Medien im Chat geladen werden.\"],\"Fdp03t\":[\"an\"],\"FfPWR0\":[\"Modal\"],\"FjkaiT\":[\"Verkleinern\"],\"FlqOE9\":[\"Was das bedeutet:\"],\"FolHNl\":[\"Konto und Authentifizierung verwalten\"],\"Fp2Dif\":[\"Den Server verlassen\"],\"G5KmCc\":[\"GZ-Line (globale Z-Line)\"],\"GDs0lz\":[\"<0>Risiko:0> Sensible Informationen (Nachrichten, private Gespräche, Authentifizierungsdaten) könnten Netzwerkadministratoren oder Angreifern zwischen IRC-Servern zugänglich sein.\"],\"GR+2I3\":[\"Einladungs-Maske hinzufügen (z.B. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Server-Hinweise schließen\"],\"GdhD7H\":[\"Erneut klicken zum Bestätigen\"],\"GjRZex\":[\"Netzwerk trennen?\"],\"GlHnXw\":[\"Nicknamewechsel fehlgeschlagen: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Vorschau:\"],\"GtmO8/\":[\"von\"],\"GtuHUQ\":[\"Diesen Kanal auf dem Server umbenennen. Alle Benutzer sehen den neuen Namen.\"],\"GuGfFX\":[\"Suche umschalten\"],\"GxkJXS\":[\"Wird hochgeladen...\"],\"GzbwnK\":[\"Dem Kanal beigetreten\"],\"GzsUDB\":[\"Erweitertes Profil\"],\"H/PnT8\":[\"Emoji einfügen\"],\"H6Izzl\":[\"Ihr bevorzugter Farbcode\"],\"H9jIv+\":[\"Beitritte/Abgänge anzeigen\"],\"HAKBY9\":[\"Dateien hochladen\"],\"HdE1If\":[\"Kanal\"],\"Hk4AW9\":[\"Ihr bevorzugter Anzeigename\"],\"HmHDk7\":[\"Mitglied auswählen\"],\"HrQzPU\":[\"Kanäle auf \",[\"networkName\"]],\"I2tXQ5\":[\"Nachricht an @\",[\"0\"],\" (Enter für neue Zeile, Shift+Enter zum Senden)\"],\"I6bw/h\":[\"Benutzer sperren\"],\"I92Z+b\":[\"Benachrichtigungen aktivieren\"],\"I9D72S\":[\"Bist du sicher, dass du diese Nachricht löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.\"],\"IA+1wo\":[\"Anzeigen, wenn Benutzer aus Kanälen gekickt werden\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Änderungen speichern\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" und \",[\"2\"],\" tippen...\"],\"IgrLD/\":[\"Pause\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Antworten\"],\"IoHMnl\":[\"Maximalwert ist \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Verbinde...\"],\"J5T9NW\":[\"Benutzerinformationen\"],\"J8Y5+z\":[\"Ups! Netz-Split! ⚠️\"],\"JBHkBA\":[\"Den Kanal verlassen\"],\"JCwL0Q\":[\"Grund eingeben (optional)\"],\"JFciKP\":[\"Umschalten\"],\"JXGkhG\":[\"Kanalnamen ändern (nur Operatoren)\"],\"JcD7qf\":[\"Weitere Aktionen\"],\"JdkA+c\":[\"Geheim (+s)\"],\"Jmu12l\":[\"Serverkanäle\"],\"JoQY+E\":[\"Damit wird das Netzwerk aus deinem soju-Bouncer entfernt. Um es wieder zu nutzen, musst du es erneut hinzufügen.\"],\"JvQ++s\":[\"Markdown aktivieren\"],\"K2jwh/\":[\"Keine WHOIS-Daten verfügbar\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Nachricht löschen\"],\"KKBlUU\":[\"Einbetten\"],\"KM0pLb\":[\"Willkommen im Kanal!\"],\"KR6W2h\":[\"Benutzer nicht mehr ignorieren\"],\"KV+Bi1\":[\"Nur auf Einladung (+i)\"],\"KdCtwE\":[\"Wie viele Sekunden Flood-Aktivität überwacht wird, bevor die Zähler zurückgesetzt werden\"],\"Kkezga\":[\"Server-Passwort\"],\"KsiQ/8\":[\"Benutzer müssen eingeladen werden\"],\"L+gB/D\":[\"Kanalinformationen\"],\"LC1a7n\":[\"Der IRC-Server hat gemeldet, dass seine Server-zu-Server-Verbindungen ein niedriges Sicherheitsniveau aufweisen. Das bedeutet, dass deine Nachrichten beim Weiterleiten zwischen IRC-Servern im Netzwerk möglicherweise nicht ordnungsgemäß verschlüsselt sind oder die SSL/TLS-Zertifikate nicht korrekt validiert werden.\"],\"LEwpeL\":[\"soju Bouncer (Steuerung)\"],\"LNfLR5\":[\"Kicks anzeigen\"],\"LP+1Z7\":[\"Netzwerk hinzufügen\"],\"LQb0W/\":[\"Alle Ereignisse anzeigen\"],\"LU7/yA\":[\"Alternativer Anzeigename. Kann Leerzeichen, Emojis und Sonderzeichen enthalten. Der echte Kanalname (\",[\"channelName\"],\") wird weiterhin für IRC-Befehle verwendet.\"],\"LUb9O7\":[\"Ein gültiger Server-Port ist erforderlich\"],\"LV4fT6\":[\"Beschreibung (optional, z.B. \\\"Beta-Tester Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Datenschutzrichtlinie\"],\"LcuSDR\":[\"Profilinformationen und Metadaten verwalten\"],\"LqLS9B\":[\"Nickwechsel anzeigen\"],\"LsDQt2\":[\"Kanaleinstellungen\"],\"LtI9AS\":[\"Eigentümer\"],\"LuNhhL\":[\"hat auf diese Nachricht reagiert\"],\"M/AZNG\":[\"URL zu Ihrem Avatar-Bild\"],\"M/WIer\":[\"Nachricht senden\"],\"M8er/5\":[\"Name:\"],\"MHk+7g\":[\"Vorheriges Bild\"],\"MRorGe\":[\"Benutzer anschreiben\"],\"MVbSGP\":[\"Zeitfenster (Sekunden)\"],\"MkpcsT\":[\"Ihre Nachrichten und Einstellungen werden lokal gespeichert\"],\"MzPdC2\":[\"Serverpasswort (PASS)\"],\"N/hDSy\":[\"Als Bot markieren – normalerweise 'on' oder leer\"],\"N6j2JH\":[[\"0\"],\" bearbeiten\"],\"N7TQbE\":[\"Benutzer zu \",[\"channelName\"],\" einladen\"],\"NCca/o\":[\"Standard-Spitznamen eingeben...\"],\"Nqs6B9\":[\"Zeigt alle externen Medien. Jede URL kann eine Anfrage an einen unbekannten Server auslösen.\"],\"Nt+9O7\":[\"WebSocket statt rohem TCP verwenden\"],\"NxIHzc\":[\"Benutzer trennen\"],\"O+v/cL\":[\"Alle Kanäle auf dem Server durchsuchen\"],\"OCGpR4\":[\"(übernehmen)\"],\"ODwSCk\":[\"GIF senden\"],\"OGQ5kK\":[\"Benachrichtigungstöne und Hervorhebungen konfigurieren\"],\"OIPt1Z\":[\"Seitenleiste der Mitgliederliste ein- oder ausblenden\"],\"OKSNq/\":[\"Sehr streng\"],\"ONWvwQ\":[\"Hochladen\"],\"OVKoQO\":[\"Ihr Kontopasswort zur Authentifizierung\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - IRC in die Zukunft bringen\"],\"OhCpra\":[\"Thema setzen…\"],\"OkltoQ\":[[\"username\"],\" per Nickname sperren (verhindert erneutes Beitreten mit demselben Nick)\"],\"P+t/Te\":[\"Keine weiteren Daten\"],\"P42Wcc\":[\"Sicher\"],\"PD38l0\":[\"Kanal-Avatar-Vorschau\"],\"PD9mEt\":[\"Nachricht eingeben...\"],\"PPqfdA\":[\"Kanaleinstellungen öffnen\"],\"PSCjfZ\":[\"Das Thema für diesen Kanal. Alle Benutzer können es sehen.\"],\"PZCecv\":[\"PDF-Vorschau\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 Mal\"],\"other\":[[\"c\"],\" Mal\"]}]],\"PguS2C\":[\"Ausnahme-Maske hinzufügen (z.B. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[[\"displayedChannelsCount\"],\" von \",[\"0\"],\" Kanälen angezeigt\"],\"PqhVlJ\":[\"Benutzer sperren (per Hostmask)\"],\"Q+chwU\":[\"Benutzername:\"],\"Q2QY4/\":[\"Diese Einladung löschen\"],\"Q3v9Wc\":[\"Ja, löschen\"],\"Q6hhn8\":[\"Einstellungen\"],\"QF4a34\":[\"Bitte gib einen Benutzernamen ein\"],\"QGqSZ2\":[\"Farbe & Formatierung\"],\"QJQd1J\":[\"Profil bearbeiten\"],\"QSzGDE\":[\"Inaktiv\"],\"QUlny5\":[\"Willkommen bei \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Mehr lesen\"],\"QuSkCF\":[\"Kanäle filtern...\"],\"QwUrDZ\":[\"hat das Thema geändert zu: \",[\"topic\"]],\"R0UH07\":[\"Bild \",[\"0\"],\" von \",[\"1\"]],\"R7SsBE\":[\"Stumm schalten\"],\"R8rf1X\":[\"Klicken, um das Thema zu setzen\"],\"RArB3D\":[\"wurde von \",[\"username\"],\" aus \",[\"channelName\"],\" gekickt\"],\"RI3cWd\":[\"Entdecke die Welt von IRC mit ObsidianIRC\"],\"RIfHS5\":[\"Neuen Einladungslink erstellen\"],\"RMMaN5\":[\"Moderiert (+m)\"],\"RWw9Lg\":[\"Fenster schließen\"],\"RZ2BuZ\":[\"Kontoregistrierung für \",[\"account\"],\" erfordert Verifizierung: \",[\"message\"]],\"RySp6q\":[\"Kommentare ausblenden\"],\"S5Togi\":[\"Netzwerke werden von deinem Bouncer geladen…\"],\"SPKQTd\":[\"Nickname ist erforderlich\"],\"SPVjfj\":[\"Standardmäßig 'kein Grund', wenn leer gelassen\"],\"SQKPvQ\":[\"Benutzer einladen\"],\"STmlpb\":[\"Zurück zur Netzwerkliste\"],\"SkZcl+\":[\"Wähle ein vordefiniertes Flood-Schutzprofil. Diese Profile bieten ausgewogene Schutzeinstellungen für verschiedene Anwendungsfälle.\"],\"Slr+3C\":[\"Min. Benutzer\"],\"Spnlre\":[\"Du hast \",[\"target\"],\" eingeladen, \",[\"channel\"],\" beizutreten\"],\"T/ckN5\":[\"Im Viewer öffnen\"],\"T91vKp\":[\"Abspielen\"],\"TV2Wdu\":[\"Erfahren Sie, wie wir Ihre Daten verwalten und Ihre Privatsphäre schützen.\"],\"TgFpwD\":[\"Wird angewendet...\"],\"TkzSFB\":[\"Keine Änderungen\"],\"TtserG\":[\"Echten Namen eingeben\"],\"Ttz9J1\":[\"Passwort eingeben...\"],\"Tz0i8g\":[\"Einstellungen\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reagieren\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Du hast noch keine Einladungslinks erstellt. Verwende das Formular oben, um deinen ersten zu erstellen.\"],\"UGT5vp\":[\"Einstellungen speichern\"],\"UV5hLB\":[\"Keine Sperren gefunden\"],\"Uaj3Nd\":[\"Statusnachrichten\"],\"Ue3uny\":[\"Standard (kein Profil)\"],\"UkARhe\":[\"Normal – Standardschutz\"],\"Umn7Cj\":[\"Noch keine Kommentare. Sei der Erste!\"],\"UtUIRh\":[[\"0\"],\" ältere Nachrichten\"],\"UwzP+U\":[\"Sichere Verbindung\"],\"V0/A4O\":[\"Kanalbesitzer\"],\"V0zZWc\":[\"Damit wird auch das unten gebundene Netzwerk geschlossen.\"],\"V4qgxE\":[\"Erstellt vor (Min.)\"],\"V8yTm6\":[\"Suche löschen\"],\"VJMMyz\":[\"ObsidianIRC - IRC in die Zukunft bringen\"],\"VJScHU\":[\"Grund\"],\"VLsmVV\":[\"Benachrichtigungen stummschalten\"],\"VbyRUy\":[\"Kommentare\"],\"Vmx0mQ\":[\"Gesetzt von:\"],\"VqnIZz\":[\"Datenschutzrichtlinie und Datenpraktiken anzeigen\"],\"VrMygG\":[\"Mindestlänge ist \",[\"0\"]],\"VrnTui\":[\"Ihre Pronomen, im Profil angezeigt\"],\"W8E3qn\":[\"Authentifiziertes Konto\"],\"WAakm9\":[\"Kanal löschen\"],\"WFxTHC\":[\"Bann-Maske hinzufügen (z.B. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Server-Host ist erforderlich\"],\"WRYdXW\":[\"Audioposition\"],\"WUOH5B\":[\"Benutzer ignorieren\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"1 weiteres Element anzeigen\"],\"other\":[[\"1\"],\" weitere Elemente anzeigen\"]}]],\"WYxRzo\":[\"Einladungslinks erstellen und verwalten\"],\"Wd38W1\":[\"Lass das Kanalfeld leer für eine allgemeine Netzwerkeinladung. Die Beschreibung ist nur für deine Aufzeichnungen â sie ist nur für dich in dieser Liste sichtbar.\"],\"Weq9zb\":[\"Allgemein\"],\"Wfj7Sk\":[\"Benachrichtigungstöne stummschalten oder aktivieren\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Benutzerprofil\"],\"X6S3lt\":[\"Einstellungen, Kanäle, Server suchen...\"],\"XEHan5\":[\"Trotzdem fortfahren\"],\"XI1+wb\":[\"Ungültiges Format\"],\"XIXeuC\":[\"Nachricht an @\",[\"0\"]],\"XMS+k4\":[\"Privatnachricht starten\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Privatnachricht loslösen\"],\"Xm/s+u\":[\"Anzeige\"],\"Xp2n93\":[\"Zeigt Medien vom vertrauenswürdigen Datei-Host deines Servers. Es werden keine Anfragen an externe Dienste gestellt.\"],\"XvjC4F\":[\"Wird gespeichert...\"],\"Y/qryO\":[\"Keine Benutzer gefunden, die deiner Suche entsprechen\"],\"YAqRpI\":[\"Kontoregistrierung für \",[\"account\"],\" erfolgreich: \",[\"message\"]],\"YEfzvP\":[\"Geschütztes Thema (+t)\"],\"YQOn6a\":[\"Mitgliederliste einklappen\"],\"YRCoE9\":[\"Kanal-Operator\"],\"YURQaF\":[\"Profil anzeigen\"],\"YdBSvr\":[\"Medienanzeige und externe Inhalte steuern\"],\"Yj6U3V\":[\"Kein zentraler Server:\"],\"YjvpGx\":[\"Pronomen\"],\"YqH4l4\":[\"Kein Schlüssel\"],\"YyUPpV\":[\"Konto:\"],\"ZJSWfw\":[\"Nachricht beim Trennen vom Server\"],\"ZR1dJ4\":[\"Einladungen\"],\"ZdWg0V\":[\"Im Browser öffnen\"],\"ZhRBbl\":[\"Nachrichten suchen…\"],\"Zmcu3y\":[\"Erweiterte Filter\"],\"a0bHay\":[\"<0>\",[\"0\"],\"0> trennen?\"],\"a2/8e5\":[\"Thema gesetzt nach (Min.)\"],\"aHKcKc\":[\"Vorherige Seite\"],\"aJTbXX\":[\"Oper Password\"],\"aQryQv\":[\"Muster existiert bereits\"],\"aW9pLN\":[\"Maximale Anzahl der zugelassenen Benutzer. Leer lassen für kein Limit.\"],\"ah4fmZ\":[\"Zeigt auch Vorschauen von YouTube, Vimeo, SoundCloud und ähnlichen bekannten Diensten.\"],\"aifXak\":[\"Keine Medien in diesem Kanal\"],\"ap2zBz\":[\"Locker\"],\"az8lvo\":[\"Aus\"],\"azXSNo\":[\"Mitgliederliste ausklappen\"],\"azdliB\":[\"Bei einem Konto anmelden\"],\"b26wlF\":[\"sie/ihr\"],\"bD/+Ei\":[\"Streng\"],\"bQ6BJn\":[\"Detaillierte Flood-Schutzregeln konfigurieren. Jede Regel legt fest, welche Aktivitäten überwacht werden sollen und welche Maßnahmen bei Überschreitung der Schwellenwerte ergriffen werden.\"],\"beV7+y\":[\"Der Benutzer erhält eine Einladung, \",[\"channelName\"],\" beizutreten.\"],\"bk84cH\":[\"Abwesenheitsnachricht\"],\"bkHdLj\":[\"IRC-Server hinzufügen\"],\"bmQLn5\":[\"Regel hinzufügen\"],\"bv4cFj\":[\"Transport\"],\"bwRvnp\":[\"Aktion\"],\"c8+EVZ\":[\"Verifiziertes Konto\"],\"cGYUlD\":[\"Es werden keine Medienvorschauen geladen.\"],\"cLF98o\":[\"Kommentare anzeigen (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Keine Benutzer verfügbar\"],\"cSgpoS\":[\"Privatnachricht anheften\"],\"cXeEKu\":[\"Damit werden auch die \",[\"0\"],\" unten gebundenen Netzwerke geschlossen.\"],\"cde3ce\":[\"Nachricht an <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Formatierte Ausgabe kopieren\"],\"cl/A5J\":[\"Willkommen bei \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Löschen\"],\"coPLXT\":[\"Wir speichern Ihre IRC-Kommunikation nicht auf unseren Servern\"],\"crYH/6\":[\"SoundCloud-Player\"],\"cv5DQb\":[\"kein Host festgelegt\"],\"d3sis4\":[\"Server hinzufügen\"],\"d9aN5k\":[[\"username\"],\" aus dem Kanal entfernen\"],\"dEgA5A\":[\"Abbrechen\"],\"dGi1We\":[\"Dieses Privatgespräch loslösen\"],\"dJVuyC\":[\"hat \",[\"channelName\"],\" verlassen (\",[\"reason\"],\")\"],\"dMtLDE\":[\"an\"],\"dXqxlh\":[\"<0>⚠️ Sicherheitsrisiko!0> Diese Verbindung könnte anfällig für Abhören oder Man-in-the-Middle-Angriffe sein.\"],\"da9Q/R\":[\"Kanalmodi geändert\"],\"dhJN3N\":[\"Kommentare anzeigen\"],\"dj2xTE\":[\"Benachrichtigung schließen\"],\"dpCzmC\":[\"Flood-Schutz-Einstellungen\"],\"e9dQpT\":[\"Möchtest du diesen Link in einem neuen Tab öffnen?\"],\"ePK91l\":[\"Bearbeiten\"],\"eYBDuB\":[\"Bild hochladen oder URL mit optionaler \",[\"size\"],\"-Substitution angeben\"],\"edBbee\":[[\"username\"],\" per hostmask sperren (verhindert erneutes Beitreten von derselben IP/Host)\"],\"ekfzWq\":[\"Benutzereinstellungen\"],\"elPDWs\":[\"IRC-Client-Erfahrung anpassen\"],\"eu2osY\":[\"<0>💡 Empfehlung:0> Fahre nur fort, wenn du diesem Server vertraust und die Risiken kennst. Teile keine sensiblen Informationen oder Passwörter über diese Verbindung.\"],\"euEhbr\":[\"Klicke, um \",[\"channel\"],\" beizutreten\"],\"ez3vLd\":[\"Mehrzeilige Eingabe aktivieren\"],\"f0J5Ki\":[\"Die Server-zu-Server-Kommunikation verwendet möglicherweise unverschlüsselte Verbindungen\"],\"f9BHJk\":[\"Benutzer warnen\"],\"fDOLLd\":[\"Keine Kanäle gefunden.\"],\"ffzDkB\":[\"Anonyme Analysen:\"],\"fq1GF9\":[\"Anzeigen, wenn Benutzer die Verbindung trennen\"],\"gCldcN\":[\"Akzentfarbe ändern\"],\"gEF57C\":[\"Dieser Server unterstützt nur einen Verbindungstyp\"],\"gJuLUI\":[\"Ignorierliste\"],\"gNzMrk\":[\"Aktueller Avatar\"],\"gjPWyO\":[\"Spitznamen eingeben...\"],\"gz6UQ3\":[\"Maximieren\"],\"h6/IMX\":[\"Erstes Netzwerk hinzufügen\"],\"h6razj\":[\"Kanalname-Maske ausschließen\"],\"hG6jnw\":[\"Kein Thema gesetzt\"],\"hG89Ed\":[\"Bild\"],\"hYgDIe\":[\"Erstellen\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"z.B. 100:1440\"],\"he3ygx\":[\"Kopieren\"],\"hehnjM\":[\"Anzahl\"],\"hzdLuQ\":[\"Nur Benutzer mit Voice oder höher können sprechen\"],\"i0qMbr\":[\"Startseite\"],\"iDNBZe\":[\"Benachrichtigungen\"],\"iH8pgl\":[\"Zurück\"],\"iL9SZg\":[\"Benutzer sperren (per Nickname)\"],\"iNt+3c\":[\"Zurück zum Bild\"],\"iQvi+a\":[\"Nicht mehr vor geringer Verbindungssicherheit für diesen Server warnen\"],\"iSLIjg\":[\"Verbinden\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Server-Host\"],\"idD8Ev\":[\"Gespeichert\"],\"iivqkW\":[\"Angemeldet seit\"],\"ij+Elv\":[\"Bildvorschau\"],\"ilIWp7\":[\"Benachrichtigungen umschalten\"],\"iuaqvB\":[\"* als Platzhalter verwenden. Beispiele: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Nach Hostmaske sperren\"],\"jA4uoI\":[\"Thema:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Grund (optional)\"],\"jUV7CU\":[\"Avatar hochladen\"],\"jW5Uwh\":[\"Steuert, wie viele externe Medien geladen werden. Aus / Sicher / Vertrauenswürdige / Alle Inhalte.\"],\"jXzms5\":[\"Anhangsoptionen\"],\"jZlrte\":[\"Farbe\"],\"jfC/xh\":[\"Kontakt\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Ältere Nachrichten laden\"],\"k3ID0F\":[\"Mitglieder filtern…\"],\"k65gsE\":[\"Vertieft ansehen\"],\"k7Zgob\":[\"Verbindung abbrechen\"],\"kAVx5h\":[\"Keine Einladungen gefunden\"],\"kCLEPU\":[\"Verbunden mit\"],\"kF5LKb\":[\"Ignorierte Muster:\"],\"kGeOx/\":[[\"0\"],\" beitreten\"],\"kITKr8\":[\"Kanal-Modi werden geladen...\"],\"kPpPsw\":[\"Du bist ein IRC Operator\"],\"kWJmRL\":[\"Du\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"JSON kopieren\"],\"krViRy\":[\"Klicken zum Kopieren als JSON\"],\"ks71ra\":[\"Ausnahmen\"],\"kw4lRv\":[\"Kanal-Halboperator\"],\"kxgIRq\":[\"Kanal auswählen oder hinzufügen, um zu beginnen.\"],\"ky6dWe\":[\"Avatar-Vorschau\"],\"l+GxCv\":[\"Kanäle werden geladen...\"],\"l+IUVW\":[\"Kontoverifizierung für \",[\"account\"],\" erfolgreich: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"erneut verbunden\"],\"other\":[[\"reconnectCount\"],\"-mal erneut verbunden\"]}]],\"l1l8sj\":[\"vor \",[\"0\"],\" T.\"],\"l5NhnV\":[\"#kanal (optional)\"],\"l5jmzx\":[[\"0\"],\" und \",[\"1\"],\" tippen...\"],\"lCF0wC\":[\"Aktualisieren\"],\"lHy8N5\":[\"Weitere Kanäle werden geladen...\"],\"lasgrr\":[\"verwendet\"],\"lbpf14\":[[\"value\"],\" beitreten\"],\"lfFsZ4\":[\"Kanäle\"],\"lkNdiH\":[\"Kontoname\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Bild hochladen\"],\"loQxaJ\":[\"Ich bin zurück\"],\"lvfaxv\":[\"STARTSEITE\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Hinzufügen\"],\"m8flAk\":[\"Vorschau (noch nicht hochgeladen)\"],\"mEPxTp\":[\"<0>⚠️ Vorsicht!0> Öffne nur Links aus vertrauenswürdigen Quellen. Bösartige Links können deine Sicherheit oder Privatsphäre gefährden.\"],\"mHGdhG\":[\"Serverinformationen\"],\"mHS8lb\":[\"Nachricht an #\",[\"0\"]],\"mMYBD9\":[\"Weit – Breiterer Schutzbereich\"],\"mTGsPd\":[\"Kanalthema\"],\"mU8j6O\":[\"Keine externen Nachrichten (+n)\"],\"mZp8FL\":[\"Automatisch auf einzeilig wechseln\"],\"mdQu8G\":[\"DeinNickname\"],\"miSSBQ\":[\"Kommentare (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Benutzer ist authentifiziert\"],\"mwtcGl\":[\"Kommentare schließen\"],\"myL0MR\":[\"Dieses Netzwerk löschen?\"],\"mzI/c+\":[\"Herunterladen\"],\"n3fGRk\":[\"gesetzt von \",[\"0\"]],\"n5+j9l\":[\"z. B. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Entspannt – Weniger aggressiver Schutz\"],\"nNflMD\":[\"Kanal verlassen\"],\"nPXkBi\":[\"WHOIS-Daten werden geladen...\"],\"nQnxxF\":[\"Nachricht an #\",[\"0\"],\" (Shift+Enter für neue Zeile)\"],\"nWMRxa\":[\"Loslösen\"],\"nkC032\":[\"Kein Flood-Profil\"],\"o69z4d\":[\"Warnmeldung an \",[\"username\"],\" senden\"],\"o9ylQi\":[\"GIFs suchen, um zu beginnen\"],\"oFGkER\":[\"Server-Hinweise\"],\"oOi11l\":[\"Nach unten scrollen\"],\"oPYIL5\":[\"Netzwerk\"],\"oQEzQR\":[\"Neue Direktnachricht\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Man-in-the-Middle-Angriffe auf Server-Verbindungen sind möglich\"],\"oeqmmJ\":[\"Vertrauenswürdige Quellen\"],\"optX0N\":[\"vor \",[\"0\"],\" Std.\"],\"ovBPCi\":[\"Standard\"],\"p0Z69r\":[\"Muster darf nicht leer sein\"],\"p1KgtK\":[\"Audio konnte nicht geladen werden\"],\"p59pEv\":[\"Weitere Details\"],\"p7sRI6\":[\"Anderen mitteilen, wenn Sie tippen\"],\"pBm1od\":[\"Geheimer Kanal\"],\"pNmiXx\":[\"Ihr Standard-Nickname für alle Server\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Kontopasswort\"],\"peNE68\":[\"Dauerhaft\"],\"plhHQt\":[\"Keine Daten\"],\"pm6+q5\":[\"Sicherheitswarnung\"],\"pn5qSs\":[\"Weitere Informationen\"],\"q0cR4S\":[\"ist jetzt bekannt als **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanal erscheint nicht in LIST- oder NAMES-Befehlen\"],\"qLpTm/\":[\"Reaktion \",[\"emoji\"],\" entfernen\"],\"qVkGWK\":[\"Anheften\"],\"qY8wNa\":[\"Homepage\"],\"qb0xJ7\":[\"Platzhalter: * beliebige Zeichen, ? ein einzelnes Zeichen. Beispiele: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Kanalschlüssel (+k)\"],\"qtoOYG\":[\"Kein Limit\"],\"r1W2AS\":[\"Dateiserver-Bild\"],\"rIPR2O\":[\"Thema gesetzt vor (Min.)\"],\"rMMSYo\":[\"Maximale Länge ist \",[\"0\"]],\"rWtzQe\":[\"Das Netzwerk hat sich geteilt und wieder verbunden. ✅\"],\"rYG2u6\":[\"Bitte warten...\"],\"rdUucN\":[\"Vorschau\"],\"rjGI/Q\":[\"Datenschutz\"],\"rk8iDX\":[\"GIFs werden geladen...\"],\"rn6SBY\":[\"Ton einschalten\"],\"s/UKqq\":[\"Wurde aus dem Kanal geworfen\"],\"s7oqXR\":[\"Netzwerk auswählen\"],\"s8cATI\":[\"ist \",[\"channelName\"],\" beigetreten\"],\"sCO9ue\":[\"Die Verbindung zu <0>\",[\"serverName\"],\"0> hat folgende Sicherheitsbedenken:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"ist jetzt bekannt als **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" hat dich eingeladen, \",[\"channel\"],\" beizutreten\"],\"sUBSbK\":[\"Noch keine Upstream-Netzwerke.\"],\"sby+1/\":[\"Zum Kopieren klicken\"],\"sfN25C\":[\"Ihr echter oder vollständiger Name\"],\"sliuzR\":[\"Link öffnen\"],\"sqrO9R\":[\"Benutzerdefinierte Erwähnungen\"],\"sr6RdJ\":[\"Mehrzeilig mit Shift+Enter\"],\"swrCpB\":[\"Der Kanal wurde von \",[\"oldName\"],\" in \",[\"newName\"],\" umbenannt von \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Erweitert\"],\"t/YqKh\":[\"Entfernen\"],\"t47eHD\":[\"Ihr eindeutiger Bezeichner auf diesem Server\"],\"tAkAh0\":[\"URL mit optionaler \",[\"size\"],\"-Substitution. Beispiel: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Seitenleiste der Kanalliste ein- oder ausblenden\"],\"tfDRzk\":[\"Speichern\"],\"tiBsJk\":[\"hat \",[\"channelName\"],\" verlassen\"],\"tt4/UD\":[\"hat sich abgemeldet (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Nickname {nick} bereits vergeben, versuche es mit {newNick}\"],\"u0a8B4\":[\"Als IRC-Operator für Verwaltungszugriff authentifizieren\"],\"u0rWFU\":[\"Erstellt nach (Min.)\"],\"u72w3t\":[\"Zu ignorierende Benutzer und Muster\"],\"u7jc2L\":[\"hat sich abgemeldet\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Speichern fehlgeschlagen: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC-Server:\"],\"ukyW4o\":[\"Deine Einladungslinks\"],\"usSSr/\":[\"Zoomstufe\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Shift+Enter für neue Zeilen (Enter sendet)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Kein Thema\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Sprache\"],\"vaHYxN\":[\"Echter Name\"],\"vhjbKr\":[\"Abwesend\"],\"w/nogd\":[[\"0\"],\" Netzwerk\",[\"1\"],\" — eines zum Beitreten auswählen\"],\"w4NYox\":[[\"title\"],\" Client\"],\"w8xQRx\":[\"Ungültiger Wert\"],\"wFjjxZ\":[\"wurde von \",[\"username\"],\" aus \",[\"channelName\"],\" gekickt (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Keine Bann-Ausnahmen gefunden\"],\"wPrGnM\":[\"Kanal-Administrator\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Anzeigen, wenn Benutzer Kanäle betreten oder verlassen\"],\"whqZ9r\":[\"Weitere Wörter oder Phrasen zum Hervorheben\"],\"wm7RV4\":[\"Benachrichtigungston\"],\"wz/Yoq\":[\"Deine Nachrichten könnten abgefangen werden, wenn sie zwischen Servern weitergeleitet werden\"],\"x3+y8b\":[\"So viele Personen haben sich über diesen Link registriert\"],\"xCJdfg\":[\"Leeren\"],\"xOTzt5\":[\"gerade eben\"],\"xUHRTR\":[\"Beim Verbinden automatisch als Operator authentifizieren\"],\"xWHwwQ\":[\"Sperren\"],\"xYilR2\":[\"Medien\"],\"xbi8D6\":[\"Dieser Server unterstützt keine Einladungslinks (die<0>obby.world/invitation0>-Capability wird nicht angekündigt). Du kannst trotzdem normal chatten; dieses Panel ist für Netzwerke mit obbyircd.\"],\"xceQrO\":[\"Nur sichere Websockets werden unterstützt\"],\"xdtXa+\":[\"Kanalname\"],\"xfXC7q\":[\"Textkanäle\"],\"xlCYOE\":[\"Weitere Nachrichten werden geladen...\"],\"xlhswE\":[\"Mindestwert ist \",[\"0\"]],\"xq97Ci\":[\"Wort oder Phrase hinzufügen...\"],\"xuRqRq\":[\"Client-Limit (+l)\"],\"xwF+7J\":[[\"0\"],\" tippt...\"],\"y1eoq1\":[\"Link kopieren\"],\"yJztBY\":[\"Netzwerk löschen\"],\"yNeucF\":[\"Dieser Server unterstützt keine erweiterten Profilmetadaten (IRCv3 METADATA). Felder wie Avatar, Anzeigename und Status sind nicht verfügbar.\"],\"yPlrca\":[\"Kanal-Avatar\"],\"yQE2r9\":[\"Laden\"],\"ySU+JY\":[\"deine@email.de\"],\"yTX1Rt\":[\"Oper-Benutzername\"],\"yYOzWD\":[\"Protokolle\"],\"yfx9Re\":[\"IRC-Operatorpasswort\"],\"ygCKqB\":[\"Stopp\"],\"ymDxJx\":[\"IRC-Operatorbenutzername\"],\"yrpRsQ\":[\"Nach Name sortieren\"],\"yz7wBu\":[\"Schließen\"],\"zJw+jA\":[\"setzt Modus: \",[\"0\"]],\"zbymaY\":[\"vor \",[\"0\"],\" Min.\"],\"zebeLu\":[\"oper-Benutzername eingeben\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/de/messages.po b/src/locales/de/messages.po
index 3bf512e8..30f562f2 100644
--- a/src/locales/de/messages.po
+++ b/src/locales/de/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - IRC in die Zukunft bringen"
msgid "— open in viewer"
msgstr "— im Viewer öffnen"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(übernehmen)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(unverändert)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} und {1} tippen..."
msgid "{0} is typing..."
msgstr "{0} tippt..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} Netzwerk{1} — eines zum Beitreten auswählen"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -205,6 +221,12 @@ msgstr "Einladungs-Maske hinzufügen (z.B. nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "IRC-Server hinzufügen"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Netzwerk hinzufügen"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Regel hinzufügen"
msgid "Add Server"
msgstr "Server hinzufügen"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Erstes Netzwerk hinzufügen"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Weitere Details"
@@ -377,6 +403,10 @@ msgstr "Zurück"
msgid "Back to image"
msgstr "Zurück zum Bild"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Zurück zur Netzwerkliste"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "{username} per hostmask sperren (verhindert erneutes Beitreten von derselben IP/Host)"
@@ -424,6 +454,10 @@ msgstr "Alle Kanäle auf dem Server durchsuchen"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Verbindung abbrechen"
msgid "Cancel reply"
msgstr "Antwort abbrechen"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Akzentfarbe ändern"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Kanalnamen ändern (nur Operatoren)"
@@ -564,7 +603,7 @@ msgstr "Suche löschen"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "Erneut klicken zum Bestätigen"
+msgstr "Erneut klicken zum Bestätigen"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "Client-Limit (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -666,9 +707,10 @@ msgstr "Benachrichtigungstöne und Hervorhebungen konfigurieren"
#: src/components/ui/InvitationsPanel.tsx
msgid "Confirm?"
-msgstr "Bestätigen?"
+msgstr "Bestätigen?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Verbinden"
@@ -814,27 +856,52 @@ msgstr "Kanal löschen"
msgid "Delete message"
msgstr "Nachricht löschen"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Netzwerk löschen"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Privatnachricht löschen"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "Diese Einladung löschen"
+msgstr "Diese Einladung löschen"
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "Diese Nachricht löschen? Dies kann nicht rückgängig gemacht werden."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Dieses Netzwerk löschen?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
msgstr "Beschreibung (optional, z.B. \"Beta-Tester Q3\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Trennen"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "<0>{0}0> trennen?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Vom soju-Bouncer trennen?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Netzwerk trennen?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Entdecken"
@@ -893,18 +960,29 @@ msgstr "Herunterladen"
msgid "Drop files to upload"
msgstr "Dateien hier ablegen zum Hochladen"
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "z. B. <0>wss://host:port/socket0>"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "z.B. 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Bearbeiten"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "{0} bearbeiten"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Profil bearbeiten"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "STARTSEITE"
msgid "Homepage"
msgstr "Homepage"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Host"
@@ -1323,7 +1402,7 @@ msgstr "Kanal verlassen"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Lass das Kanalfeld leer für eine allgemeine Netzwerkeinladung. Die Beschreibung ist nur für deine Aufzeichnungen — sie ist nur für dich in dieser Liste sichtbar."
+msgstr "Lass das Kanalfeld leer für eine allgemeine Netzwerkeinladung. Die Beschreibung ist nur für deine Aufzeichnungen â sie ist nur für dich in dieser Liste sichtbar."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "Den Kanal verlassen"
msgid "Let others know when you are typing"
msgstr "Anderen mitteilen, wenn Sie tippen"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Link-Vorschau"
@@ -1375,6 +1458,10 @@ msgstr "GIFs werden geladen..."
msgid "Loading more channels..."
msgstr "Weitere Kanäle werden geladen..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Netzwerke werden von deinem Bouncer geladen…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "WHOIS-Daten werden geladen..."
@@ -1565,10 +1652,21 @@ msgstr "Name:"
msgid "network"
msgstr "Netzwerk"
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Netzwerk gebunden über soju Bouncer"
+
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Netzwerkname"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Netzwerke auf {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Neue Direktnachricht"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (z.B. spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Keine Datei ausgewählt"
msgid "No flood profile"
msgstr "Kein Flood-Profil"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "kein Host festgelegt"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Keine Einladungen gefunden"
@@ -1680,7 +1783,7 @@ msgstr "Keine Ergebnisse gefunden"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "Kein Server ausgewählt. Wähle zuerst einen Server in der Seitenleiste; Einladungslinks werden pro Server verwaltet."
+msgstr "Kein Server ausgewählt. Wähle zuerst einen Server in der Seitenleiste; Einladungslinks werden pro Server verwaltet."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "Kein Thema gesetzt"
msgid "No unread mentions or messages"
msgstr "Keine ungelesenen Erwähnungen oder Nachrichten"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Noch keine Upstream-Netzwerke."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Keine Benutzer verfügbar"
@@ -1784,6 +1891,10 @@ msgstr "Ups! Netz-Split! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Öffnen"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Kanaleinstellungen öffnen"
@@ -1887,6 +1998,10 @@ msgstr "Privatnachricht anheften"
msgid "Pin this private message conversation"
msgstr "Dieses Privatgespräch anheften"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Klartext"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "Benutzer anschreiben"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Port"
@@ -2006,6 +2122,7 @@ msgstr "hat auf diese Nachricht reagiert"
msgid "Read more"
msgstr "Mehr lesen"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Grund"
msgid "Reason (optional)"
msgstr "Grund (optional)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Erneut mit Server verbinden"
@@ -2095,6 +2213,7 @@ msgstr "Regeln"
msgid "Safe"
msgstr "Sicher"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Vor-/Zurückspulen"
msgid "Select a channel"
msgstr "Kanal auswählen"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Netzwerk auswählen"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Mitglied auswählen"
@@ -2276,6 +2399,10 @@ msgstr "Server-Operatoren im Netzwerk könnten deine Nachrichten lesen"
msgid "Server Password"
msgstr "Server-Passwort"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Serverpasswort (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Die Server-zu-Server-Kommunikation verwendet möglicherweise unverschlüsselte Verbindungen"
@@ -2380,6 +2507,12 @@ msgstr "Angemeldet seit"
msgid "Software:"
msgstr "Software:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "soju Bouncer (Steuerung)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Nach Name sortieren"
@@ -2457,7 +2590,11 @@ msgstr "Dieses Bild ist abgelaufen"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "So viele Personen haben sich über diesen Link registriert"
+msgstr "So viele Personen haben sich über diesen Link registriert"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Damit wird das Netzwerk aus deinem soju-Bouncer entfernt. Um es wieder zu nutzen, musst du es erneut hinzufügen."
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "Dieser Server unterstützt keine erweiterten Profilmetadaten (IRCv3 META
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Dieser Server unterstützt keine Einladungslinks (die<0>obby.world/invitation0>-Capability wird nicht angekündigt). Du kannst trotzdem normal chatten; dieses Panel ist für Netzwerke mit obbyircd."
+msgstr "Dieser Server unterstützt keine Einladungslinks (die<0>obby.world/invitation0>-Capability wird nicht angekündigt). Du kannst trotzdem normal chatten; dieses Panel ist für Netzwerke mit obbyircd."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Dieser Server unterstützt nur einen Verbindungstyp"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Damit werden auch die {0} unten gebundenen Netzwerke geschlossen."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Damit wird auch das unten gebundene Netzwerk geschlossen."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Zeit (Min)"
@@ -2479,6 +2625,10 @@ msgstr "Zeit (Min)"
msgid "Time Window (seconds)"
msgstr "Zeitfenster (Sekunden)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Thema:"
msgid "Total: {0}"
msgstr "Gesamt: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Transport"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Vertrauenswürdige Quellen"
@@ -2641,6 +2795,7 @@ msgstr "Benutzerprofil"
msgid "User Settings"
msgstr "Benutzereinstellungen"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Weit – Breiterer Schutzbereich"
msgid "Will default to 'no reason' if left empty"
msgstr "Standardmäßig 'kein Grund', wenn leer gelassen"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Ja, löschen"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2814,6 +2973,11 @@ msgstr "Du hast noch keine Einladungslinks erstellt. Verwende das Formular oben,
msgid "You invited {target} to join {channel}"
msgstr "Du hast {target} eingeladen, {channel} beizutreten"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Du bist mit <0>{0}0> verbunden."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Ihr Kontopasswort zur Authentifizierung"
@@ -2822,6 +2986,10 @@ msgstr "Ihr Kontopasswort zur Authentifizierung"
msgid "Your account username for authentication"
msgstr "Ihr Kontobenutzername zur Authentifizierung"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Dein Bouncer hat noch keine Netzwerke. Füge eines hinzu, um zu starten."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Ihr Standard-Nickname für alle Server"
diff --git a/src/locales/en/messages.mjs b/src/locales/en/messages.mjs
index 1387cd2d..491f526a 100644
--- a/src/locales/en/messages.mjs
+++ b/src/locales/en/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Invalid pattern format. Use nick!user@host format (wildcards * allowed)\"],\"+6NQQA\":[\"General Support Channel\"],\"+6NyRG\":[\"Client\"],\"+K0AvT\":[\"Disconnect\"],\"+cyFdH\":[\"Default message when marking yourself as away\"],\"+mVPqU\":[\"Render markdown formatting in messages\"],\"+vqCJH\":[\"Your account username for authentication\"],\"+yPBXI\":[\"Choose file\"],\"+zy2Nq\":[\"Type\"],\"/09cao\":[\"Low Link Security (Level \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Users outside the channel cannot send messages to it\"],\"/4C8U0\":[\"Copy all\"],\"/6BzZF\":[\"Toggle Member List\"],\"/AkXyp\":[\"Confirm?\"],\"/TNOPk\":[\"User is away\"],\"/XQgft\":[\"Discover\"],\"/cF7Rs\":[\"Volume\"],\"/dqduX\":[\"Next page\"],\"/fc3q4\":[\"All Content\"],\"/kISDh\":[\"Enable Notification Sounds\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Play sounds for mentions and messages\"],\"0/0ZGA\":[\"Channel Name Mask\"],\"0D6j7U\":[\"Learn more about custom rules →\"],\"0XsHcR\":[\"Kick User\"],\"0ZpE//\":[\"Sort by Users\"],\"0bEPwz\":[\"Set Away\"],\"0dGkPt\":[\"Expand channel list\"],\"0gS7M5\":[\"Display Name\"],\"0kS+M8\":[\"ExampleNET\"],\"0rgoY7\":[\"Only connect to servers you choose\"],\"0wdd7X\":[\"Join\"],\"0wkVYx\":[\"Private Messages\"],\"111uHX\":[\"Link preview\"],\"196EG4\":[\"Delete Private Chat\"],\"1DSr1i\":[\"Register for an account\"],\"1O/24y\":[\"Toggle Channel List\"],\"1VPJJ2\":[\"External Link Warning\"],\"1ZC/dv\":[\"No unread mentions or messages\"],\"1pO1zi\":[\"Server name is required\"],\"1uwfzQ\":[\"View Channel Topic\"],\"268g7c\":[\"Enter display name\"],\"2F9+AZ\":[\"No raw IRC traffic captured yet. Try connecting or sending a message.\"],\"2FOFq1\":[\"Server operators on the network could potentially read your messages\"],\"2FYpfJ\":[\"More\"],\"2HF1Y2\":[[\"inviter\"],\" has invited \",[\"target\"],\" to join \",[\"channel\"]],\"2I70QL\":[\"View user profile information\"],\"2QYdmE\":[\"Users:\"],\"2QpEjG\":[\"left\"],\"2YE223\":[\"Message #\",[\"0\"],\" (Enter for new line, Shift+Enter to send)\"],\"2bimFY\":[\"Use server password\"],\"2iTmdZ\":[\"Local Storage:\"],\"2odkwe\":[\"Strict - More aggressive protection\"],\"2uDhbA\":[\"Enter username to invite\"],\"2ygf/L\":[\"← Back\"],\"2zEgxj\":[\"Search GIFs...\"],\"3RdPhl\":[\"Rename Channel\"],\"3THokf\":[\"Voiced User\"],\"3TSz9S\":[\"Minimize\"],\"3jBDvM\":[\"Channel Display Name\"],\"3ryuFU\":[\"Optional crash reports to improve the app\"],\"3uBF/8\":[\"Close viewer\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Enter account name...\"],\"4/Rr0R\":[\"Invite a user to the current channel\"],\"4EZrJN\":[\"Rules\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Flood Profile (+F)\"],\"4RZQRK\":[\"What are you up to?\"],\"4hfTrB\":[\"Nickname\"],\"4n99LO\":[\"Already in \",[\"0\"]],\"4t6vMV\":[\"Automatically switch to single line for short messages\"],\"4vsHmf\":[\"Time (min)\"],\"5+INAX\":[\"Highlight messages that mention you\"],\"5R5Pv/\":[\"Oper Name\"],\"678PKt\":[\"Network Name\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Password required to join the channel. Leave empty to remove the key.\"],\"6HhMs3\":[\"Quit Message\"],\"6V3Ea3\":[\"Copied\"],\"6lGV3K\":[\"Show less\"],\"6yFOEi\":[\"Enter oper password...\"],\"7+IHTZ\":[\"No file chosen\"],\"73hrRi\":[\"nick!user@host (e.g., spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Send private message\"],\"7U1W7c\":[\"Very Relaxed\"],\"7Y1YQj\":[\"Realname:\"],\"7YHArF\":[\"— open in viewer\"],\"7fjnVl\":[\"Search users...\"],\"7jL88x\":[\"Delete this message? This cannot be undone.\"],\"7nGhhM\":[\"What's on your mind?\"],\"7sEpu1\":[\"Members — \",[\"0\"]],\"7sNhEz\":[\"Username\"],\"8H0Q+x\":[\"Learn more about profiles →\"],\"8Phu0A\":[\"Display when users change their nickname\"],\"8XTG9e\":[\"Enter oper password\"],\"8XsV2J\":[\"Retry sending\"],\"8ZsakT\":[\"Password\"],\"8kR84m\":[\"You are about to open an external link:\"],\"8lCgih\":[\"Remove Rule\"],\"8o3dPc\":[\"Drop files to upload\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"joined\"],\"other\":[\"joined \",[\"joinCount\"],\" times\"]}]],\"9BMLnJ\":[\"Reconnect to server\"],\"9OEgyT\":[\"Add reaction\"],\"9PQ8m2\":[\"G-Line (Global Ban)\"],\"9Qs99X\":[\"Email:\"],\"9QupBP\":[\"Remove pattern\"],\"9bG48P\":[\"Sending\"],\"9f5f0u\":[\"Questions about privacy? Contact us:\"],\"9unqs3\":[\"Away:\"],\"9v3hwv\":[\"No servers found.\"],\"9zb2WA\":[\"Connecting\"],\"A1taO8\":[\"Search\"],\"A2adVi\":[\"Send Typing Notifications\"],\"A9Rhec\":[\"Channel Name\"],\"AWOSPo\":[\"Zoom in\"],\"AXSpEQ\":[\"Oper on Connect\"],\"AeXO77\":[\"Account\"],\"AhNP40\":[\"Seek\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Changed nickname\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Cancel reply\"],\"ApSx0O\":[\"Found \",[\"0\"],\" messages matching \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"No results found\"],\"AyNqAB\":[\"Display all server events in chat\"],\"B/QqGw\":[\"Away from keyboard\"],\"B8AaMI\":[\"This field is required\"],\"BA2c49\":[\"Server doesn't support advanced LIST filtering\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" and \",[\"3\"],\" others are typing...\"],\"BGul2A\":[\"You have unsaved changes. Are you sure you want to close without saving?\"],\"BIf9fi\":[\"Your status message\"],\"BPm98R\":[\"No server is selected. Pick a server from the sidebar first; invite links are managed per-server.\"],\"BZz3md\":[\"Your personal website\"],\"Bgm/H7\":[\"Allow entering multiple lines of text\"],\"BiQIl1\":[\"Pin this private message conversation\"],\"BlNZZ2\":[\"Click to jump to message\"],\"Bowq3c\":[\"Only operators can change the channel topic\"],\"Btozzp\":[\"This image has expired\"],\"Bycfjm\":[\"Total: \",[\"0\"]],\"C6IBQc\":[\"Copy entire JSON\"],\"C9L9wL\":[\"Data Collection\"],\"CDq4wC\":[\"Moderate User\"],\"CHVRxG\":[\"Message @\",[\"0\"],\" (Shift+Enter for new line)\"],\"CN9zdR\":[\"Oper name and password are required\"],\"CW3sYa\":[\"Add reaction \",[\"emoji\"]],\"CaAkqd\":[\"Show Quits\"],\"CbvaYj\":[\"Ban by Nickname\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Select a channel\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"System\"],\"D28t6+\":[\"joined and quit\"],\"DB8zMK\":[\"Apply\"],\"DBcWHr\":[\"Custom notification sound file\"],\"DTy9Xw\":[\"Media Previews\"],\"Dj4pSr\":[\"Choose a secure password\"],\"Du+zn+\":[\"Searching...\"],\"Du2T2f\":[\"Setting not found\"],\"DwsSVQ\":[\"Apply Filters & Refresh\"],\"E3W/zd\":[\"Default Nickname\"],\"E6nRW7\":[\"Copy URL\"],\"E703RG\":[\"Modes:\"],\"EAeu1Z\":[\"Send Invite\"],\"EFKJQT\":[\"Setting\"],\"EGPQBv\":[\"Custom Flood Rules (+f)\"],\"ELik0r\":[\"View Full Privacy Policy\"],\"EPbeC2\":[\"View or edit the channel topic\"],\"EQCDNT\":[\"Enter oper username...\"],\"EUvulZ\":[\"Found 1 message matching \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Next image\"],\"EdQY6l\":[\"None\"],\"EnqLYU\":[\"Search servers...\"],\"F0OKMc\":[\"Edit Server\"],\"F6Int2\":[\"Enable Highlights\"],\"FDoLyE\":[\"Max Users\"],\"FUU/hZ\":[\"Control how much external media is loaded in chat.\"],\"Fdp03t\":[\"on\"],\"FfPWR0\":[\"Modal\"],\"FjkaiT\":[\"Zoom out\"],\"FlqOE9\":[\"What this means:\"],\"FolHNl\":[\"Manage your account and authentication\"],\"Fp2Dif\":[\"Quit the server\"],\"G5KmCc\":[\"GZ-Line (Global Z-Line)\"],\"GDs0lz\":[\"<0>Risk:0> Sensitive information (messages, private conversations, authentication details) could be exposed to network administrators or attackers positioned between IRC servers.\"],\"GR+2I3\":[\"Add invitation mask (e.g., nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Close popped out server notices\"],\"GdhD7H\":[\"Click again to confirm\"],\"GlHnXw\":[\"Nick change failed: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Preview:\"],\"GtmO8/\":[\"from\"],\"GtuHUQ\":[\"Rename this channel on the server. All users will see the new name.\"],\"GuGfFX\":[\"Toggle search\"],\"GxkJXS\":[\"Uploading...\"],\"GzbwnK\":[\"Joined the channel\"],\"GzsUDB\":[\"Extended Profile\"],\"H/PnT8\":[\"Insert emoji\"],\"H6Izzl\":[\"Your preferred color code\"],\"H9jIv+\":[\"Show Joins/Parts\"],\"HAKBY9\":[\"Upload Files\"],\"HdE1If\":[\"Channel\"],\"Hk4AW9\":[\"Your preferred display name\"],\"HmHDk7\":[\"Select Member\"],\"HrQzPU\":[\"Channels on \",[\"networkName\"]],\"I2tXQ5\":[\"Message @\",[\"0\"],\" (Enter for new line, Shift+Enter to send)\"],\"I6bw/h\":[\"Ban User\"],\"I92Z+b\":[\"Enable notifications\"],\"I9D72S\":[\"Are you sure you want to delete this message? This action cannot be undone.\"],\"IA+1wo\":[\"Display when users are kicked from channels\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Save Changes\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" and \",[\"2\"],\" are typing...\"],\"IgrLD/\":[\"Pause\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Reply\"],\"IoHMnl\":[\"Maximum value is \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Connecting...\"],\"J5T9NW\":[\"User Information\"],\"J8Y5+z\":[\"Oops! The net split! ⚠️\"],\"JBHkBA\":[\"Left the channel\"],\"JCwL0Q\":[\"Enter reason (optional)\"],\"JFciKP\":[\"Toggle\"],\"JXGkhG\":[\"Change the channel name (operators only)\"],\"JcD7qf\":[\"More actions\"],\"JdkA+c\":[\"Secret (+s)\"],\"Jmu12l\":[\"Server Channels\"],\"JvQ++s\":[\"Enable Markdown\"],\"K2jwh/\":[\"No WHOIS data available\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Delete message\"],\"KKBlUU\":[\"Embed\"],\"KM0pLb\":[\"Welcome to the channel!\"],\"KR6W2h\":[\"Unignore User\"],\"KV+Bi1\":[\"Invite-Only (+i)\"],\"KdCtwE\":[\"How many seconds to monitor for flood activity before resetting counters\"],\"Kkezga\":[\"Server Password\"],\"KsiQ/8\":[\"Users must be invited to join the channel\"],\"L+gB/D\":[\"Channel Information\"],\"LC1a7n\":[\"The IRC server has reported that its server-to-server links have a low security level. This means that when your messages are relayed between IRC servers in the network, they may not be properly encrypted or the SSL/TLS certificates may not be validated correctly.\"],\"LNfLR5\":[\"Show Kicks\"],\"LQb0W/\":[\"Show All Events\"],\"LU7/yA\":[\"Alternative name for display in the UI. May contain spaces, emoji, and special characters. The real channel name (\",[\"channelName\"],\") will still be used for IRC commands.\"],\"LUb9O7\":[\"Valid server port is required\"],\"LV4fT6\":[\"Description (optional, e.g. \\\"Beta testers Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Privacy Policy\"],\"LcuSDR\":[\"Manage your profile information and metadata\"],\"LqLS9B\":[\"Show Nick Changes\"],\"LsDQt2\":[\"Channel Settings\"],\"LtI9AS\":[\"Owner\"],\"LuNhhL\":[\"reacted to this message\"],\"M/AZNG\":[\"URL to your avatar image\"],\"M/WIer\":[\"Send Message\"],\"M8er/5\":[\"Name:\"],\"MHk+7g\":[\"Previous image\"],\"MRorGe\":[\"PM User\"],\"MVbSGP\":[\"Time Window (seconds)\"],\"MkpcsT\":[\"Your messages and settings are stored locally on your device\"],\"N/hDSy\":[\"Mark as bot - usually 'on' or empty\"],\"N7TQbE\":[\"Invite User to \",[\"channelName\"]],\"NCca/o\":[\"Enter default nickname...\"],\"Nqs6B9\":[\"Shows all external media. Any URL may cause a request to an unknown server.\"],\"Nt+9O7\":[\"Use WebSocket instead of raw TCP\"],\"NxIHzc\":[\"Kill User\"],\"O+v/cL\":[\"Browse all channels on the server\"],\"ODwSCk\":[\"Send a GIF\"],\"OGQ5kK\":[\"Configure notification sounds and highlights\"],\"OIPt1Z\":[\"Show or hide the member list sidebar\"],\"OKSNq/\":[\"Very Strict\"],\"ONWvwQ\":[\"Upload\"],\"OVKoQO\":[\"Your account password for authentication\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Bringing IRC to the future\"],\"OhCpra\":[\"Set a topic…\"],\"OkltoQ\":[\"Ban \",[\"username\"],\" by nickname (prevents them from rejoining with the same nick)\"],\"P+t/Te\":[\"No additional data\"],\"P42Wcc\":[\"Safe\"],\"PD38l0\":[\"Channel avatar preview\"],\"PD9mEt\":[\"Type a message...\"],\"PPqfdA\":[\"Open channel configuration settings\"],\"PSCjfZ\":[\"The topic that will be displayed for this channel. All users can see the topic.\"],\"PZCecv\":[\"PDF preview\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 time\"],\"other\":[[\"c\"],\" times\"]}]],\"PguS2C\":[\"Add exception mask (e.g., nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Showing \",[\"displayedChannelsCount\"],\" of \",[\"0\"],\" channels\"],\"PqhVlJ\":[\"Ban User (by Hostmask)\"],\"Q+chwU\":[\"Username:\"],\"Q2QY4/\":[\"Delete this invite\"],\"Q6hhn8\":[\"Preferences\"],\"QF4a34\":[\"Please enter a username\"],\"QGqSZ2\":[\"Color & Formatting\"],\"QJQd1J\":[\"Edit Profile\"],\"QSzGDE\":[\"Idle\"],\"QUlny5\":[\"Welcome to \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Read more\"],\"QuSkCF\":[\"Filter channels...\"],\"QwUrDZ\":[\"changed the topic to: \",[\"topic\"]],\"R0UH07\":[\"Image \",[\"0\"],\" of \",[\"1\"]],\"R7SsBE\":[\"Mute\"],\"R8rf1X\":[\"Click to set topic\"],\"RArB3D\":[\"was kicked from \",[\"channelName\"],\" by \",[\"username\"]],\"RI3cWd\":[\"Discover the world of IRC with ObsidianIRC\"],\"RIfHS5\":[\"Create a new invite link\"],\"RMMaN5\":[\"Moderated (+m)\"],\"RWw9Lg\":[\"Close modal\"],\"RZ2BuZ\":[\"Account registration for \",[\"account\"],\" requires verification: \",[\"message\"]],\"RySp6q\":[\"Hide comments\"],\"SPKQTd\":[\"Nickname is required\"],\"SPVjfj\":[\"Will default to 'no reason' if left empty\"],\"SQKPvQ\":[\"Invite User\"],\"SkZcl+\":[\"Choose a predefined flood protection profile. These profiles provide balanced protection settings for different use cases.\"],\"Slr+3C\":[\"Min Users\"],\"Spnlre\":[\"You invited \",[\"target\"],\" to join \",[\"channel\"]],\"T/ckN5\":[\"Open in viewer\"],\"T91vKp\":[\"Play\"],\"TV2Wdu\":[\"Learn how we handle your data and protect your privacy.\"],\"TgFpwD\":[\"Applying...\"],\"TkzSFB\":[\"No Changes\"],\"TtserG\":[\"Enter real name\"],\"Ttz9J1\":[\"Enter password...\"],\"Tz0i8g\":[\"Settings\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"React\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"You haven't created any invite links yet. Use the form above to mint your first one.\"],\"UGT5vp\":[\"Save Settings\"],\"UV5hLB\":[\"No bans found\"],\"Uaj3Nd\":[\"Status Messages\"],\"Ue3uny\":[\"Default (no profile)\"],\"UkARhe\":[\"Normal - Standard protection\"],\"Umn7Cj\":[\"No comments yet. Be the first!\"],\"UtUIRh\":[[\"0\"],\" older messages\"],\"UwzP+U\":[\"Secure Connection\"],\"V0/A4O\":[\"Channel Owner\"],\"V4qgxE\":[\"Created Before (min ago)\"],\"V8yTm6\":[\"Clear search\"],\"VJMMyz\":[\"ObsidianIRC - Bringing IRC to the future\"],\"VJScHU\":[\"Reason\"],\"VLsmVV\":[\"Mute notifications\"],\"VbyRUy\":[\"Comments\"],\"Vmx0mQ\":[\"Set by:\"],\"VqnIZz\":[\"View our privacy policy and data practices\"],\"VrMygG\":[\"Minimum length is \",[\"0\"]],\"VrnTui\":[\"Your pronouns, shown in your profile\"],\"W8E3qn\":[\"Authenticated Account\"],\"WAakm9\":[\"Delete Channel\"],\"WFxTHC\":[\"Add ban mask (e.g., nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Server host is required\"],\"WRYdXW\":[\"Audio position\"],\"WUOH5B\":[\"Ignore User\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Show 1 more item\"],\"other\":[\"Show \",[\"1\"],\" more items\"]}]],\"WYxRzo\":[\"Create and manage your invite links\"],\"Wd38W1\":[\"Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list.\"],\"Weq9zb\":[\"General\"],\"Wfj7Sk\":[\"Mute or unmute notification sounds\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"User Profile\"],\"X6S3lt\":[\"Search settings, channels, servers...\"],\"XEHan5\":[\"Continue Anyway\"],\"XI1+wb\":[\"Invalid format\"],\"XIXeuC\":[\"Message @\",[\"0\"]],\"XMS+k4\":[\"Start Private Message\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Unpin Private Chat\"],\"Xm/s+u\":[\"Display\"],\"Xp2n93\":[\"Shows media from your server's trusted file host. No requests are made to external services.\"],\"XvjC4F\":[\"Saving...\"],\"Y/qryO\":[\"No users found matching your search\"],\"YAqRpI\":[\"Account registration successful for \",[\"account\"],\": \",[\"message\"]],\"YEfzvP\":[\"Protected Topic (+t)\"],\"YQOn6a\":[\"Collapse member list\"],\"YRCoE9\":[\"Channel Operator\"],\"YURQaF\":[\"View Profile\"],\"YdBSvr\":[\"Control media display and external content\"],\"Yj6U3V\":[\"No Central Server:\"],\"YjvpGx\":[\"Pronouns\"],\"YqH4l4\":[\"No key\"],\"YyUPpV\":[\"Account:\"],\"ZJSWfw\":[\"Message shown when you disconnect from the server\"],\"ZR1dJ4\":[\"Invitations\"],\"ZdWg0V\":[\"Open in browser\"],\"ZhRBbl\":[\"Search messages…\"],\"Zmcu3y\":[\"Advanced Filters\"],\"a2/8e5\":[\"Topic Set After (min ago)\"],\"aHKcKc\":[\"Previous page\"],\"aJTbXX\":[\"Oper Password\"],\"aQryQv\":[\"Pattern already exists\"],\"aW9pLN\":[\"Maximum number of users allowed in the channel. Leave empty for no limit.\"],\"ah4fmZ\":[\"Also shows previews from YouTube, Vimeo, SoundCloud, and similar known services.\"],\"aifXak\":[\"No media in this channel\"],\"ap2zBz\":[\"Relaxed\"],\"az8lvo\":[\"Off\"],\"azXSNo\":[\"Expand member list\"],\"azdliB\":[\"Login to an account\"],\"b26wlF\":[\"she/her\"],\"bD/+Ei\":[\"Strict\"],\"bQ6BJn\":[\"Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded.\"],\"beV7+y\":[\"The user will receive an invitation to join \",[\"channelName\"],\".\"],\"bk84cH\":[\"Away Message\"],\"bkHdLj\":[\"Add IRC Server\"],\"bmQLn5\":[\"Add Rule\"],\"bwRvnp\":[\"Action\"],\"c8+EVZ\":[\"Verified account\"],\"cGYUlD\":[\"No media previews are loaded.\"],\"cLF98o\":[\"Show comments (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"No users available\"],\"cSgpoS\":[\"Pin Private Chat\"],\"cde3ce\":[\"Message <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Copy formatted output\"],\"cl/A5J\":[\"Welcome to \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Delete\"],\"coPLXT\":[\"We don't store your IRC communications on our servers\"],\"crYH/6\":[\"SoundCloud player\"],\"d3sis4\":[\"Add Server\"],\"d9aN5k\":[\"Remove \",[\"username\"],\" from the channel\"],\"dEgA5A\":[\"Cancel\"],\"dGi1We\":[\"Unpin this private message conversation\"],\"dJVuyC\":[\"left \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"to\"],\"dXqxlh\":[\"<0>⚠️ Security Risk!0> This connection may be vulnerable to interception or man-in-the-middle attacks.\"],\"da9Q/R\":[\"Changed channel modes\"],\"dhJN3N\":[\"Show comments\"],\"dj2xTE\":[\"Dismiss notification\"],\"dpCzmC\":[\"Flood Protection Settings\"],\"e9dQpT\":[\"Do you want to open this link in a new tab?\"],\"ePK91l\":[\"Edit\"],\"eYBDuB\":[\"Upload an image or provide a URL with optional \",[\"size\"],\" substitution for dynamic sizing\"],\"edBbee\":[\"Ban \",[\"username\"],\" by hostmask (prevents them from rejoining from the same IP/host)\"],\"ekfzWq\":[\"User Settings\"],\"elPDWs\":[\"Customize your IRC client experience\"],\"eu2osY\":[\"<0>💡 Recommendation:0> Only proceed if you trust this server and understand the risks. Avoid sharing sensitive information or passwords over this connection.\"],\"euEhbr\":[\"Click to join \",[\"channel\"]],\"ez3vLd\":[\"Enable Multiline Input\"],\"f0J5Ki\":[\"Server-to-server communication may use unencrypted connections\"],\"f9BHJk\":[\"Warn User\"],\"fDOLLd\":[\"No channels found.\"],\"ffzDkB\":[\"Anonymous Analytics:\"],\"fq1GF9\":[\"Display when users disconnect from server\"],\"gEF57C\":[\"This server only supports one connection type\"],\"gJuLUI\":[\"Ignore List\"],\"gNzMrk\":[\"Current avatar\"],\"gjPWyO\":[\"Enter nickname...\"],\"gz6UQ3\":[\"Maximize\"],\"h6razj\":[\"Exclude Channel Name Mask\"],\"hG6jnw\":[\"No topic set\"],\"hG89Ed\":[\"Image\"],\"hYgDIe\":[\"Create\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"e.g., 100:1440\"],\"he3ygx\":[\"Copy\"],\"hehnjM\":[\"Amount\"],\"hzdLuQ\":[\"Only users with voice or higher can speak\"],\"i0qMbr\":[\"Home\"],\"iDNBZe\":[\"Notifications\"],\"iH8pgl\":[\"Back\"],\"iL9SZg\":[\"Ban User (by Nickname)\"],\"iNt+3c\":[\"Back to image\"],\"iQvi+a\":[\"Don't warn me about low link security for this server\"],\"iSLIjg\":[\"Connect\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Server Host\"],\"idD8Ev\":[\"Saved\"],\"iivqkW\":[\"Signed On\"],\"ij+Elv\":[\"Image preview\"],\"ilIWp7\":[\"Toggle Notifications\"],\"iuaqvB\":[\"Use * for wildcards. Examples: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Ban by Hostmask\"],\"jA4uoI\":[\"Topic:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Reason (optional)\"],\"jUV7CU\":[\"Upload Avatar\"],\"jW5Uwh\":[\"Control how much external media is loaded. Off / Safe / Trusted Sources / All Content.\"],\"jXzms5\":[\"Attachment options\"],\"jZlrte\":[\"Color\"],\"jfC/xh\":[\"Contact\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Load older messages\"],\"k3ID0F\":[\"Filter members…\"],\"k65gsE\":[\"Deep dive\"],\"k7Zgob\":[\"Cancel Connection\"],\"kAVx5h\":[\"No invitations found\"],\"kCLEPU\":[\"Connected To\"],\"kF5LKb\":[\"Ignored patterns:\"],\"kGeOx/\":[\"Join \",[\"0\"]],\"kITKr8\":[\"Loading channel modes...\"],\"kPpPsw\":[\"You are an IRC Operator\"],\"kWJmRL\":[\"You\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Copy JSON\"],\"krViRy\":[\"Click to copy as JSON\"],\"ks71ra\":[\"Exceptions\"],\"kw4lRv\":[\"Channel Half Operator\"],\"kxgIRq\":[\"Select or add a channel to get started.\"],\"ky6dWe\":[\"Avatar preview\"],\"l+GxCv\":[\"Loading channels...\"],\"l+IUVW\":[\"Account verification successful for \",[\"account\"],\": \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"reconnected\"],\"other\":[\"reconnected \",[\"reconnectCount\"],\" times\"]}]],\"l1l8sj\":[[\"0\"],\"d ago\"],\"l5NhnV\":[\"#channel (optional)\"],\"l5jmzx\":[[\"0\"],\" and \",[\"1\"],\" are typing...\"],\"lCF0wC\":[\"Refresh\"],\"lHy8N5\":[\"Loading more channels...\"],\"lasgrr\":[\"used\"],\"lbpf14\":[\"Join \",[\"value\"]],\"lfFsZ4\":[\"Channels\"],\"lkNdiH\":[\"Account Name\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Upload Image\"],\"loQxaJ\":[\"I'm Back\"],\"lvfaxv\":[\"HOME\"],\"m16xKo\":[\"Add\"],\"m8flAk\":[\"Preview (not yet uploaded)\"],\"mEPxTp\":[\"<0>⚠️ Be careful!0> Only open links from trusted sources. Malicious links can compromise your security or privacy.\"],\"mHGdhG\":[\"Server Information\"],\"mHS8lb\":[\"Message #\",[\"0\"]],\"mMYBD9\":[\"Wide - Broader protection scope\"],\"mTGsPd\":[\"Channel Topic\"],\"mU8j6O\":[\"No External Messages (+n)\"],\"mZp8FL\":[\"Auto Fallback to Single Line\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"Comments (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"User is authenticated\"],\"mwtcGl\":[\"Close comments\"],\"mzI/c+\":[\"Download\"],\"n3fGRk\":[\"set by \",[\"0\"]],\"nE9jsU\":[\"Relaxed - Less aggressive protection\"],\"nNflMD\":[\"Leave channel\"],\"nPXkBi\":[\"Loading WHOIS data...\"],\"nQnxxF\":[\"Message #\",[\"0\"],\" (Shift+Enter for new line)\"],\"nWMRxa\":[\"Unpin\"],\"nkC032\":[\"No flood profile\"],\"o69z4d\":[\"Send a warning message to \",[\"username\"]],\"o9ylQi\":[\"Search for GIFs to get started\"],\"oFGkER\":[\"Server Notices\"],\"oOi11l\":[\"Scroll to bottom\"],\"oPYIL5\":[\"network\"],\"oQEzQR\":[\"New DM\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Man-in-the-middle attacks on server links are possible\"],\"oeqmmJ\":[\"Trusted Sources\"],\"optX0N\":[[\"0\"],\"h ago\"],\"ovBPCi\":[\"Default\"],\"p0Z69r\":[\"Pattern cannot be empty\"],\"p1KgtK\":[\"Failed to load audio\"],\"p59pEv\":[\"Additional Details\"],\"p7sRI6\":[\"Let others know when you are typing\"],\"pBm1od\":[\"Secret channel\"],\"pNmiXx\":[\"Your default nickname for all servers\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Account Password\"],\"peNE68\":[\"Permanent\"],\"plhHQt\":[\"No data\"],\"pm6+q5\":[\"Security Warning\"],\"pn5qSs\":[\"Additional Information\"],\"q0cR4S\":[\"are now known as **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Channel won't appear in LIST or NAMES commands\"],\"qLpTm/\":[\"Remove reaction \",[\"emoji\"]],\"qVkGWK\":[\"Pin\"],\"qY8wNa\":[\"Homepage\"],\"qb0xJ7\":[\"Use wildcards: * matches any sequence, ? matches any single character. Examples: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Channel Key (+k)\"],\"qtoOYG\":[\"No limit\"],\"r1W2AS\":[\"Filehost image\"],\"rIPR2O\":[\"Topic Set Before (min ago)\"],\"rMMSYo\":[\"Maximum length is \",[\"0\"]],\"rWtzQe\":[\"The network split and rejoined. ✅\"],\"rYG2u6\":[\"Please wait...\"],\"rdUucN\":[\"Preview\"],\"rjGI/Q\":[\"Privacy\"],\"rk8iDX\":[\"Loading GIFs...\"],\"rn6SBY\":[\"Unmute\"],\"s/UKqq\":[\"Was kicked from the channel\"],\"s8cATI\":[\"joined \",[\"channelName\"]],\"sCO9ue\":[\"The connection to <0>\",[\"serverName\"],\"0> has the following security concerns:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"is now known as **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" has invited you to join \",[\"channel\"]],\"sby+1/\":[\"Click to copy\"],\"sfN25C\":[\"Your real or full name\"],\"sliuzR\":[\"Open Link\"],\"sqrO9R\":[\"Custom Mentions\"],\"sr6RdJ\":[\"Multiline on Shift+Enter\"],\"swrCpB\":[\"Channel has been renamed from \",[\"oldName\"],\" to \",[\"newName\"],\" by \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Advanced\"],\"t/YqKh\":[\"Remove\"],\"t47eHD\":[\"Your unique identifier on this server\"],\"tAkAh0\":[\"URL with optional \",[\"size\"],\" substitution for dynamic sizing. Example: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Show or hide the channel list sidebar\"],\"tfDRzk\":[\"Save\"],\"tiBsJk\":[\"left \",[\"channelName\"]],\"tt4/UD\":[\"quit (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Nickname {nick} already in use, retrying with {newNick}\"],\"u0a8B4\":[\"Authenticate as an IRC Operator for administrative access\"],\"u0rWFU\":[\"Created After (min ago)\"],\"u72w3t\":[\"Users and patterns to ignore\"],\"u7jc2L\":[\"quit\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Save failed: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC Servers:\"],\"ukyW4o\":[\"Your invite links\"],\"usSSr/\":[\"Zoom level\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Use Shift+Enter for new lines (Enter sends)\"],\"vERlcd\":[\"Profile\"],\"vK0RL8\":[\"No topic\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Language\"],\"vaHYxN\":[\"Real Name\"],\"vhjbKr\":[\"Away\"],\"w4NYox\":[[\"title\"],\" Client\"],\"w8xQRx\":[\"Invalid value\"],\"wFjjxZ\":[\"was kicked from \",[\"channelName\"],\" by \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"No ban exceptions found\"],\"wPrGnM\":[\"Channel Admin\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Display when users join or leave channels\"],\"whqZ9r\":[\"Additional words or phrases to highlight\"],\"wm7RV4\":[\"Notification Sound\"],\"wz/Yoq\":[\"Your messages could be intercepted when relayed between servers\"],\"x3+y8b\":[\"This many people registered through this link\"],\"xCJdfg\":[\"Clear\"],\"xOTzt5\":[\"just now\"],\"xUHRTR\":[\"Automatically authenticate as operator on connect\"],\"xWHwwQ\":[\"Bans\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks.\"],\"xceQrO\":[\"Only secure websockets are supported\"],\"xdtXa+\":[\"channel-name\"],\"xfXC7q\":[\"Text Channels\"],\"xlCYOE\":[\"Getting more messages...\"],\"xlhswE\":[\"Minimum value is \",[\"0\"]],\"xq97Ci\":[\"Add a word or phrase...\"],\"xuRqRq\":[\"Client Limit (+l)\"],\"xwF+7J\":[[\"0\"],\" is typing...\"],\"y1eoq1\":[\"Copy link\"],\"yNeucF\":[\"This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available.\"],\"yPlrca\":[\"Channel Avatar\"],\"yQE2r9\":[\"Loading\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Oper Username\"],\"yYOzWD\":[\"logs\"],\"yfx9Re\":[\"IRC operator password\"],\"ygCKqB\":[\"Stop\"],\"ymDxJx\":[\"IRC operator username\"],\"yrpRsQ\":[\"Sort by Name\"],\"yz7wBu\":[\"Close\"],\"zJw+jA\":[\"sets mode: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"m ago\"],\"zebeLu\":[\"Enter oper username\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Invalid pattern format. Use nick!user@host format (wildcards * allowed)\"],\"+6NQQA\":[\"General Support Channel\"],\"+6NyRG\":[\"Client\"],\"+K0AvT\":[\"Disconnect\"],\"+cyFdH\":[\"Default message when marking yourself as away\"],\"+mVPqU\":[\"Render markdown formatting in messages\"],\"+vqCJH\":[\"Your account username for authentication\"],\"+yPBXI\":[\"Choose file\"],\"+zy2Nq\":[\"Type\"],\"/09cao\":[\"Low Link Security (Level \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Users outside the channel cannot send messages to it\"],\"/4C8U0\":[\"Copy all\"],\"/6BzZF\":[\"Toggle Member List\"],\"/AkXyp\":[\"Confirm?\"],\"/TNOPk\":[\"User is away\"],\"/XQgft\":[\"Discover\"],\"/cF7Rs\":[\"Volume\"],\"/dqduX\":[\"Next page\"],\"/fc3q4\":[\"All Content\"],\"/kISDh\":[\"Enable Notification Sounds\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Play sounds for mentions and messages\"],\"0/0ZGA\":[\"Channel Name Mask\"],\"0D6j7U\":[\"Learn more about custom rules →\"],\"0XsHcR\":[\"Kick User\"],\"0ZpE//\":[\"Sort by Users\"],\"0bEPwz\":[\"Set Away\"],\"0dGkPt\":[\"Expand channel list\"],\"0gS7M5\":[\"Display Name\"],\"0kS+M8\":[\"ExampleNET\"],\"0rgoY7\":[\"Only connect to servers you choose\"],\"0wdd7X\":[\"Join\"],\"0wkVYx\":[\"Private Messages\"],\"111uHX\":[\"Link preview\"],\"196EG4\":[\"Delete Private Chat\"],\"1DSr1i\":[\"Register for an account\"],\"1O/24y\":[\"Toggle Channel List\"],\"1TNIig\":[\"Open\"],\"1VPJJ2\":[\"External Link Warning\"],\"1ZC/dv\":[\"No unread mentions or messages\"],\"1pO1zi\":[\"Server name is required\"],\"1uwfzQ\":[\"View Channel Topic\"],\"268g7c\":[\"Enter display name\"],\"2CEOW6\":[\"Network bound through soju bouncer\"],\"2F9+AZ\":[\"No raw IRC traffic captured yet. Try connecting or sending a message.\"],\"2FOFq1\":[\"Server operators on the network could potentially read your messages\"],\"2FYpfJ\":[\"More\"],\"2HF1Y2\":[[\"inviter\"],\" has invited \",[\"target\"],\" to join \",[\"channel\"]],\"2I70QL\":[\"View user profile information\"],\"2QYdmE\":[\"Users:\"],\"2QpEjG\":[\"left\"],\"2YE223\":[\"Message #\",[\"0\"],\" (Enter for new line, Shift+Enter to send)\"],\"2bimFY\":[\"Use server password\"],\"2iTmdZ\":[\"Local Storage:\"],\"2odkwe\":[\"Strict - More aggressive protection\"],\"2uDhbA\":[\"Enter username to invite\"],\"2ygf/L\":[\"← Back\"],\"2zEgxj\":[\"Search GIFs...\"],\"3RdPhl\":[\"Rename Channel\"],\"3THokf\":[\"Voiced User\"],\"3TSz9S\":[\"Minimize\"],\"3jBDvM\":[\"Channel Display Name\"],\"3ryuFU\":[\"Optional crash reports to improve the app\"],\"3uBF/8\":[\"Close viewer\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Enter account name...\"],\"4/Rr0R\":[\"Invite a user to the current channel\"],\"4EZrJN\":[\"Rules\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Flood Profile (+F)\"],\"4RZQRK\":[\"What are you up to?\"],\"4hfTrB\":[\"Nickname\"],\"4n99LO\":[\"Already in \",[\"0\"]],\"4t6vMV\":[\"Automatically switch to single line for short messages\"],\"4vsHmf\":[\"Time (min)\"],\"4x/Axu\":[\"Your bouncer doesn't have any networks yet. Add one to get started.\"],\"5+INAX\":[\"Highlight messages that mention you\"],\"5R5Pv/\":[\"Oper Name\"],\"678PKt\":[\"Network Name\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Password required to join the channel. Leave empty to remove the key.\"],\"6HhMs3\":[\"Quit Message\"],\"6V3Ea3\":[\"Copied\"],\"6lGV3K\":[\"Show less\"],\"6yFOEi\":[\"Enter oper password...\"],\"7+IHTZ\":[\"No file chosen\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (e.g., spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Send private message\"],\"7U1W7c\":[\"Very Relaxed\"],\"7Y1YQj\":[\"Realname:\"],\"7YHArF\":[\"— open in viewer\"],\"7fjnVl\":[\"Search users...\"],\"7jL88x\":[\"Delete this message? This cannot be undone.\"],\"7nGhhM\":[\"What's on your mind?\"],\"7sEpu1\":[\"Members — \",[\"0\"]],\"7sNhEz\":[\"Username\"],\"8H0Q+x\":[\"Learn more about profiles →\"],\"8Phu0A\":[\"Display when users change their nickname\"],\"8XTG9e\":[\"Enter oper password\"],\"8XsV2J\":[\"Retry sending\"],\"8ZsakT\":[\"Password\"],\"8kR84m\":[\"You are about to open an external link:\"],\"8lCgih\":[\"Remove Rule\"],\"8o3dPc\":[\"Drop files to upload\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"joined\"],\"other\":[\"joined \",[\"joinCount\"],\" times\"]}]],\"9BMLnJ\":[\"Reconnect to server\"],\"9OEgyT\":[\"Add reaction\"],\"9PQ8m2\":[\"G-Line (Global Ban)\"],\"9Qs99X\":[\"Email:\"],\"9QupBP\":[\"Remove pattern\"],\"9W7tl5\":[\"(unchanged)\"],\"9bG48P\":[\"Sending\"],\"9f5f0u\":[\"Questions about privacy? Contact us:\"],\"9iweoP\":[\"Networks on \",[\"0\"]],\"9unqs3\":[\"Away:\"],\"9v3hwv\":[\"No servers found.\"],\"9zb2WA\":[\"Connecting\"],\"A1taO8\":[\"Search\"],\"A2adVi\":[\"Send Typing Notifications\"],\"A9Rhec\":[\"Channel Name\"],\"AWOSPo\":[\"Zoom in\"],\"AXSpEQ\":[\"Oper on Connect\"],\"AdKRCX\":[\"You're connected to <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Account\"],\"AhNP40\":[\"Seek\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Changed nickname\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Cancel reply\"],\"ApSx0O\":[\"Found \",[\"0\"],\" messages matching \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"No results found\"],\"AyNqAB\":[\"Display all server events in chat\"],\"B/QqGw\":[\"Away from keyboard\"],\"B0sB2k\":[\"Plaintext\"],\"B8AaMI\":[\"This field is required\"],\"BA2c49\":[\"Server doesn't support advanced LIST filtering\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" and \",[\"3\"],\" others are typing...\"],\"BGul2A\":[\"You have unsaved changes. Are you sure you want to close without saving?\"],\"BIf9fi\":[\"Your status message\"],\"BOJWfb\":[\"Disconnect from soju bouncer?\"],\"BPm98R\":[\"No server is selected. Pick a server from the sidebar first; invite links are managed per-server.\"],\"BZz3md\":[\"Your personal website\"],\"Bgm/H7\":[\"Allow entering multiple lines of text\"],\"BiQIl1\":[\"Pin this private message conversation\"],\"BlNZZ2\":[\"Click to jump to message\"],\"Bowq3c\":[\"Only operators can change the channel topic\"],\"Btozzp\":[\"This image has expired\"],\"Bycfjm\":[\"Total: \",[\"0\"]],\"C6IBQc\":[\"Copy entire JSON\"],\"C9L9wL\":[\"Data Collection\"],\"CDq4wC\":[\"Moderate User\"],\"CHVRxG\":[\"Message @\",[\"0\"],\" (Shift+Enter for new line)\"],\"CN9zdR\":[\"Oper name and password are required\"],\"CW3sYa\":[\"Add reaction \",[\"emoji\"]],\"CaAkqd\":[\"Show Quits\"],\"CbvaYj\":[\"Ban by Nickname\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Select a channel\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"System\"],\"D28t6+\":[\"joined and quit\"],\"DB8zMK\":[\"Apply\"],\"DBcWHr\":[\"Custom notification sound file\"],\"DTy9Xw\":[\"Media Previews\"],\"Dj4pSr\":[\"Choose a secure password\"],\"Du+zn+\":[\"Searching...\"],\"Du2T2f\":[\"Setting not found\"],\"DwsSVQ\":[\"Apply Filters & Refresh\"],\"E3W/zd\":[\"Default Nickname\"],\"E6nRW7\":[\"Copy URL\"],\"E703RG\":[\"Modes:\"],\"EAeu1Z\":[\"Send Invite\"],\"EFKJQT\":[\"Setting\"],\"EGPQBv\":[\"Custom Flood Rules (+f)\"],\"ELik0r\":[\"View Full Privacy Policy\"],\"EPbeC2\":[\"View or edit the channel topic\"],\"EQCDNT\":[\"Enter oper username...\"],\"EUvulZ\":[\"Found 1 message matching \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Next image\"],\"EdQY6l\":[\"None\"],\"EnqLYU\":[\"Search servers...\"],\"F0OKMc\":[\"Edit Server\"],\"F6Int2\":[\"Enable Highlights\"],\"FDoLyE\":[\"Max Users\"],\"FUU/hZ\":[\"Control how much external media is loaded in chat.\"],\"Fdp03t\":[\"on\"],\"FfPWR0\":[\"Modal\"],\"FjkaiT\":[\"Zoom out\"],\"FlqOE9\":[\"What this means:\"],\"FolHNl\":[\"Manage your account and authentication\"],\"Fp2Dif\":[\"Quit the server\"],\"G5KmCc\":[\"GZ-Line (Global Z-Line)\"],\"GDs0lz\":[\"<0>Risk:0> Sensitive information (messages, private conversations, authentication details) could be exposed to network administrators or attackers positioned between IRC servers.\"],\"GR+2I3\":[\"Add invitation mask (e.g., nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Close popped out server notices\"],\"GdhD7H\":[\"Click again to confirm\"],\"GjRZex\":[\"Disconnect network?\"],\"GlHnXw\":[\"Nick change failed: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Preview:\"],\"GtmO8/\":[\"from\"],\"GtuHUQ\":[\"Rename this channel on the server. All users will see the new name.\"],\"GuGfFX\":[\"Toggle search\"],\"GxkJXS\":[\"Uploading...\"],\"GzbwnK\":[\"Joined the channel\"],\"GzsUDB\":[\"Extended Profile\"],\"H/PnT8\":[\"Insert emoji\"],\"H6Izzl\":[\"Your preferred color code\"],\"H9jIv+\":[\"Show Joins/Parts\"],\"HAKBY9\":[\"Upload Files\"],\"HdE1If\":[\"Channel\"],\"Hk4AW9\":[\"Your preferred display name\"],\"HmHDk7\":[\"Select Member\"],\"HrQzPU\":[\"Channels on \",[\"networkName\"]],\"I2tXQ5\":[\"Message @\",[\"0\"],\" (Enter for new line, Shift+Enter to send)\"],\"I6bw/h\":[\"Ban User\"],\"I92Z+b\":[\"Enable notifications\"],\"I9D72S\":[\"Are you sure you want to delete this message? This action cannot be undone.\"],\"IA+1wo\":[\"Display when users are kicked from channels\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Save Changes\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" and \",[\"2\"],\" are typing...\"],\"IgrLD/\":[\"Pause\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Reply\"],\"IoHMnl\":[\"Maximum value is \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Connecting...\"],\"J5T9NW\":[\"User Information\"],\"J8Y5+z\":[\"Oops! The net split! ⚠️\"],\"JBHkBA\":[\"Left the channel\"],\"JCwL0Q\":[\"Enter reason (optional)\"],\"JFciKP\":[\"Toggle\"],\"JXGkhG\":[\"Change the channel name (operators only)\"],\"JcD7qf\":[\"More actions\"],\"JdkA+c\":[\"Secret (+s)\"],\"Jmu12l\":[\"Server Channels\"],\"JoQY+E\":[\"This removes the network from your soju bouncer. To use it again, you'll need to add it back.\"],\"JvQ++s\":[\"Enable Markdown\"],\"K2jwh/\":[\"No WHOIS data available\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Delete message\"],\"KKBlUU\":[\"Embed\"],\"KM0pLb\":[\"Welcome to the channel!\"],\"KR6W2h\":[\"Unignore User\"],\"KV+Bi1\":[\"Invite-Only (+i)\"],\"KdCtwE\":[\"How many seconds to monitor for flood activity before resetting counters\"],\"Kkezga\":[\"Server Password\"],\"KsiQ/8\":[\"Users must be invited to join the channel\"],\"L+gB/D\":[\"Channel Information\"],\"LC1a7n\":[\"The IRC server has reported that its server-to-server links have a low security level. This means that when your messages are relayed between IRC servers in the network, they may not be properly encrypted or the SSL/TLS certificates may not be validated correctly.\"],\"LEwpeL\":[\"soju bouncer (control)\"],\"LNfLR5\":[\"Show Kicks\"],\"LP+1Z7\":[\"Add Network\"],\"LQb0W/\":[\"Show All Events\"],\"LU7/yA\":[\"Alternative name for display in the UI. May contain spaces, emoji, and special characters. The real channel name (\",[\"channelName\"],\") will still be used for IRC commands.\"],\"LUb9O7\":[\"Valid server port is required\"],\"LV4fT6\":[\"Description (optional, e.g. \\\"Beta testers Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Privacy Policy\"],\"LcuSDR\":[\"Manage your profile information and metadata\"],\"LqLS9B\":[\"Show Nick Changes\"],\"LsDQt2\":[\"Channel Settings\"],\"LtI9AS\":[\"Owner\"],\"LuNhhL\":[\"reacted to this message\"],\"M/AZNG\":[\"URL to your avatar image\"],\"M/WIer\":[\"Send Message\"],\"M8er/5\":[\"Name:\"],\"MHk+7g\":[\"Previous image\"],\"MRorGe\":[\"PM User\"],\"MVbSGP\":[\"Time Window (seconds)\"],\"MkpcsT\":[\"Your messages and settings are stored locally on your device\"],\"MzPdC2\":[\"Server Password (PASS)\"],\"N/hDSy\":[\"Mark as bot - usually 'on' or empty\"],\"N6j2JH\":[\"Edit \",[\"0\"]],\"N7TQbE\":[\"Invite User to \",[\"channelName\"]],\"NCca/o\":[\"Enter default nickname...\"],\"Nqs6B9\":[\"Shows all external media. Any URL may cause a request to an unknown server.\"],\"Nt+9O7\":[\"Use WebSocket instead of raw TCP\"],\"NxIHzc\":[\"Kill User\"],\"O+v/cL\":[\"Browse all channels on the server\"],\"OCGpR4\":[\"(inherit)\"],\"ODwSCk\":[\"Send a GIF\"],\"OGQ5kK\":[\"Configure notification sounds and highlights\"],\"OIPt1Z\":[\"Show or hide the member list sidebar\"],\"OKSNq/\":[\"Very Strict\"],\"ONWvwQ\":[\"Upload\"],\"OVKoQO\":[\"Your account password for authentication\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Bringing IRC to the future\"],\"OhCpra\":[\"Set a topic…\"],\"OkltoQ\":[\"Ban \",[\"username\"],\" by nickname (prevents them from rejoining with the same nick)\"],\"P+t/Te\":[\"No additional data\"],\"P42Wcc\":[\"Safe\"],\"PD38l0\":[\"Channel avatar preview\"],\"PD9mEt\":[\"Type a message...\"],\"PPqfdA\":[\"Open channel configuration settings\"],\"PSCjfZ\":[\"The topic that will be displayed for this channel. All users can see the topic.\"],\"PZCecv\":[\"PDF preview\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 time\"],\"other\":[[\"c\"],\" times\"]}]],\"PguS2C\":[\"Add exception mask (e.g., nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Showing \",[\"displayedChannelsCount\"],\" of \",[\"0\"],\" channels\"],\"PqhVlJ\":[\"Ban User (by Hostmask)\"],\"Q+chwU\":[\"Username:\"],\"Q2QY4/\":[\"Delete this invite\"],\"Q3v9Wc\":[\"Yes, delete\"],\"Q6hhn8\":[\"Preferences\"],\"QF4a34\":[\"Please enter a username\"],\"QGqSZ2\":[\"Color & Formatting\"],\"QJQd1J\":[\"Edit Profile\"],\"QSzGDE\":[\"Idle\"],\"QUlny5\":[\"Welcome to \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Read more\"],\"QuSkCF\":[\"Filter channels...\"],\"QwUrDZ\":[\"changed the topic to: \",[\"topic\"]],\"R0UH07\":[\"Image \",[\"0\"],\" of \",[\"1\"]],\"R7SsBE\":[\"Mute\"],\"R8rf1X\":[\"Click to set topic\"],\"RArB3D\":[\"was kicked from \",[\"channelName\"],\" by \",[\"username\"]],\"RI3cWd\":[\"Discover the world of IRC with ObsidianIRC\"],\"RIfHS5\":[\"Create a new invite link\"],\"RMMaN5\":[\"Moderated (+m)\"],\"RWw9Lg\":[\"Close modal\"],\"RZ2BuZ\":[\"Account registration for \",[\"account\"],\" requires verification: \",[\"message\"]],\"RySp6q\":[\"Hide comments\"],\"S5Togi\":[\"Loading networks from your bouncer…\"],\"SPKQTd\":[\"Nickname is required\"],\"SPVjfj\":[\"Will default to 'no reason' if left empty\"],\"SQKPvQ\":[\"Invite User\"],\"STmlpb\":[\"Back to network list\"],\"SkZcl+\":[\"Choose a predefined flood protection profile. These profiles provide balanced protection settings for different use cases.\"],\"Slr+3C\":[\"Min Users\"],\"Spnlre\":[\"You invited \",[\"target\"],\" to join \",[\"channel\"]],\"T/ckN5\":[\"Open in viewer\"],\"T91vKp\":[\"Play\"],\"TV2Wdu\":[\"Learn how we handle your data and protect your privacy.\"],\"TgFpwD\":[\"Applying...\"],\"TkzSFB\":[\"No Changes\"],\"TtserG\":[\"Enter real name\"],\"Ttz9J1\":[\"Enter password...\"],\"Tz0i8g\":[\"Settings\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"React\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"You haven't created any invite links yet. Use the form above to mint your first one.\"],\"UGT5vp\":[\"Save Settings\"],\"UV5hLB\":[\"No bans found\"],\"Uaj3Nd\":[\"Status Messages\"],\"Ue3uny\":[\"Default (no profile)\"],\"UkARhe\":[\"Normal - Standard protection\"],\"Umn7Cj\":[\"No comments yet. Be the first!\"],\"UtUIRh\":[[\"0\"],\" older messages\"],\"UwzP+U\":[\"Secure Connection\"],\"V0/A4O\":[\"Channel Owner\"],\"V0zZWc\":[\"This will also close the bound network below.\"],\"V4qgxE\":[\"Created Before (min ago)\"],\"V8yTm6\":[\"Clear search\"],\"VJMMyz\":[\"ObsidianIRC - Bringing IRC to the future\"],\"VJScHU\":[\"Reason\"],\"VLsmVV\":[\"Mute notifications\"],\"VbyRUy\":[\"Comments\"],\"Vmx0mQ\":[\"Set by:\"],\"VqnIZz\":[\"View our privacy policy and data practices\"],\"VrMygG\":[\"Minimum length is \",[\"0\"]],\"VrnTui\":[\"Your pronouns, shown in your profile\"],\"W8E3qn\":[\"Authenticated Account\"],\"WAakm9\":[\"Delete Channel\"],\"WFxTHC\":[\"Add ban mask (e.g., nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Server host is required\"],\"WRYdXW\":[\"Audio position\"],\"WUOH5B\":[\"Ignore User\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Show 1 more item\"],\"other\":[\"Show \",[\"1\"],\" more items\"]}]],\"WYxRzo\":[\"Create and manage your invite links\"],\"Wd38W1\":[\"Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list.\"],\"Weq9zb\":[\"General\"],\"Wfj7Sk\":[\"Mute or unmute notification sounds\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"User Profile\"],\"X6S3lt\":[\"Search settings, channels, servers...\"],\"XEHan5\":[\"Continue Anyway\"],\"XI1+wb\":[\"Invalid format\"],\"XIXeuC\":[\"Message @\",[\"0\"]],\"XMS+k4\":[\"Start Private Message\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Unpin Private Chat\"],\"Xm/s+u\":[\"Display\"],\"Xp2n93\":[\"Shows media from your server's trusted file host. No requests are made to external services.\"],\"XvjC4F\":[\"Saving...\"],\"Y/qryO\":[\"No users found matching your search\"],\"YAqRpI\":[\"Account registration successful for \",[\"account\"],\": \",[\"message\"]],\"YEfzvP\":[\"Protected Topic (+t)\"],\"YQOn6a\":[\"Collapse member list\"],\"YRCoE9\":[\"Channel Operator\"],\"YURQaF\":[\"View Profile\"],\"YdBSvr\":[\"Control media display and external content\"],\"Yj6U3V\":[\"No Central Server:\"],\"YjvpGx\":[\"Pronouns\"],\"YqH4l4\":[\"No key\"],\"YyUPpV\":[\"Account:\"],\"ZJSWfw\":[\"Message shown when you disconnect from the server\"],\"ZR1dJ4\":[\"Invitations\"],\"ZdWg0V\":[\"Open in browser\"],\"ZhRBbl\":[\"Search messages…\"],\"Zmcu3y\":[\"Advanced Filters\"],\"a0bHay\":[\"Disconnect <0>\",[\"0\"],\"0>?\"],\"a2/8e5\":[\"Topic Set After (min ago)\"],\"aHKcKc\":[\"Previous page\"],\"aJTbXX\":[\"Oper Password\"],\"aQryQv\":[\"Pattern already exists\"],\"aW9pLN\":[\"Maximum number of users allowed in the channel. Leave empty for no limit.\"],\"ah4fmZ\":[\"Also shows previews from YouTube, Vimeo, SoundCloud, and similar known services.\"],\"aifXak\":[\"No media in this channel\"],\"ap2zBz\":[\"Relaxed\"],\"az8lvo\":[\"Off\"],\"azXSNo\":[\"Expand member list\"],\"azdliB\":[\"Login to an account\"],\"b26wlF\":[\"she/her\"],\"bD/+Ei\":[\"Strict\"],\"bQ6BJn\":[\"Configure detailed flood protection rules. Each rule specifies what type of activity to monitor and what action to take when thresholds are exceeded.\"],\"beV7+y\":[\"The user will receive an invitation to join \",[\"channelName\"],\".\"],\"bk84cH\":[\"Away Message\"],\"bkHdLj\":[\"Add IRC Server\"],\"bmQLn5\":[\"Add Rule\"],\"bv4cFj\":[\"Transport\"],\"bwRvnp\":[\"Action\"],\"c8+EVZ\":[\"Verified account\"],\"cGYUlD\":[\"No media previews are loaded.\"],\"cLF98o\":[\"Show comments (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"No users available\"],\"cSgpoS\":[\"Pin Private Chat\"],\"cXeEKu\":[\"This will also close the \",[\"0\"],\" bound networks below.\"],\"cde3ce\":[\"Message <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Copy formatted output\"],\"cl/A5J\":[\"Welcome to \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Delete\"],\"coPLXT\":[\"We don't store your IRC communications on our servers\"],\"crYH/6\":[\"SoundCloud player\"],\"cv5DQb\":[\"no host set\"],\"d3sis4\":[\"Add Server\"],\"d9aN5k\":[\"Remove \",[\"username\"],\" from the channel\"],\"dEgA5A\":[\"Cancel\"],\"dGi1We\":[\"Unpin this private message conversation\"],\"dJVuyC\":[\"left \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"to\"],\"dXqxlh\":[\"<0>⚠️ Security Risk!0> This connection may be vulnerable to interception or man-in-the-middle attacks.\"],\"da9Q/R\":[\"Changed channel modes\"],\"dhJN3N\":[\"Show comments\"],\"dj2xTE\":[\"Dismiss notification\"],\"dpCzmC\":[\"Flood Protection Settings\"],\"e9dQpT\":[\"Do you want to open this link in a new tab?\"],\"ePK91l\":[\"Edit\"],\"eYBDuB\":[\"Upload an image or provide a URL with optional \",[\"size\"],\" substitution for dynamic sizing\"],\"edBbee\":[\"Ban \",[\"username\"],\" by hostmask (prevents them from rejoining from the same IP/host)\"],\"ekfzWq\":[\"User Settings\"],\"elPDWs\":[\"Customize your IRC client experience\"],\"eu2osY\":[\"<0>💡 Recommendation:0> Only proceed if you trust this server and understand the risks. Avoid sharing sensitive information or passwords over this connection.\"],\"euEhbr\":[\"Click to join \",[\"channel\"]],\"ez3vLd\":[\"Enable Multiline Input\"],\"f0J5Ki\":[\"Server-to-server communication may use unencrypted connections\"],\"f9BHJk\":[\"Warn User\"],\"fDOLLd\":[\"No channels found.\"],\"ffzDkB\":[\"Anonymous Analytics:\"],\"fq1GF9\":[\"Display when users disconnect from server\"],\"gCldcN\":[\"Change accent color\"],\"gEF57C\":[\"This server only supports one connection type\"],\"gJuLUI\":[\"Ignore List\"],\"gNzMrk\":[\"Current avatar\"],\"gjPWyO\":[\"Enter nickname...\"],\"gz6UQ3\":[\"Maximize\"],\"h6/IMX\":[\"Add your first network\"],\"h6razj\":[\"Exclude Channel Name Mask\"],\"hG6jnw\":[\"No topic set\"],\"hG89Ed\":[\"Image\"],\"hYgDIe\":[\"Create\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"e.g., 100:1440\"],\"he3ygx\":[\"Copy\"],\"hehnjM\":[\"Amount\"],\"hzdLuQ\":[\"Only users with voice or higher can speak\"],\"i0qMbr\":[\"Home\"],\"iDNBZe\":[\"Notifications\"],\"iH8pgl\":[\"Back\"],\"iL9SZg\":[\"Ban User (by Nickname)\"],\"iNt+3c\":[\"Back to image\"],\"iQvi+a\":[\"Don't warn me about low link security for this server\"],\"iSLIjg\":[\"Connect\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Server Host\"],\"idD8Ev\":[\"Saved\"],\"iivqkW\":[\"Signed On\"],\"ij+Elv\":[\"Image preview\"],\"ilIWp7\":[\"Toggle Notifications\"],\"iuaqvB\":[\"Use * for wildcards. Examples: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Ban by Hostmask\"],\"jA4uoI\":[\"Topic:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Reason (optional)\"],\"jUV7CU\":[\"Upload Avatar\"],\"jW5Uwh\":[\"Control how much external media is loaded. Off / Safe / Trusted Sources / All Content.\"],\"jXzms5\":[\"Attachment options\"],\"jZlrte\":[\"Color\"],\"jfC/xh\":[\"Contact\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Load older messages\"],\"k3ID0F\":[\"Filter members…\"],\"k65gsE\":[\"Deep dive\"],\"k7Zgob\":[\"Cancel Connection\"],\"kAVx5h\":[\"No invitations found\"],\"kCLEPU\":[\"Connected To\"],\"kF5LKb\":[\"Ignored patterns:\"],\"kGeOx/\":[\"Join \",[\"0\"]],\"kITKr8\":[\"Loading channel modes...\"],\"kPpPsw\":[\"You are an IRC Operator\"],\"kWJmRL\":[\"You\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Copy JSON\"],\"krViRy\":[\"Click to copy as JSON\"],\"ks71ra\":[\"Exceptions\"],\"kw4lRv\":[\"Channel Half Operator\"],\"kxgIRq\":[\"Select or add a channel to get started.\"],\"ky6dWe\":[\"Avatar preview\"],\"l+GxCv\":[\"Loading channels...\"],\"l+IUVW\":[\"Account verification successful for \",[\"account\"],\": \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"reconnected\"],\"other\":[\"reconnected \",[\"reconnectCount\"],\" times\"]}]],\"l1l8sj\":[[\"0\"],\"d ago\"],\"l5NhnV\":[\"#channel (optional)\"],\"l5jmzx\":[[\"0\"],\" and \",[\"1\"],\" are typing...\"],\"lCF0wC\":[\"Refresh\"],\"lHy8N5\":[\"Loading more channels...\"],\"lasgrr\":[\"used\"],\"lbpf14\":[\"Join \",[\"value\"]],\"lfFsZ4\":[\"Channels\"],\"lkNdiH\":[\"Account Name\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Upload Image\"],\"loQxaJ\":[\"I'm Back\"],\"lvfaxv\":[\"HOME\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Add\"],\"m8flAk\":[\"Preview (not yet uploaded)\"],\"mEPxTp\":[\"<0>⚠️ Be careful!0> Only open links from trusted sources. Malicious links can compromise your security or privacy.\"],\"mHGdhG\":[\"Server Information\"],\"mHS8lb\":[\"Message #\",[\"0\"]],\"mMYBD9\":[\"Wide - Broader protection scope\"],\"mTGsPd\":[\"Channel Topic\"],\"mU8j6O\":[\"No External Messages (+n)\"],\"mZp8FL\":[\"Auto Fallback to Single Line\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"Comments (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"User is authenticated\"],\"mwtcGl\":[\"Close comments\"],\"myL0MR\":[\"Delete this network?\"],\"mzI/c+\":[\"Download\"],\"n3fGRk\":[\"set by \",[\"0\"]],\"n5+j9l\":[\"e.g. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Relaxed - Less aggressive protection\"],\"nNflMD\":[\"Leave channel\"],\"nPXkBi\":[\"Loading WHOIS data...\"],\"nQnxxF\":[\"Message #\",[\"0\"],\" (Shift+Enter for new line)\"],\"nWMRxa\":[\"Unpin\"],\"nkC032\":[\"No flood profile\"],\"o69z4d\":[\"Send a warning message to \",[\"username\"]],\"o9ylQi\":[\"Search for GIFs to get started\"],\"oFGkER\":[\"Server Notices\"],\"oOi11l\":[\"Scroll to bottom\"],\"oPYIL5\":[\"network\"],\"oQEzQR\":[\"New DM\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Man-in-the-middle attacks on server links are possible\"],\"oeqmmJ\":[\"Trusted Sources\"],\"optX0N\":[[\"0\"],\"h ago\"],\"ovBPCi\":[\"Default\"],\"p0Z69r\":[\"Pattern cannot be empty\"],\"p1KgtK\":[\"Failed to load audio\"],\"p59pEv\":[\"Additional Details\"],\"p7sRI6\":[\"Let others know when you are typing\"],\"pBm1od\":[\"Secret channel\"],\"pNmiXx\":[\"Your default nickname for all servers\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Account Password\"],\"peNE68\":[\"Permanent\"],\"plhHQt\":[\"No data\"],\"pm6+q5\":[\"Security Warning\"],\"pn5qSs\":[\"Additional Information\"],\"q0cR4S\":[\"are now known as **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Channel won't appear in LIST or NAMES commands\"],\"qLpTm/\":[\"Remove reaction \",[\"emoji\"]],\"qVkGWK\":[\"Pin\"],\"qY8wNa\":[\"Homepage\"],\"qb0xJ7\":[\"Use wildcards: * matches any sequence, ? matches any single character. Examples: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Channel Key (+k)\"],\"qtoOYG\":[\"No limit\"],\"r1W2AS\":[\"Filehost image\"],\"rIPR2O\":[\"Topic Set Before (min ago)\"],\"rMMSYo\":[\"Maximum length is \",[\"0\"]],\"rWtzQe\":[\"The network split and rejoined. ✅\"],\"rYG2u6\":[\"Please wait...\"],\"rdUucN\":[\"Preview\"],\"rjGI/Q\":[\"Privacy\"],\"rk8iDX\":[\"Loading GIFs...\"],\"rn6SBY\":[\"Unmute\"],\"s/UKqq\":[\"Was kicked from the channel\"],\"s7oqXR\":[\"Select a Network\"],\"s8cATI\":[\"joined \",[\"channelName\"]],\"sCO9ue\":[\"The connection to <0>\",[\"serverName\"],\"0> has the following security concerns:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"is now known as **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" has invited you to join \",[\"channel\"]],\"sUBSbK\":[\"No upstream networks yet.\"],\"sby+1/\":[\"Click to copy\"],\"sfN25C\":[\"Your real or full name\"],\"sliuzR\":[\"Open Link\"],\"sqrO9R\":[\"Custom Mentions\"],\"sr6RdJ\":[\"Multiline on Shift+Enter\"],\"swrCpB\":[\"Channel has been renamed from \",[\"oldName\"],\" to \",[\"newName\"],\" by \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Advanced\"],\"t/YqKh\":[\"Remove\"],\"t47eHD\":[\"Your unique identifier on this server\"],\"tAkAh0\":[\"URL with optional \",[\"size\"],\" substitution for dynamic sizing. Example: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Show or hide the channel list sidebar\"],\"tfDRzk\":[\"Save\"],\"tiBsJk\":[\"left \",[\"channelName\"]],\"tt4/UD\":[\"quit (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Nickname {nick} already in use, retrying with {newNick}\"],\"u0a8B4\":[\"Authenticate as an IRC Operator for administrative access\"],\"u0rWFU\":[\"Created After (min ago)\"],\"u72w3t\":[\"Users and patterns to ignore\"],\"u7jc2L\":[\"quit\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Save failed: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC Servers:\"],\"ukyW4o\":[\"Your invite links\"],\"usSSr/\":[\"Zoom level\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Use Shift+Enter for new lines (Enter sends)\"],\"vERlcd\":[\"Profile\"],\"vK0RL8\":[\"No topic\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Language\"],\"vaHYxN\":[\"Real Name\"],\"vhjbKr\":[\"Away\"],\"w/nogd\":[[\"0\"],\" network\",[\"1\"],\" — pick one to join\"],\"w4NYox\":[[\"title\"],\" Client\"],\"w8xQRx\":[\"Invalid value\"],\"wFjjxZ\":[\"was kicked from \",[\"channelName\"],\" by \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"No ban exceptions found\"],\"wPrGnM\":[\"Channel Admin\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Display when users join or leave channels\"],\"whqZ9r\":[\"Additional words or phrases to highlight\"],\"wm7RV4\":[\"Notification Sound\"],\"wz/Yoq\":[\"Your messages could be intercepted when relayed between servers\"],\"x3+y8b\":[\"This many people registered through this link\"],\"xCJdfg\":[\"Clear\"],\"xOTzt5\":[\"just now\"],\"xUHRTR\":[\"Automatically authenticate as operator on connect\"],\"xWHwwQ\":[\"Bans\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks.\"],\"xceQrO\":[\"Only secure websockets are supported\"],\"xdtXa+\":[\"channel-name\"],\"xfXC7q\":[\"Text Channels\"],\"xlCYOE\":[\"Getting more messages...\"],\"xlhswE\":[\"Minimum value is \",[\"0\"]],\"xq97Ci\":[\"Add a word or phrase...\"],\"xuRqRq\":[\"Client Limit (+l)\"],\"xwF+7J\":[[\"0\"],\" is typing...\"],\"y1eoq1\":[\"Copy link\"],\"yJztBY\":[\"Delete network\"],\"yNeucF\":[\"This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available.\"],\"yPlrca\":[\"Channel Avatar\"],\"yQE2r9\":[\"Loading\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Oper Username\"],\"yYOzWD\":[\"logs\"],\"yfx9Re\":[\"IRC operator password\"],\"ygCKqB\":[\"Stop\"],\"ymDxJx\":[\"IRC operator username\"],\"yrpRsQ\":[\"Sort by Name\"],\"yz7wBu\":[\"Close\"],\"zJw+jA\":[\"sets mode: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"m ago\"],\"zebeLu\":[\"Enter oper username\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/en/messages.po b/src/locales/en/messages.po
index 979bf3a6..d4ff4b40 100644
--- a/src/locales/en/messages.po
+++ b/src/locales/en/messages.po
@@ -21,6 +21,16 @@ msgstr "ObsidianIRC - Bringing IRC to the future"
msgid "— open in viewer"
msgstr "— open in viewer"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(inherit)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(unchanged)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -45,6 +55,12 @@ msgstr "{0} and {1} are typing..."
msgid "{0} is typing..."
msgstr "{0} is typing..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} network{1} — pick one to join"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -204,6 +220,12 @@ msgstr "Add invitation mask (e.g., nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "Add IRC Server"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Add Network"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -223,6 +245,10 @@ msgstr "Add Rule"
msgid "Add Server"
msgstr "Add Server"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Add your first network"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Additional Details"
@@ -376,6 +402,10 @@ msgstr "Back"
msgid "Back to image"
msgstr "Back to image"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Back to network list"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
@@ -423,6 +453,10 @@ msgstr "Browse all channels on the server"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -442,6 +476,11 @@ msgstr "Cancel Connection"
msgid "Cancel reply"
msgstr "Cancel reply"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Change accent color"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Change the channel name (operators only)"
@@ -605,6 +644,8 @@ msgstr "Client Limit (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -668,6 +709,7 @@ msgid "Confirm?"
msgstr "Confirm?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Connect"
@@ -813,6 +855,10 @@ msgstr "Delete Channel"
msgid "Delete message"
msgstr "Delete message"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Delete network"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Delete Private Chat"
@@ -825,15 +871,36 @@ msgstr "Delete this invite"
msgid "Delete this message? This cannot be undone."
msgstr "Delete this message? This cannot be undone."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Delete this network?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
msgstr "Description (optional, e.g. \"Beta testers Q3\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Disconnect"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "Disconnect <0>{0}0>?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Disconnect from soju bouncer?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Disconnect network?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Discover"
@@ -892,18 +959,29 @@ msgstr "Download"
msgid "Drop files to upload"
msgstr "Drop files to upload"
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "e.g. <0>wss://host:port/socket0>"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "e.g., 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Edit"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "Edit {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Edit Profile"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1123,6 +1201,7 @@ msgstr "HOME"
msgid "Homepage"
msgstr "Homepage"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Host"
@@ -1346,6 +1425,10 @@ msgstr "Left the channel"
msgid "Let others know when you are typing"
msgstr "Let others know when you are typing"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Link preview"
@@ -1374,6 +1457,10 @@ msgstr "Loading GIFs..."
msgid "Loading more channels..."
msgstr "Loading more channels..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Loading networks from your bouncer…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "Loading WHOIS data..."
@@ -1564,10 +1651,21 @@ msgstr "Name:"
msgid "network"
msgstr "network"
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Network bound through soju bouncer"
+
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Network Name"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Networks on {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "New DM"
@@ -1590,6 +1688,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1649,6 +1748,10 @@ msgstr "No file chosen"
msgid "No flood profile"
msgstr "No flood profile"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "no host set"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "No invitations found"
@@ -1697,6 +1800,10 @@ msgstr "No topic set"
msgid "No unread mentions or messages"
msgstr "No unread mentions or messages"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "No upstream networks yet."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "No users available"
@@ -1783,6 +1890,10 @@ msgstr "Oops! The net split! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Open"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Open channel configuration settings"
@@ -1886,6 +1997,10 @@ msgstr "Pin Private Chat"
msgid "Pin this private message conversation"
msgstr "Pin this private message conversation"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Plaintext"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1914,6 +2029,7 @@ msgid "PM User"
msgstr "PM User"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Port"
@@ -2005,6 +2121,7 @@ msgstr "reacted to this message"
msgid "Read more"
msgstr "Read more"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2023,6 +2140,7 @@ msgstr "Reason"
msgid "Reason (optional)"
msgstr "Reason (optional)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Reconnect to server"
@@ -2094,6 +2212,7 @@ msgstr "Rules"
msgid "Safe"
msgstr "Safe"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2186,6 +2305,10 @@ msgstr "Seek"
msgid "Select a channel"
msgstr "Select a channel"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Select a Network"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Select Member"
@@ -2275,6 +2398,10 @@ msgstr "Server operators on the network could potentially read your messages"
msgid "Server Password"
msgstr "Server Password"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Server Password (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Server-to-server communication may use unencrypted connections"
@@ -2379,6 +2506,12 @@ msgstr "Signed On"
msgid "Software:"
msgstr "Software:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "soju bouncer (control)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Sort by Name"
@@ -2458,6 +2591,10 @@ msgstr "This image has expired"
msgid "This many people registered through this link"
msgstr "This many people registered through this link"
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
msgstr "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2470,6 +2607,15 @@ msgstr "This server doesn't support invite links (the<0>obby.world/invitation0
msgid "This server only supports one connection type"
msgstr "This server only supports one connection type"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "This will also close the {0} bound networks below."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "This will also close the bound network below."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Time (min)"
@@ -2478,6 +2624,10 @@ msgstr "Time (min)"
msgid "Time Window (seconds)"
msgstr "Time Window (seconds)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2526,6 +2676,10 @@ msgstr "Topic:"
msgid "Total: {0}"
msgstr "Total: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Transport"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Trusted Sources"
@@ -2640,6 +2794,7 @@ msgstr "User Profile"
msgid "User Settings"
msgstr "User Settings"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2787,6 +2942,10 @@ msgstr "Wide - Broader protection scope"
msgid "Will default to 'no reason' if left empty"
msgstr "Will default to 'no reason' if left empty"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Yes, delete"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2813,6 +2972,11 @@ msgstr "You haven't created any invite links yet. Use the form above to mint you
msgid "You invited {target} to join {channel}"
msgstr "You invited {target} to join {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "You're connected to <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Your account password for authentication"
@@ -2821,6 +2985,10 @@ msgstr "Your account password for authentication"
msgid "Your account username for authentication"
msgstr "Your account username for authentication"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Your bouncer doesn't have any networks yet. Add one to get started."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Your default nickname for all servers"
diff --git a/src/locales/es/messages.mjs b/src/locales/es/messages.mjs
index b1474fac..f5d5ad43 100644
--- a/src/locales/es/messages.mjs
+++ b/src/locales/es/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Formato de patrón inválido. Usa el formato nick!user@host (se permiten comodines *)\"],\"+6NQQA\":[\"Canal de soporte general\"],\"+6NyRG\":[\"Cliente\"],\"+K0AvT\":[\"Desconectar\"],\"+cyFdH\":[\"Mensaje predeterminado al marcarse como ausente\"],\"+mVPqU\":[\"Renderizar formato Markdown en mensajes\"],\"+vqCJH\":[\"Tu nombre de usuario de cuenta para autenticación\"],\"+yPBXI\":[\"Elegir archivo\"],\"+zy2Nq\":[\"Tipo\"],\"/09cao\":[\"Baja seguridad de enlace (Nivel \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Los usuarios fuera del canal no pueden enviar mensajes a él\"],\"/4C8U0\":[\"Copiar todo\"],\"/6BzZF\":[\"Alternar lista de miembros\"],\"/AkXyp\":[\"¿Confirmar?\"],\"/TNOPk\":[\"El usuario está ausente\"],\"/XQgft\":[\"Explorar\"],\"/cF7Rs\":[\"Volumen\"],\"/dqduX\":[\"Página siguiente\"],\"/fc3q4\":[\"Todo el contenido\"],\"/kISDh\":[\"Activar sonidos de notificación\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Reproducir sonidos para menciones y mensajes\"],\"0/0ZGA\":[\"Máscara de nombre de canal\"],\"0D6j7U\":[\"Más información sobre reglas personalizadas →\"],\"0XsHcR\":[\"Expulsar usuario\"],\"0ZpE//\":[\"Ordenar por usuarios\"],\"0bEPwz\":[\"Marcar como ausente\"],\"0dGkPt\":[\"Expandir lista de canales\"],\"0gS7M5\":[\"Nombre a mostrar\"],\"0kS+M8\":[\"EjemploRED\"],\"0rgoY7\":[\"Solo te conectas a los servidores que eliges\"],\"0wdd7X\":[\"Unirse\"],\"0wkVYx\":[\"Mensajes privados\"],\"111uHX\":[\"Vista previa del enlace\"],\"196EG4\":[\"Eliminar chat privado\"],\"1DSr1i\":[\"Registrar una cuenta\"],\"1O/24y\":[\"Alternar lista de canales\"],\"1VPJJ2\":[\"Advertencia de enlace externo\"],\"1ZC/dv\":[\"Sin menciones ni mensajes sin leer\"],\"1pO1zi\":[\"El nombre del servidor es obligatorio\"],\"1uwfzQ\":[\"Ver tema del canal\"],\"268g7c\":[\"Ingresa el nombre a mostrar\"],\"2F9+AZ\":[\"Aún no se ha capturado tráfico IRC sin procesar. Prueba a conectarte o enviar un mensaje.\"],\"2FOFq1\":[\"Los operadores del servidor en la red podrían leer tus mensajes\"],\"2FYpfJ\":[\"Más\"],\"2HF1Y2\":[[\"inviter\"],\" ha invitado a \",[\"target\"],\" a unirse a \",[\"channel\"]],\"2I70QL\":[\"Ver información del perfil de usuario\"],\"2QYdmE\":[\"Usuarios:\"],\"2QpEjG\":[\"salió\"],\"2YE223\":[\"Mensaje en #\",[\"0\"],\" (Intro para nueva línea, Mayús+Intro para enviar)\"],\"2bimFY\":[\"Usar contraseña del servidor\"],\"2iTmdZ\":[\"Almacenamiento local:\"],\"2odkwe\":[\"Estricto – Protección más agresiva\"],\"2uDhbA\":[\"Ingresa el nombre de usuario a invitar\"],\"2ygf/L\":[\"← Atrás\"],\"2zEgxj\":[\"Buscar GIFs...\"],\"3RdPhl\":[\"Renombrar canal\"],\"3THokf\":[\"Usuario con voz\"],\"3TSz9S\":[\"Minimizar\"],\"3jBDvM\":[\"Nombre a mostrar del canal\"],\"3ryuFU\":[\"Informes de errores opcionales para mejorar la app\"],\"3uBF/8\":[\"Cerrar visor\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Ingresa nombre de cuenta...\"],\"4/Rr0R\":[\"Invitar a un usuario al canal actual\"],\"4EZrJN\":[\"Reglas\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Perfil de flood (+F)\"],\"4RZQRK\":[\"¿Qué estás haciendo?\"],\"4hfTrB\":[\"Apodo\"],\"4n99LO\":[\"Ya en \",[\"0\"]],\"4t6vMV\":[\"Cambiar automáticamente a línea única para mensajes cortos\"],\"4vsHmf\":[\"Tiempo (min)\"],\"5+INAX\":[\"Resaltar mensajes que te mencionan\"],\"5R5Pv/\":[\"Nombre de oper\"],\"678PKt\":[\"Nombre de red\"],\"6Aih4U\":[\"Desconectado\"],\"6CO3WE\":[\"Contraseña requerida para unirse al canal. Deja vacío para eliminar la clave.\"],\"6HhMs3\":[\"Mensaje de salida\"],\"6V3Ea3\":[\"Copiado\"],\"6lGV3K\":[\"Mostrar menos\"],\"6yFOEi\":[\"Ingresa contraseña de oper...\"],\"7+IHTZ\":[\"Ningún archivo elegido\"],\"73hrRi\":[\"nick!user@host (ej., spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Enviar mensaje privado\"],\"7U1W7c\":[\"Muy relajado\"],\"7Y1YQj\":[\"Nombre real:\"],\"7YHArF\":[\"— abrir en visor\"],\"7fjnVl\":[\"Buscar usuarios...\"],\"7jL88x\":[\"¿Eliminar este mensaje? Esta acción no se puede deshacer.\"],\"7nGhhM\":[\"¿Qué piensas?\"],\"7sEpu1\":[\"Miembros — \",[\"0\"]],\"7sNhEz\":[\"Nombre de usuario\"],\"8H0Q+x\":[\"Más información sobre perfiles →\"],\"8Phu0A\":[\"Mostrar cuando los usuarios cambian su apodo\"],\"8XTG9e\":[\"Ingresa la contraseña de oper\"],\"8XsV2J\":[\"Reintentar envío\"],\"8ZsakT\":[\"Contraseña\"],\"8kR84m\":[\"Estás a punto de abrir un enlace externo:\"],\"8lCgih\":[\"Eliminar regla\"],\"8o3dPc\":[\"Suelta archivos para subirlos\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"se unió\"],\"other\":[\"se unió \",[\"joinCount\"],\" veces\"]}]],\"9BMLnJ\":[\"Reconectar al servidor\"],\"9OEgyT\":[\"Agregar reacción\"],\"9PQ8m2\":[\"G-Line (ban global)\"],\"9Qs99X\":[\"Correo electrónico:\"],\"9QupBP\":[\"Eliminar patrón\"],\"9bG48P\":[\"Enviando\"],\"9f5f0u\":[\"¿Preguntas sobre privacidad? Contáctanos:\"],\"9unqs3\":[\"Ausente:\"],\"9v3hwv\":[\"No se encontraron servidores.\"],\"9zb2WA\":[\"Conectando\"],\"A1taO8\":[\"Buscar\"],\"A2adVi\":[\"Enviar notificaciones de escritura\"],\"A9Rhec\":[\"Nombre del canal\"],\"AWOSPo\":[\"Acercar\"],\"AXSpEQ\":[\"Oper al conectar\"],\"AeXO77\":[\"Cuenta\"],\"AhNP40\":[\"Buscar posición\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Apodo cambiado\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Cancelar respuesta\"],\"ApSx0O\":[\"Se encontraron \",[\"0\"],\" mensajes que coinciden con \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"No se encontraron resultados\"],\"AyNqAB\":[\"Mostrar todos los eventos del servidor en el chat\"],\"B/QqGw\":[\"Alejado del teclado\"],\"B8AaMI\":[\"Este campo es obligatorio\"],\"BA2c49\":[\"El servidor no admite filtrado avanzado de LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" y \",[\"3\"],\" más están escribiendo...\"],\"BGul2A\":[\"Tienes cambios sin guardar. ¿Seguro que deseas cerrar sin guardar?\"],\"BIf9fi\":[\"Tu mensaje de estado\"],\"BPm98R\":[\"No hay ningún servidor seleccionado. Elige primero un servidor en la barra lateral; los enlaces de invitación se gestionan por servidor.\"],\"BZz3md\":[\"Tu sitio web personal\"],\"Bgm/H7\":[\"Permitir ingresar múltiples líneas de texto\"],\"BiQIl1\":[\"Fijar esta conversación de mensaje privado\"],\"BlNZZ2\":[\"Haz clic para ir al mensaje\"],\"Bowq3c\":[\"Solo los operadores pueden cambiar el tema del canal\"],\"Btozzp\":[\"Esta imagen ha expirado\"],\"Bycfjm\":[\"Total: \",[\"0\"]],\"C6IBQc\":[\"Copiar JSON completo\"],\"C9L9wL\":[\"Recopilación de datos\"],\"CDq4wC\":[\"Moderar usuario\"],\"CHVRxG\":[\"Mensaje a @\",[\"0\"],\" (Mayús+Intro para nueva línea)\"],\"CN9zdR\":[\"El nombre y la contraseña de oper son obligatorios\"],\"CW3sYa\":[\"Agregar reacción \",[\"emoji\"]],\"CaAkqd\":[\"Mostrar desconexiones\"],\"CbvaYj\":[\"Banear por apodo\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Selecciona un canal\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"Sistema\"],\"D28t6+\":[\"se unió y salió\"],\"DB8zMK\":[\"Aplicar\"],\"DBcWHr\":[\"Archivo de sonido de notificación personalizado\"],\"DTy9Xw\":[\"Vistas previas de medios\"],\"Dj4pSr\":[\"Elige una contraseña segura\"],\"Du+zn+\":[\"Buscando...\"],\"Du2T2f\":[\"Ajuste no encontrado\"],\"DwsSVQ\":[\"Aplicar filtros y actualizar\"],\"E3W/zd\":[\"Apodo predeterminado\"],\"E6nRW7\":[\"Copiar URL\"],\"E703RG\":[\"Modos:\"],\"EAeu1Z\":[\"Enviar invitación\"],\"EFKJQT\":[\"Ajuste\"],\"EGPQBv\":[\"Reglas de flood personalizadas (+f)\"],\"ELik0r\":[\"Ver política de privacidad completa\"],\"EPbeC2\":[\"Ver o editar el tema del canal\"],\"EQCDNT\":[\"Ingresa nombre de usuario oper...\"],\"EUvulZ\":[\"Se encontró 1 mensaje que coincide con \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Imagen siguiente\"],\"EdQY6l\":[\"Ninguno\"],\"EnqLYU\":[\"Buscar servidores...\"],\"F0OKMc\":[\"Editar servidor\"],\"F6Int2\":[\"Activar resaltados\"],\"FDoLyE\":[\"Máximo de usuarios\"],\"FUU/hZ\":[\"Controla cuántos medios externos se cargan en el chat.\"],\"Fdp03t\":[\"activado\"],\"FfPWR0\":[\"Modal\"],\"FjkaiT\":[\"Alejar\"],\"FlqOE9\":[\"Qué significa esto:\"],\"FolHNl\":[\"Administra tu cuenta y autenticación\"],\"Fp2Dif\":[\"Salió del servidor\"],\"G5KmCc\":[\"GZ-Line (Z-Line global)\"],\"GDs0lz\":[\"<0>Riesgo:0> La información sensible (mensajes, conversaciones privadas, datos de autenticación) podría quedar expuesta a administradores de red o atacantes situados entre los servidores IRC.\"],\"GR+2I3\":[\"Agregar máscara de invitación (p. ej., nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Cerrar avisos del servidor desplegados\"],\"GdhD7H\":[\"Haz clic de nuevo para confirmar\"],\"GlHnXw\":[\"Cambio de apodo fallido: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Vista previa:\"],\"GtmO8/\":[\"de\"],\"GtuHUQ\":[\"Renombrar este canal en el servidor. Todos los usuarios verán el nuevo nombre.\"],\"GuGfFX\":[\"Alternar búsqueda\"],\"GxkJXS\":[\"Subiendo...\"],\"GzbwnK\":[\"Se unió al canal\"],\"GzsUDB\":[\"Perfil extendido\"],\"H/PnT8\":[\"Insertar emoji\"],\"H6Izzl\":[\"Tu código de color preferido\"],\"H9jIv+\":[\"Mostrar entradas/salidas\"],\"HAKBY9\":[\"Subir archivos\"],\"HdE1If\":[\"Canal\"],\"Hk4AW9\":[\"Tu nombre de visualización preferido\"],\"HmHDk7\":[\"Seleccionar miembro\"],\"HrQzPU\":[\"Canales en \",[\"networkName\"]],\"I2tXQ5\":[\"Mensaje a @\",[\"0\"],\" (Intro para nueva línea, Mayús+Intro para enviar)\"],\"I6bw/h\":[\"Banear usuario\"],\"I92Z+b\":[\"Activar notificaciones\"],\"I9D72S\":[\"¿Estás seguro de que deseas eliminar este mensaje? Esta acción no se puede deshacer.\"],\"IA+1wo\":[\"Mostrar cuando los usuarios son expulsados de los canales\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Guardar cambios\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" y \",[\"2\"],\" están escribiendo...\"],\"IgrLD/\":[\"Pausar\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Responder\"],\"IoHMnl\":[\"El valor máximo es \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Conectando...\"],\"J5T9NW\":[\"Información del usuario\"],\"J8Y5+z\":[\"¡Vaya! ¡División de red! ⚠️\"],\"JBHkBA\":[\"Abandonó el canal\"],\"JCwL0Q\":[\"Ingresa un motivo (opcional)\"],\"JFciKP\":[\"Alternar\"],\"JXGkhG\":[\"Cambiar el nombre del canal (solo operadores)\"],\"JcD7qf\":[\"Más acciones\"],\"JdkA+c\":[\"Secreto (+s)\"],\"Jmu12l\":[\"Canales del servidor\"],\"JvQ++s\":[\"Activar Markdown\"],\"K2jwh/\":[\"No hay datos WHOIS disponibles\"],\"KAXSwC\":[\"Voz\"],\"KDfTdX\":[\"Eliminar mensaje\"],\"KKBlUU\":[\"Insertar\"],\"KM0pLb\":[\"¡Bienvenido al canal!\"],\"KR6W2h\":[\"Dejar de ignorar usuario\"],\"KV+Bi1\":[\"Solo por invitación (+i)\"],\"KdCtwE\":[\"Cuántos segundos monitorear la actividad de flood antes de restablecer los contadores\"],\"Kkezga\":[\"Contraseña del servidor\"],\"KsiQ/8\":[\"Los usuarios deben ser invitados para unirse al canal\"],\"L+gB/D\":[\"Información del canal\"],\"LC1a7n\":[\"El servidor IRC ha informado que sus enlaces entre servidores tienen un nivel de seguridad bajo. Esto significa que cuando tus mensajes se retransmiten entre servidores IRC en la red, es posible que no estén correctamente cifrados o que los certificados SSL/TLS no se validen correctamente.\"],\"LNfLR5\":[\"Mostrar expulsiones\"],\"LQb0W/\":[\"Mostrar todos los eventos\"],\"LU7/yA\":[\"Nombre alternativo para mostrar en la interfaz. Puede contener espacios, emoji y caracteres especiales. El nombre real del canal (\",[\"channelName\"],\") seguirá usándose para los comandos IRC.\"],\"LUb9O7\":[\"Se requiere un puerto de servidor válido\"],\"LV4fT6\":[\"Descripción (opcional, p. ej. \\\"Probadores beta T3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Política de privacidad\"],\"LcuSDR\":[\"Administra la información y metadatos de tu perfil\"],\"LqLS9B\":[\"Mostrar cambios de apodo\"],\"LsDQt2\":[\"Configuración del canal\"],\"LtI9AS\":[\"Propietario\"],\"LuNhhL\":[\"reaccionó a este mensaje\"],\"M/AZNG\":[\"URL de tu imagen de avatar\"],\"M/WIer\":[\"Enviar mensaje\"],\"M8er/5\":[\"Nombre:\"],\"MHk+7g\":[\"Imagen anterior\"],\"MRorGe\":[\"MP al usuario\"],\"MVbSGP\":[\"Ventana de tiempo (segundos)\"],\"MkpcsT\":[\"Tus mensajes y ajustes se almacenan localmente en tu dispositivo\"],\"N/hDSy\":[\"Marcar como bot, normalmente 'on' o vacío\"],\"N7TQbE\":[\"Invitar usuario a \",[\"channelName\"]],\"NCca/o\":[\"Ingresa apodo predeterminado...\"],\"Nqs6B9\":[\"Muestra todos los medios externos. Cualquier URL puede generar una solicitud a un servidor desconocido.\"],\"Nt+9O7\":[\"Usar WebSocket en lugar de TCP sin procesar\"],\"NxIHzc\":[\"Expulsar usuario\"],\"O+v/cL\":[\"Ver todos los canales del servidor\"],\"ODwSCk\":[\"Enviar un GIF\"],\"OGQ5kK\":[\"Configurar sonidos de notificación y resaltados\"],\"OIPt1Z\":[\"Mostrar u ocultar la barra lateral de miembros\"],\"OKSNq/\":[\"Muy estricto\"],\"ONWvwQ\":[\"Subir\"],\"OVKoQO\":[\"Tu contraseña de cuenta para autenticación\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Llevando IRC al futuro\"],\"OhCpra\":[\"Establece un tema…\"],\"OkltoQ\":[\"Banear a \",[\"username\"],\" por apodo (impide que vuelva a unirse con el mismo nick)\"],\"P+t/Te\":[\"Sin datos adicionales\"],\"P42Wcc\":[\"Seguro\"],\"PD38l0\":[\"Vista previa del avatar del canal\"],\"PD9mEt\":[\"Escribe un mensaje...\"],\"PPqfdA\":[\"Abrir configuración del canal\"],\"PSCjfZ\":[\"El tema que se mostrará para este canal. Todos los usuarios pueden ver el tema.\"],\"PZCecv\":[\"Vista previa de PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 vez\"],\"other\":[[\"c\"],\" veces\"]}]],\"PguS2C\":[\"Agregar máscara de excepción (p. ej., nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Mostrando \",[\"displayedChannelsCount\"],\" de \",[\"0\"],\" canales\"],\"PqhVlJ\":[\"Banear usuario (por hostmask)\"],\"Q+chwU\":[\"Nombre de usuario:\"],\"Q2QY4/\":[\"Eliminar esta invitación\"],\"Q6hhn8\":[\"Preferencias\"],\"QF4a34\":[\"Por favor, introduce un nombre de usuario\"],\"QGqSZ2\":[\"Color y formato\"],\"QJQd1J\":[\"Editar perfil\"],\"QSzGDE\":[\"Inactivo\"],\"QUlny5\":[\"¡Bienvenido a \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Leer más\"],\"QuSkCF\":[\"Filtrar canales...\"],\"QwUrDZ\":[\"cambió el tema a: \",[\"topic\"]],\"R0UH07\":[\"Imagen \",[\"0\"],\" de \",[\"1\"]],\"R7SsBE\":[\"Silenciar\"],\"R8rf1X\":[\"Haz clic para establecer el tema\"],\"RArB3D\":[\"fue expulsado de \",[\"channelName\"],\" por \",[\"username\"]],\"RI3cWd\":[\"Descubre el mundo de IRC con ObsidianIRC\"],\"RIfHS5\":[\"Crear un nuevo enlace de invitación\"],\"RMMaN5\":[\"Moderado (+m)\"],\"RWw9Lg\":[\"Cerrar ventana\"],\"RZ2BuZ\":[\"Se requiere verificación para el registro de la cuenta \",[\"account\"],\": \",[\"message\"]],\"RySp6q\":[\"Ocultar comentarios\"],\"SPKQTd\":[\"El apodo es obligatorio\"],\"SPVjfj\":[\"Por defecto será 'sin motivo' si se deja vacío\"],\"SQKPvQ\":[\"Invitar usuario\"],\"SkZcl+\":[\"Elige un perfil de protección contra flood predefinido. Estos perfiles ofrecen configuraciones de protección equilibradas para diferentes casos de uso.\"],\"Slr+3C\":[\"Mínimo de usuarios\"],\"Spnlre\":[\"Has invitado a \",[\"target\"],\" a unirse a \",[\"channel\"]],\"T/ckN5\":[\"Abrir en el visor\"],\"T91vKp\":[\"Reproducir\"],\"TV2Wdu\":[\"Conoce cómo gestionamos tus datos y protegemos tu privacidad.\"],\"TgFpwD\":[\"Aplicando...\"],\"TkzSFB\":[\"Sin cambios\"],\"TtserG\":[\"Ingresa el nombre real\"],\"Ttz9J1\":[\"Ingresa contraseña...\"],\"Tz0i8g\":[\"Ajustes\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reaccionar\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Aún no has creado ningún enlace de invitación. Usa el formulario de arriba para generar el primero.\"],\"UGT5vp\":[\"Guardar configuración\"],\"UV5hLB\":[\"No se encontraron bans\"],\"Uaj3Nd\":[\"Mensajes de estado\"],\"Ue3uny\":[\"Predeterminado (sin perfil)\"],\"UkARhe\":[\"Normal – Protección estándar\"],\"Umn7Cj\":[\"Aún no hay comentarios. ¡Sé el primero!\"],\"UtUIRh\":[[\"0\"],\" mensajes anteriores\"],\"UwzP+U\":[\"Conexión segura\"],\"V0/A4O\":[\"Propietario del canal\"],\"V4qgxE\":[\"Creado hace menos de (min)\"],\"V8yTm6\":[\"Limpiar búsqueda\"],\"VJMMyz\":[\"ObsidianIRC - Llevando IRC al futuro\"],\"VJScHU\":[\"Motivo\"],\"VLsmVV\":[\"Silenciar notificaciones\"],\"VbyRUy\":[\"Comentarios\"],\"Vmx0mQ\":[\"Establecido por:\"],\"VqnIZz\":[\"Ver nuestra política de privacidad y prácticas de datos\"],\"VrMygG\":[\"La longitud mínima es \",[\"0\"]],\"VrnTui\":[\"Tus pronombres, mostrados en tu perfil\"],\"W8E3qn\":[\"Cuenta autenticada\"],\"WAakm9\":[\"Eliminar canal\"],\"WFxTHC\":[\"Agregar máscara de ban (p. ej., nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"El host del servidor es obligatorio\"],\"WRYdXW\":[\"Posición del audio\"],\"WUOH5B\":[\"Ignorar usuario\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Mostrar 1 elemento más\"],\"other\":[\"Mostrar \",[\"1\"],\" elementos más\"]}]],\"WYxRzo\":[\"Crea y gestiona tus enlaces de invitación\"],\"Wd38W1\":[\"Deja el canal en blanco para una invitación genérica a la red. La descripción es solo para tus registros — visible únicamente para ti en esta lista.\"],\"Weq9zb\":[\"General\"],\"Wfj7Sk\":[\"Silenciar o activar los sonidos de notificación\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Perfil de usuario\"],\"X6S3lt\":[\"Buscar ajustes, canales, servidores...\"],\"XEHan5\":[\"Continuar de todos modos\"],\"XI1+wb\":[\"Formato no válido\"],\"XIXeuC\":[\"Mensaje a @\",[\"0\"]],\"XMS+k4\":[\"Iniciar mensaje privado\"],\"XWgxXq\":[\"Álbum\"],\"Xd7+IT\":[\"Desfijar chat privado\"],\"Xm/s+u\":[\"Pantalla\"],\"Xp2n93\":[\"Muestra medios desde el host de archivos de confianza de tu servidor. No se realizan solicitudes a servicios externos.\"],\"XvjC4F\":[\"Guardando...\"],\"Y/qryO\":[\"No se encontraron usuarios que coincidan con tu búsqueda\"],\"YAqRpI\":[\"Registro de cuenta exitoso para \",[\"account\"],\": \",[\"message\"]],\"YEfzvP\":[\"Tema protegido (+t)\"],\"YQOn6a\":[\"Contraer lista de miembros\"],\"YRCoE9\":[\"Operador del canal\"],\"YURQaF\":[\"Ver perfil\"],\"YdBSvr\":[\"Controlar la visualización de medios y contenido externo\"],\"Yj6U3V\":[\"Sin servidor central:\"],\"YjvpGx\":[\"Pronombres\"],\"YqH4l4\":[\"Sin clave\"],\"YyUPpV\":[\"Cuenta:\"],\"ZJSWfw\":[\"Mensaje al desconectarse del servidor\"],\"ZR1dJ4\":[\"Invitaciones\"],\"ZdWg0V\":[\"Abrir en el navegador\"],\"ZhRBbl\":[\"Buscar mensajes…\"],\"Zmcu3y\":[\"Filtros avanzados\"],\"a2/8e5\":[\"Tema establecido hace más de (min)\"],\"aHKcKc\":[\"Página anterior\"],\"aJTbXX\":[\"Contraseña de oper\"],\"aQryQv\":[\"El patrón ya existe\"],\"aW9pLN\":[\"Número máximo de usuarios permitidos en el canal. Deja vacío para no establecer límite.\"],\"ah4fmZ\":[\"También muestra vistas previas de YouTube, Vimeo, SoundCloud y servicios conocidos similares.\"],\"aifXak\":[\"No hay medios en este canal\"],\"ap2zBz\":[\"Relajado\"],\"az8lvo\":[\"Desactivado\"],\"azXSNo\":[\"Expandir lista de miembros\"],\"azdliB\":[\"Iniciar sesión en una cuenta\"],\"b26wlF\":[\"ella/la\"],\"bD/+Ei\":[\"Estricto\"],\"bQ6BJn\":[\"Configura reglas detalladas de protección contra flood. Cada regla especifica qué tipo de actividad monitorear y qué acción tomar cuando se superan los umbrales.\"],\"beV7+y\":[\"El usuario recibirá una invitación para unirse a \",[\"channelName\"],\".\"],\"bk84cH\":[\"Mensaje de ausencia\"],\"bkHdLj\":[\"Agregar servidor IRC\"],\"bmQLn5\":[\"Añadir regla\"],\"bwRvnp\":[\"Acción\"],\"c8+EVZ\":[\"Cuenta verificada\"],\"cGYUlD\":[\"No se carga ninguna vista previa de medios.\"],\"cLF98o\":[\"Mostrar comentarios (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"No hay usuarios disponibles\"],\"cSgpoS\":[\"Fijar chat privado\"],\"cde3ce\":[\"Mensaje a <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Copiar salida formateada\"],\"cl/A5J\":[\"¡Bienvenido a \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Eliminar\"],\"coPLXT\":[\"No almacenamos tus comunicaciones IRC en nuestros servidores\"],\"crYH/6\":[\"Reproductor de SoundCloud\"],\"d3sis4\":[\"Agregar servidor\"],\"d9aN5k\":[\"Eliminar a \",[\"username\"],\" del canal\"],\"dEgA5A\":[\"Cancelar\"],\"dGi1We\":[\"Desfijar esta conversación de mensaje privado\"],\"dJVuyC\":[\"salió de \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"a\"],\"dXqxlh\":[\"<0>⚠️ ¡Riesgo de seguridad!0> Esta conexión puede ser vulnerable a intercepciones o ataques de intermediario.\"],\"da9Q/R\":[\"Modos del canal cambiados\"],\"dhJN3N\":[\"Mostrar comentarios\"],\"dj2xTE\":[\"Descartar notificación\"],\"dpCzmC\":[\"Configuración de protección contra flood\"],\"e9dQpT\":[\"¿Deseas abrir este enlace en una nueva pestaña?\"],\"ePK91l\":[\"Editar\"],\"eYBDuB\":[\"Sube una imagen o proporciona una URL con sustitución opcional de \",[\"size\"],\" para tamaño dinámico\"],\"edBbee\":[\"Banear a \",[\"username\"],\" por hostmask (impide que vuelva a unirse desde la misma IP/host)\"],\"ekfzWq\":[\"Configuración de usuario\"],\"elPDWs\":[\"Personaliza tu experiencia con el cliente IRC\"],\"eu2osY\":[\"<0>💡 Recomendación:0> Continúa solo si confías en este servidor y entiendes los riesgos. Evita compartir información sensible o contraseñas a través de esta conexión.\"],\"euEhbr\":[\"Haz clic para unirte a \",[\"channel\"]],\"ez3vLd\":[\"Activar entrada multilínea\"],\"f0J5Ki\":[\"La comunicación entre servidores puede usar conexiones sin cifrar\"],\"f9BHJk\":[\"Advertir al usuario\"],\"fDOLLd\":[\"No se encontraron canales.\"],\"ffzDkB\":[\"Analíticas anónimas:\"],\"fq1GF9\":[\"Mostrar cuando los usuarios se desconectan del servidor\"],\"gEF57C\":[\"Este servidor solo admite un tipo de conexión\"],\"gJuLUI\":[\"Lista de ignorados\"],\"gNzMrk\":[\"Avatar actual\"],\"gjPWyO\":[\"Ingresa tu apodo...\"],\"gz6UQ3\":[\"Maximizar\"],\"h6razj\":[\"Excluir máscara de nombre de canal\"],\"hG6jnw\":[\"Sin tema establecido\"],\"hG89Ed\":[\"Imagen\"],\"hYgDIe\":[\"Crear\"],\"hZ6znB\":[\"Puerto\"],\"ha+Bz5\":[\"ej., 100:1440\"],\"he3ygx\":[\"Copiar\"],\"hehnjM\":[\"Cantidad\"],\"hzdLuQ\":[\"Solo los usuarios con Voice o superior pueden hablar\"],\"i0qMbr\":[\"Inicio\"],\"iDNBZe\":[\"Notificaciones\"],\"iH8pgl\":[\"Atrás\"],\"iL9SZg\":[\"Banear usuario (por apodo)\"],\"iNt+3c\":[\"Volver a la imagen\"],\"iQvi+a\":[\"No advertirme sobre la baja seguridad de enlaces para este servidor\"],\"iSLIjg\":[\"Conectar\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Host del servidor\"],\"idD8Ev\":[\"Guardado\"],\"iivqkW\":[\"Conectado desde\"],\"ij+Elv\":[\"Vista previa de imagen\"],\"ilIWp7\":[\"Alternar notificaciones\"],\"iuaqvB\":[\"Usa * como comodín. Ejemplos: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Banear por hostmask\"],\"jA4uoI\":[\"Tema:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Motivo (opcional)\"],\"jUV7CU\":[\"Subir avatar\"],\"jW5Uwh\":[\"Controla cuántos medios externos se cargan. Desactivado / Seguro / Fuentes confiables / Todo el contenido.\"],\"jXzms5\":[\"Opciones de adjunto\"],\"jZlrte\":[\"Color\"],\"jfC/xh\":[\"Contacto\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Cargar mensajes anteriores\"],\"k3ID0F\":[\"Filtrar miembros…\"],\"k65gsE\":[\"Ver en detalle\"],\"k7Zgob\":[\"Cancelar conexión\"],\"kAVx5h\":[\"No se encontraron invitaciones\"],\"kCLEPU\":[\"Conectado a\"],\"kF5LKb\":[\"Patrones ignorados:\"],\"kGeOx/\":[\"Unirse a \",[\"0\"]],\"kITKr8\":[\"Cargando modos del canal...\"],\"kPpPsw\":[\"Eres un IRC Operator\"],\"kWJmRL\":[\"Tú\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Copiar JSON\"],\"krViRy\":[\"Clic para copiar como JSON\"],\"ks71ra\":[\"Excepciones\"],\"kw4lRv\":[\"Medio operador del canal\"],\"kxgIRq\":[\"Selecciona o agrega un canal para comenzar.\"],\"ky6dWe\":[\"Vista previa del avatar\"],\"l+GxCv\":[\"Cargando canales...\"],\"l+IUVW\":[\"Verificación de cuenta exitosa para \",[\"account\"],\": \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"se reconectó\"],\"other\":[\"se reconectó \",[\"reconnectCount\"],\" veces\"]}]],\"l1l8sj\":[\"hace \",[\"0\"],\"d\"],\"l5NhnV\":[\"#canal (opcional)\"],\"l5jmzx\":[[\"0\"],\" y \",[\"1\"],\" están escribiendo...\"],\"lCF0wC\":[\"Actualizar\"],\"lHy8N5\":[\"Cargando más canales...\"],\"lasgrr\":[\"usado\"],\"lbpf14\":[\"Unirse a \",[\"value\"]],\"lfFsZ4\":[\"Canales\"],\"lkNdiH\":[\"Nombre de cuenta\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Subir imagen\"],\"loQxaJ\":[\"He vuelto\"],\"lvfaxv\":[\"INICIO\"],\"m16xKo\":[\"Agregar\"],\"m8flAk\":[\"Vista previa (aún no subido)\"],\"mEPxTp\":[\"<0>⚠️ ¡Ten cuidado!0> Solo abre enlaces de fuentes de confianza. Los enlaces maliciosos pueden comprometer tu seguridad o privacidad.\"],\"mHGdhG\":[\"Información del servidor\"],\"mHS8lb\":[\"Mensaje en #\",[\"0\"]],\"mMYBD9\":[\"Amplio – Ámbito de protección más amplio\"],\"mTGsPd\":[\"Tema del canal\"],\"mU8j6O\":[\"Sin mensajes externos (+n)\"],\"mZp8FL\":[\"Retorno automático a línea única\"],\"mdQu8G\":[\"TuApodo\"],\"miSSBQ\":[\"Comentarios (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"El usuario está autenticado\"],\"mwtcGl\":[\"Cerrar comentarios\"],\"mzI/c+\":[\"Descargar\"],\"n3fGRk\":[\"establecido por \",[\"0\"]],\"nE9jsU\":[\"Relajado – Protección menos agresiva\"],\"nNflMD\":[\"Salir del canal\"],\"nPXkBi\":[\"Cargando datos WHOIS...\"],\"nQnxxF\":[\"Mensaje en #\",[\"0\"],\" (Mayús+Intro para nueva línea)\"],\"nWMRxa\":[\"Desfijar\"],\"nkC032\":[\"Sin perfil de flood\"],\"o69z4d\":[\"Enviar un mensaje de advertencia a \",[\"username\"]],\"o9ylQi\":[\"Busca GIFs para empezar\"],\"oFGkER\":[\"Avisos del servidor\"],\"oOi11l\":[\"Ir al final\"],\"oPYIL5\":[\"red\"],\"oQEzQR\":[\"Nuevo mensaje directo\"],\"oXOSPE\":[\"En línea\"],\"oal760\":[\"Son posibles ataques de intermediario en los enlaces del servidor\"],\"oeqmmJ\":[\"Fuentes de confianza\"],\"optX0N\":[\"hace \",[\"0\"],\"h\"],\"ovBPCi\":[\"Predeterminado\"],\"p0Z69r\":[\"El patrón no puede estar vacío\"],\"p1KgtK\":[\"Error al cargar el audio\"],\"p59pEv\":[\"Detalles adicionales\"],\"p7sRI6\":[\"Avisar a otros cuando estás escribiendo\"],\"pBm1od\":[\"Canal secreto\"],\"pNmiXx\":[\"Tu apodo predeterminado para todos los servidores\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Contraseña de la cuenta\"],\"peNE68\":[\"Permanente\"],\"plhHQt\":[\"Sin datos\"],\"pm6+q5\":[\"Advertencia de seguridad\"],\"pn5qSs\":[\"Información adicional\"],\"q0cR4S\":[\"ahora se llama **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"El canal no aparecerá en los comandos LIST ni NAMES\"],\"qLpTm/\":[\"Eliminar reacción \",[\"emoji\"]],\"qVkGWK\":[\"Fijar\"],\"qY8wNa\":[\"Página de inicio\"],\"qb0xJ7\":[\"Usa comodines: * coincide con cualquier secuencia, ? coincide con cualquier carácter individual. Ejemplos: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Clave del canal (+k)\"],\"qtoOYG\":[\"Sin límite\"],\"r1W2AS\":[\"Imagen del servidor de archivos\"],\"rIPR2O\":[\"Tema establecido hace menos de (min)\"],\"rMMSYo\":[\"La longitud máxima es \",[\"0\"]],\"rWtzQe\":[\"La red se dividió y se reconectó. ✅\"],\"rYG2u6\":[\"Por favor espera...\"],\"rdUucN\":[\"Vista previa\"],\"rjGI/Q\":[\"Privacidad\"],\"rk8iDX\":[\"Cargando GIFs...\"],\"rn6SBY\":[\"Activar sonido\"],\"s/UKqq\":[\"Fue expulsado del canal\"],\"s8cATI\":[\"se unió a \",[\"channelName\"]],\"sCO9ue\":[\"La conexión a <0>\",[\"serverName\"],\"0> presenta los siguientes problemas de seguridad:\"],\"sGH11W\":[\"Servidor\"],\"sHI1H+\":[\"ahora se llama **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" te ha invitado a unirte a \",[\"channel\"]],\"sby+1/\":[\"Haz clic para copiar\"],\"sfN25C\":[\"Tu nombre real o completo\"],\"sliuzR\":[\"Abrir enlace\"],\"sqrO9R\":[\"Menciones personalizadas\"],\"sr6RdJ\":[\"Multilínea con Shift+Enter\"],\"swrCpB\":[\"El canal ha sido renombrado de \",[\"oldName\"],\" a \",[\"newName\"],\" por \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Avanzado\"],\"t/YqKh\":[\"Eliminar\"],\"t47eHD\":[\"Tu identificador único en este servidor\"],\"tAkAh0\":[\"URL con sustitución opcional de \",[\"size\"],\" para tamaño dinámico. Ejemplo: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Mostrar u ocultar la barra lateral de canales\"],\"tfDRzk\":[\"Guardar\"],\"tiBsJk\":[\"salió de \",[\"channelName\"]],\"tt4/UD\":[\"salió (\",[\"reason\"],\")\"],\"u0TcnO\":[\"El apodo {nick} ya está en uso, reintentando con {newNick}\"],\"u0a8B4\":[\"Autenticarse como operador IRC para acceso administrativo\"],\"u0rWFU\":[\"Creado hace más de (min)\"],\"u72w3t\":[\"Usuarios y patrones a ignorar\"],\"u7jc2L\":[\"salió\"],\"uAQUqI\":[\"Estado\"],\"uB85T3\":[\"Error al guardar: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"Servidores IRC:\"],\"ukyW4o\":[\"Tus enlaces de invitación\"],\"usSSr/\":[\"Nivel de zoom\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Shift+Enter para nuevas líneas (Enter envía)\"],\"vERlcd\":[\"Perfil\"],\"vK0RL8\":[\"Sin tema\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Idioma\"],\"vaHYxN\":[\"Nombre real\"],\"vhjbKr\":[\"Ausente\"],\"w4NYox\":[\"cliente \",[\"title\"]],\"w8xQRx\":[\"Valor no válido\"],\"wFjjxZ\":[\"fue expulsado de \",[\"channelName\"],\" por \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"No se encontraron excepciones de ban\"],\"wPrGnM\":[\"Administrador del canal\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Mostrar cuando los usuarios entran o salen de canales\"],\"whqZ9r\":[\"Palabras o frases adicionales para resaltar\"],\"wm7RV4\":[\"Sonido de notificación\"],\"wz/Yoq\":[\"Tus mensajes podrían ser interceptados al retransmitirse entre servidores\"],\"x3+y8b\":[\"Esta cantidad de personas se ha registrado mediante este enlace\"],\"xCJdfg\":[\"Limpiar\"],\"xOTzt5\":[\"ahora mismo\"],\"xUHRTR\":[\"Autenticarse automáticamente como operador al conectar\"],\"xWHwwQ\":[\"Bans\"],\"xYilR2\":[\"Medios\"],\"xbi8D6\":[\"Este servidor no admite enlaces de invitación (no se anuncia la capacidad<0>obby.world/invitation0>). Puedes seguir chateando con normalidad; este panel es para redes basadas en obbyircd.\"],\"xceQrO\":[\"Solo se admiten websockets seguros\"],\"xdtXa+\":[\"nombre-del-canal\"],\"xfXC7q\":[\"Canales de texto\"],\"xlCYOE\":[\"Cargando más mensajes...\"],\"xlhswE\":[\"El valor mínimo es \",[\"0\"]],\"xq97Ci\":[\"Agregar una palabra o frase...\"],\"xuRqRq\":[\"Límite de usuarios (+l)\"],\"xwF+7J\":[[\"0\"],\" está escribiendo...\"],\"y1eoq1\":[\"Copiar enlace\"],\"yNeucF\":[\"Este servidor no admite metadatos de perfil extendido (extensión IRCv3 METADATA). Los campos adicionales como avatar, nombre a mostrar y estado no están disponibles.\"],\"yPlrca\":[\"Avatar del canal\"],\"yQE2r9\":[\"Cargando\"],\"ySU+JY\":[\"tu@correo.com\"],\"yTX1Rt\":[\"Nombre de usuario Oper\"],\"yYOzWD\":[\"registros\"],\"yfx9Re\":[\"Contraseña de operador IRC\"],\"ygCKqB\":[\"Detener\"],\"ymDxJx\":[\"Nombre de usuario de operador IRC\"],\"yrpRsQ\":[\"Ordenar por nombre\"],\"yz7wBu\":[\"Cerrar\"],\"zJw+jA\":[\"establece modo: \",[\"0\"]],\"zbymaY\":[\"hace \",[\"0\"],\"m\"],\"zebeLu\":[\"Ingresa el nombre de usuario de oper\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Formato de patrón inválido. Usa el formato nick!user@host (se permiten comodines *)\"],\"+6NQQA\":[\"Canal de soporte general\"],\"+6NyRG\":[\"Cliente\"],\"+K0AvT\":[\"Desconectar\"],\"+cyFdH\":[\"Mensaje predeterminado al marcarse como ausente\"],\"+mVPqU\":[\"Renderizar formato Markdown en mensajes\"],\"+vqCJH\":[\"Tu nombre de usuario de cuenta para autenticación\"],\"+yPBXI\":[\"Elegir archivo\"],\"+zy2Nq\":[\"Tipo\"],\"/09cao\":[\"Baja seguridad de enlace (Nivel \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Los usuarios fuera del canal no pueden enviar mensajes a él\"],\"/4C8U0\":[\"Copiar todo\"],\"/6BzZF\":[\"Alternar lista de miembros\"],\"/AkXyp\":[\"¿Confirmar?\"],\"/TNOPk\":[\"El usuario está ausente\"],\"/XQgft\":[\"Explorar\"],\"/cF7Rs\":[\"Volumen\"],\"/dqduX\":[\"Página siguiente\"],\"/fc3q4\":[\"Todo el contenido\"],\"/kISDh\":[\"Activar sonidos de notificación\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Reproducir sonidos para menciones y mensajes\"],\"0/0ZGA\":[\"Máscara de nombre de canal\"],\"0D6j7U\":[\"Más información sobre reglas personalizadas →\"],\"0XsHcR\":[\"Expulsar usuario\"],\"0ZpE//\":[\"Ordenar por usuarios\"],\"0bEPwz\":[\"Marcar como ausente\"],\"0dGkPt\":[\"Expandir lista de canales\"],\"0gS7M5\":[\"Nombre a mostrar\"],\"0kS+M8\":[\"EjemploRED\"],\"0rgoY7\":[\"Solo te conectas a los servidores que eliges\"],\"0wdd7X\":[\"Unirse\"],\"0wkVYx\":[\"Mensajes privados\"],\"111uHX\":[\"Vista previa del enlace\"],\"196EG4\":[\"Eliminar chat privado\"],\"1DSr1i\":[\"Registrar una cuenta\"],\"1O/24y\":[\"Alternar lista de canales\"],\"1TNIig\":[\"Abrir\"],\"1VPJJ2\":[\"Advertencia de enlace externo\"],\"1ZC/dv\":[\"Sin menciones ni mensajes sin leer\"],\"1pO1zi\":[\"El nombre del servidor es obligatorio\"],\"1uwfzQ\":[\"Ver tema del canal\"],\"268g7c\":[\"Ingresa el nombre a mostrar\"],\"2CEOW6\":[\"Red vinculada a través del bouncer soju\"],\"2F9+AZ\":[\"Aún no se ha capturado tráfico IRC sin procesar. Prueba a conectarte o enviar un mensaje.\"],\"2FOFq1\":[\"Los operadores del servidor en la red podrían leer tus mensajes\"],\"2FYpfJ\":[\"Más\"],\"2HF1Y2\":[[\"inviter\"],\" ha invitado a \",[\"target\"],\" a unirse a \",[\"channel\"]],\"2I70QL\":[\"Ver información del perfil de usuario\"],\"2QYdmE\":[\"Usuarios:\"],\"2QpEjG\":[\"salió\"],\"2YE223\":[\"Mensaje en #\",[\"0\"],\" (Intro para nueva línea, Mayús+Intro para enviar)\"],\"2bimFY\":[\"Usar contraseña del servidor\"],\"2iTmdZ\":[\"Almacenamiento local:\"],\"2odkwe\":[\"Estricto – Protección más agresiva\"],\"2uDhbA\":[\"Ingresa el nombre de usuario a invitar\"],\"2ygf/L\":[\"← Atrás\"],\"2zEgxj\":[\"Buscar GIFs...\"],\"3RdPhl\":[\"Renombrar canal\"],\"3THokf\":[\"Usuario con voz\"],\"3TSz9S\":[\"Minimizar\"],\"3jBDvM\":[\"Nombre a mostrar del canal\"],\"3ryuFU\":[\"Informes de errores opcionales para mejorar la app\"],\"3uBF/8\":[\"Cerrar visor\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Ingresa nombre de cuenta...\"],\"4/Rr0R\":[\"Invitar a un usuario al canal actual\"],\"4EZrJN\":[\"Reglas\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Perfil de flood (+F)\"],\"4RZQRK\":[\"¿Qué estás haciendo?\"],\"4hfTrB\":[\"Apodo\"],\"4n99LO\":[\"Ya en \",[\"0\"]],\"4t6vMV\":[\"Cambiar automáticamente a línea única para mensajes cortos\"],\"4vsHmf\":[\"Tiempo (min)\"],\"4x/Axu\":[\"Tu bouncer aún no tiene ninguna red. Agrega una para empezar.\"],\"5+INAX\":[\"Resaltar mensajes que te mencionan\"],\"5R5Pv/\":[\"Nombre de oper\"],\"678PKt\":[\"Nombre de red\"],\"6Aih4U\":[\"Desconectado\"],\"6CO3WE\":[\"Contraseña requerida para unirse al canal. Deja vacío para eliminar la clave.\"],\"6HhMs3\":[\"Mensaje de salida\"],\"6V3Ea3\":[\"Copiado\"],\"6lGV3K\":[\"Mostrar menos\"],\"6yFOEi\":[\"Ingresa contraseña de oper...\"],\"7+IHTZ\":[\"Ningún archivo elegido\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (ej., spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Enviar mensaje privado\"],\"7U1W7c\":[\"Muy relajado\"],\"7Y1YQj\":[\"Nombre real:\"],\"7YHArF\":[\"— abrir en visor\"],\"7fjnVl\":[\"Buscar usuarios...\"],\"7jL88x\":[\"¿Eliminar este mensaje? Esta acción no se puede deshacer.\"],\"7nGhhM\":[\"¿Qué piensas?\"],\"7sEpu1\":[\"Miembros — \",[\"0\"]],\"7sNhEz\":[\"Nombre de usuario\"],\"8H0Q+x\":[\"Más información sobre perfiles →\"],\"8Phu0A\":[\"Mostrar cuando los usuarios cambian su apodo\"],\"8XTG9e\":[\"Ingresa la contraseña de oper\"],\"8XsV2J\":[\"Reintentar envío\"],\"8ZsakT\":[\"Contraseña\"],\"8kR84m\":[\"Estás a punto de abrir un enlace externo:\"],\"8lCgih\":[\"Eliminar regla\"],\"8o3dPc\":[\"Suelta archivos para subirlos\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"se unió\"],\"other\":[\"se unió \",[\"joinCount\"],\" veces\"]}]],\"9BMLnJ\":[\"Reconectar al servidor\"],\"9OEgyT\":[\"Agregar reacción\"],\"9PQ8m2\":[\"G-Line (ban global)\"],\"9Qs99X\":[\"Correo electrónico:\"],\"9QupBP\":[\"Eliminar patrón\"],\"9W7tl5\":[\"(sin cambios)\"],\"9bG48P\":[\"Enviando\"],\"9f5f0u\":[\"¿Preguntas sobre privacidad? Contáctanos:\"],\"9iweoP\":[\"Redes en \",[\"0\"]],\"9unqs3\":[\"Ausente:\"],\"9v3hwv\":[\"No se encontraron servidores.\"],\"9zb2WA\":[\"Conectando\"],\"A1taO8\":[\"Buscar\"],\"A2adVi\":[\"Enviar notificaciones de escritura\"],\"A9Rhec\":[\"Nombre del canal\"],\"AWOSPo\":[\"Acercar\"],\"AXSpEQ\":[\"Oper al conectar\"],\"AdKRCX\":[\"Estás conectado a <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Cuenta\"],\"AhNP40\":[\"Buscar posición\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Apodo cambiado\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Cancelar respuesta\"],\"ApSx0O\":[\"Se encontraron \",[\"0\"],\" mensajes que coinciden con \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"No se encontraron resultados\"],\"AyNqAB\":[\"Mostrar todos los eventos del servidor en el chat\"],\"B/QqGw\":[\"Alejado del teclado\"],\"B0sB2k\":[\"Texto plano\"],\"B8AaMI\":[\"Este campo es obligatorio\"],\"BA2c49\":[\"El servidor no admite filtrado avanzado de LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" y \",[\"3\"],\" más están escribiendo...\"],\"BGul2A\":[\"Tienes cambios sin guardar. ¿Seguro que deseas cerrar sin guardar?\"],\"BIf9fi\":[\"Tu mensaje de estado\"],\"BOJWfb\":[\"¿Desconectar del bouncer soju?\"],\"BPm98R\":[\"No hay ningún servidor seleccionado. Elige primero un servidor en la barra lateral; los enlaces de invitación se gestionan por servidor.\"],\"BZz3md\":[\"Tu sitio web personal\"],\"Bgm/H7\":[\"Permitir ingresar múltiples líneas de texto\"],\"BiQIl1\":[\"Fijar esta conversación de mensaje privado\"],\"BlNZZ2\":[\"Haz clic para ir al mensaje\"],\"Bowq3c\":[\"Solo los operadores pueden cambiar el tema del canal\"],\"Btozzp\":[\"Esta imagen ha expirado\"],\"Bycfjm\":[\"Total: \",[\"0\"]],\"C6IBQc\":[\"Copiar JSON completo\"],\"C9L9wL\":[\"Recopilación de datos\"],\"CDq4wC\":[\"Moderar usuario\"],\"CHVRxG\":[\"Mensaje a @\",[\"0\"],\" (Mayús+Intro para nueva línea)\"],\"CN9zdR\":[\"El nombre y la contraseña de oper son obligatorios\"],\"CW3sYa\":[\"Agregar reacción \",[\"emoji\"]],\"CaAkqd\":[\"Mostrar desconexiones\"],\"CbvaYj\":[\"Banear por apodo\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Selecciona un canal\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"Sistema\"],\"D28t6+\":[\"se unió y salió\"],\"DB8zMK\":[\"Aplicar\"],\"DBcWHr\":[\"Archivo de sonido de notificación personalizado\"],\"DTy9Xw\":[\"Vistas previas de medios\"],\"Dj4pSr\":[\"Elige una contraseña segura\"],\"Du+zn+\":[\"Buscando...\"],\"Du2T2f\":[\"Ajuste no encontrado\"],\"DwsSVQ\":[\"Aplicar filtros y actualizar\"],\"E3W/zd\":[\"Apodo predeterminado\"],\"E6nRW7\":[\"Copiar URL\"],\"E703RG\":[\"Modos:\"],\"EAeu1Z\":[\"Enviar invitación\"],\"EFKJQT\":[\"Ajuste\"],\"EGPQBv\":[\"Reglas de flood personalizadas (+f)\"],\"ELik0r\":[\"Ver política de privacidad completa\"],\"EPbeC2\":[\"Ver o editar el tema del canal\"],\"EQCDNT\":[\"Ingresa nombre de usuario oper...\"],\"EUvulZ\":[\"Se encontró 1 mensaje que coincide con \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Imagen siguiente\"],\"EdQY6l\":[\"Ninguno\"],\"EnqLYU\":[\"Buscar servidores...\"],\"F0OKMc\":[\"Editar servidor\"],\"F6Int2\":[\"Activar resaltados\"],\"FDoLyE\":[\"Máximo de usuarios\"],\"FUU/hZ\":[\"Controla cuántos medios externos se cargan en el chat.\"],\"Fdp03t\":[\"activado\"],\"FfPWR0\":[\"Modal\"],\"FjkaiT\":[\"Alejar\"],\"FlqOE9\":[\"Qué significa esto:\"],\"FolHNl\":[\"Administra tu cuenta y autenticación\"],\"Fp2Dif\":[\"Salió del servidor\"],\"G5KmCc\":[\"GZ-Line (Z-Line global)\"],\"GDs0lz\":[\"<0>Riesgo:0> La información sensible (mensajes, conversaciones privadas, datos de autenticación) podría quedar expuesta a administradores de red o atacantes situados entre los servidores IRC.\"],\"GR+2I3\":[\"Agregar máscara de invitación (p. ej., nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Cerrar avisos del servidor desplegados\"],\"GdhD7H\":[\"Haz clic de nuevo para confirmar\"],\"GjRZex\":[\"¿Desconectar red?\"],\"GlHnXw\":[\"Cambio de apodo fallido: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Vista previa:\"],\"GtmO8/\":[\"de\"],\"GtuHUQ\":[\"Renombrar este canal en el servidor. Todos los usuarios verán el nuevo nombre.\"],\"GuGfFX\":[\"Alternar búsqueda\"],\"GxkJXS\":[\"Subiendo...\"],\"GzbwnK\":[\"Se unió al canal\"],\"GzsUDB\":[\"Perfil extendido\"],\"H/PnT8\":[\"Insertar emoji\"],\"H6Izzl\":[\"Tu código de color preferido\"],\"H9jIv+\":[\"Mostrar entradas/salidas\"],\"HAKBY9\":[\"Subir archivos\"],\"HdE1If\":[\"Canal\"],\"Hk4AW9\":[\"Tu nombre de visualización preferido\"],\"HmHDk7\":[\"Seleccionar miembro\"],\"HrQzPU\":[\"Canales en \",[\"networkName\"]],\"I2tXQ5\":[\"Mensaje a @\",[\"0\"],\" (Intro para nueva línea, Mayús+Intro para enviar)\"],\"I6bw/h\":[\"Banear usuario\"],\"I92Z+b\":[\"Activar notificaciones\"],\"I9D72S\":[\"¿Estás seguro de que deseas eliminar este mensaje? Esta acción no se puede deshacer.\"],\"IA+1wo\":[\"Mostrar cuando los usuarios son expulsados de los canales\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Guardar cambios\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" y \",[\"2\"],\" están escribiendo...\"],\"IgrLD/\":[\"Pausar\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Responder\"],\"IoHMnl\":[\"El valor máximo es \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Conectando...\"],\"J5T9NW\":[\"Información del usuario\"],\"J8Y5+z\":[\"¡Vaya! ¡División de red! ⚠️\"],\"JBHkBA\":[\"Abandonó el canal\"],\"JCwL0Q\":[\"Ingresa un motivo (opcional)\"],\"JFciKP\":[\"Alternar\"],\"JXGkhG\":[\"Cambiar el nombre del canal (solo operadores)\"],\"JcD7qf\":[\"Más acciones\"],\"JdkA+c\":[\"Secreto (+s)\"],\"Jmu12l\":[\"Canales del servidor\"],\"JoQY+E\":[\"Esto elimina la red de tu bouncer soju. Para usarla de nuevo, deberás añadirla otra vez.\"],\"JvQ++s\":[\"Activar Markdown\"],\"K2jwh/\":[\"No hay datos WHOIS disponibles\"],\"KAXSwC\":[\"Voz\"],\"KDfTdX\":[\"Eliminar mensaje\"],\"KKBlUU\":[\"Insertar\"],\"KM0pLb\":[\"¡Bienvenido al canal!\"],\"KR6W2h\":[\"Dejar de ignorar usuario\"],\"KV+Bi1\":[\"Solo por invitación (+i)\"],\"KdCtwE\":[\"Cuántos segundos monitorear la actividad de flood antes de restablecer los contadores\"],\"Kkezga\":[\"Contraseña del servidor\"],\"KsiQ/8\":[\"Los usuarios deben ser invitados para unirse al canal\"],\"L+gB/D\":[\"Información del canal\"],\"LC1a7n\":[\"El servidor IRC ha informado que sus enlaces entre servidores tienen un nivel de seguridad bajo. Esto significa que cuando tus mensajes se retransmiten entre servidores IRC en la red, es posible que no estén correctamente cifrados o que los certificados SSL/TLS no se validen correctamente.\"],\"LEwpeL\":[\"bouncer soju (control)\"],\"LNfLR5\":[\"Mostrar expulsiones\"],\"LP+1Z7\":[\"Agregar red\"],\"LQb0W/\":[\"Mostrar todos los eventos\"],\"LU7/yA\":[\"Nombre alternativo para mostrar en la interfaz. Puede contener espacios, emoji y caracteres especiales. El nombre real del canal (\",[\"channelName\"],\") seguirá usándose para los comandos IRC.\"],\"LUb9O7\":[\"Se requiere un puerto de servidor válido\"],\"LV4fT6\":[\"Descripción (opcional, p. ej. \\\"Probadores beta T3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Política de privacidad\"],\"LcuSDR\":[\"Administra la información y metadatos de tu perfil\"],\"LqLS9B\":[\"Mostrar cambios de apodo\"],\"LsDQt2\":[\"Configuración del canal\"],\"LtI9AS\":[\"Propietario\"],\"LuNhhL\":[\"reaccionó a este mensaje\"],\"M/AZNG\":[\"URL de tu imagen de avatar\"],\"M/WIer\":[\"Enviar mensaje\"],\"M8er/5\":[\"Nombre:\"],\"MHk+7g\":[\"Imagen anterior\"],\"MRorGe\":[\"MP al usuario\"],\"MVbSGP\":[\"Ventana de tiempo (segundos)\"],\"MkpcsT\":[\"Tus mensajes y ajustes se almacenan localmente en tu dispositivo\"],\"MzPdC2\":[\"Contraseña del servidor (PASS)\"],\"N/hDSy\":[\"Marcar como bot, normalmente 'on' o vacío\"],\"N6j2JH\":[\"Editar \",[\"0\"]],\"N7TQbE\":[\"Invitar usuario a \",[\"channelName\"]],\"NCca/o\":[\"Ingresa apodo predeterminado...\"],\"Nqs6B9\":[\"Muestra todos los medios externos. Cualquier URL puede generar una solicitud a un servidor desconocido.\"],\"Nt+9O7\":[\"Usar WebSocket en lugar de TCP sin procesar\"],\"NxIHzc\":[\"Expulsar usuario\"],\"O+v/cL\":[\"Ver todos los canales del servidor\"],\"OCGpR4\":[\"(heredar)\"],\"ODwSCk\":[\"Enviar un GIF\"],\"OGQ5kK\":[\"Configurar sonidos de notificación y resaltados\"],\"OIPt1Z\":[\"Mostrar u ocultar la barra lateral de miembros\"],\"OKSNq/\":[\"Muy estricto\"],\"ONWvwQ\":[\"Subir\"],\"OVKoQO\":[\"Tu contraseña de cuenta para autenticación\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Llevando IRC al futuro\"],\"OhCpra\":[\"Establece un tema…\"],\"OkltoQ\":[\"Banear a \",[\"username\"],\" por apodo (impide que vuelva a unirse con el mismo nick)\"],\"P+t/Te\":[\"Sin datos adicionales\"],\"P42Wcc\":[\"Seguro\"],\"PD38l0\":[\"Vista previa del avatar del canal\"],\"PD9mEt\":[\"Escribe un mensaje...\"],\"PPqfdA\":[\"Abrir configuración del canal\"],\"PSCjfZ\":[\"El tema que se mostrará para este canal. Todos los usuarios pueden ver el tema.\"],\"PZCecv\":[\"Vista previa de PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 vez\"],\"other\":[[\"c\"],\" veces\"]}]],\"PguS2C\":[\"Agregar máscara de excepción (p. ej., nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Mostrando \",[\"displayedChannelsCount\"],\" de \",[\"0\"],\" canales\"],\"PqhVlJ\":[\"Banear usuario (por hostmask)\"],\"Q+chwU\":[\"Nombre de usuario:\"],\"Q2QY4/\":[\"Eliminar esta invitación\"],\"Q3v9Wc\":[\"Sí, eliminar\"],\"Q6hhn8\":[\"Preferencias\"],\"QF4a34\":[\"Por favor, introduce un nombre de usuario\"],\"QGqSZ2\":[\"Color y formato\"],\"QJQd1J\":[\"Editar perfil\"],\"QSzGDE\":[\"Inactivo\"],\"QUlny5\":[\"¡Bienvenido a \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Leer más\"],\"QuSkCF\":[\"Filtrar canales...\"],\"QwUrDZ\":[\"cambió el tema a: \",[\"topic\"]],\"R0UH07\":[\"Imagen \",[\"0\"],\" de \",[\"1\"]],\"R7SsBE\":[\"Silenciar\"],\"R8rf1X\":[\"Haz clic para establecer el tema\"],\"RArB3D\":[\"fue expulsado de \",[\"channelName\"],\" por \",[\"username\"]],\"RI3cWd\":[\"Descubre el mundo de IRC con ObsidianIRC\"],\"RIfHS5\":[\"Crear un nuevo enlace de invitación\"],\"RMMaN5\":[\"Moderado (+m)\"],\"RWw9Lg\":[\"Cerrar ventana\"],\"RZ2BuZ\":[\"Se requiere verificación para el registro de la cuenta \",[\"account\"],\": \",[\"message\"]],\"RySp6q\":[\"Ocultar comentarios\"],\"S5Togi\":[\"Cargando redes desde tu bouncer…\"],\"SPKQTd\":[\"El apodo es obligatorio\"],\"SPVjfj\":[\"Por defecto será 'sin motivo' si se deja vacío\"],\"SQKPvQ\":[\"Invitar usuario\"],\"STmlpb\":[\"Volver a la lista de redes\"],\"SkZcl+\":[\"Elige un perfil de protección contra flood predefinido. Estos perfiles ofrecen configuraciones de protección equilibradas para diferentes casos de uso.\"],\"Slr+3C\":[\"Mínimo de usuarios\"],\"Spnlre\":[\"Has invitado a \",[\"target\"],\" a unirse a \",[\"channel\"]],\"T/ckN5\":[\"Abrir en el visor\"],\"T91vKp\":[\"Reproducir\"],\"TV2Wdu\":[\"Conoce cómo gestionamos tus datos y protegemos tu privacidad.\"],\"TgFpwD\":[\"Aplicando...\"],\"TkzSFB\":[\"Sin cambios\"],\"TtserG\":[\"Ingresa el nombre real\"],\"Ttz9J1\":[\"Ingresa contraseña...\"],\"Tz0i8g\":[\"Ajustes\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reaccionar\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Aún no has creado ningún enlace de invitación. Usa el formulario de arriba para generar el primero.\"],\"UGT5vp\":[\"Guardar configuración\"],\"UV5hLB\":[\"No se encontraron bans\"],\"Uaj3Nd\":[\"Mensajes de estado\"],\"Ue3uny\":[\"Predeterminado (sin perfil)\"],\"UkARhe\":[\"Normal – Protección estándar\"],\"Umn7Cj\":[\"Aún no hay comentarios. ¡Sé el primero!\"],\"UtUIRh\":[[\"0\"],\" mensajes anteriores\"],\"UwzP+U\":[\"Conexión segura\"],\"V0/A4O\":[\"Propietario del canal\"],\"V0zZWc\":[\"Esto también cerrará la red vinculada siguiente.\"],\"V4qgxE\":[\"Creado hace menos de (min)\"],\"V8yTm6\":[\"Limpiar búsqueda\"],\"VJMMyz\":[\"ObsidianIRC - Llevando IRC al futuro\"],\"VJScHU\":[\"Motivo\"],\"VLsmVV\":[\"Silenciar notificaciones\"],\"VbyRUy\":[\"Comentarios\"],\"Vmx0mQ\":[\"Establecido por:\"],\"VqnIZz\":[\"Ver nuestra política de privacidad y prácticas de datos\"],\"VrMygG\":[\"La longitud mínima es \",[\"0\"]],\"VrnTui\":[\"Tus pronombres, mostrados en tu perfil\"],\"W8E3qn\":[\"Cuenta autenticada\"],\"WAakm9\":[\"Eliminar canal\"],\"WFxTHC\":[\"Agregar máscara de ban (p. ej., nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"El host del servidor es obligatorio\"],\"WRYdXW\":[\"Posición del audio\"],\"WUOH5B\":[\"Ignorar usuario\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Mostrar 1 elemento más\"],\"other\":[\"Mostrar \",[\"1\"],\" elementos más\"]}]],\"WYxRzo\":[\"Crea y gestiona tus enlaces de invitación\"],\"Wd38W1\":[\"Deja el canal en blanco para una invitación genérica a la red. La descripción es solo para tus registros â visible únicamente para ti en esta lista.\"],\"Weq9zb\":[\"General\"],\"Wfj7Sk\":[\"Silenciar o activar los sonidos de notificación\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Perfil de usuario\"],\"X6S3lt\":[\"Buscar ajustes, canales, servidores...\"],\"XEHan5\":[\"Continuar de todos modos\"],\"XI1+wb\":[\"Formato no válido\"],\"XIXeuC\":[\"Mensaje a @\",[\"0\"]],\"XMS+k4\":[\"Iniciar mensaje privado\"],\"XWgxXq\":[\"Álbum\"],\"Xd7+IT\":[\"Desfijar chat privado\"],\"Xm/s+u\":[\"Pantalla\"],\"Xp2n93\":[\"Muestra medios desde el host de archivos de confianza de tu servidor. No se realizan solicitudes a servicios externos.\"],\"XvjC4F\":[\"Guardando...\"],\"Y/qryO\":[\"No se encontraron usuarios que coincidan con tu búsqueda\"],\"YAqRpI\":[\"Registro de cuenta exitoso para \",[\"account\"],\": \",[\"message\"]],\"YEfzvP\":[\"Tema protegido (+t)\"],\"YQOn6a\":[\"Contraer lista de miembros\"],\"YRCoE9\":[\"Operador del canal\"],\"YURQaF\":[\"Ver perfil\"],\"YdBSvr\":[\"Controlar la visualización de medios y contenido externo\"],\"Yj6U3V\":[\"Sin servidor central:\"],\"YjvpGx\":[\"Pronombres\"],\"YqH4l4\":[\"Sin clave\"],\"YyUPpV\":[\"Cuenta:\"],\"ZJSWfw\":[\"Mensaje al desconectarse del servidor\"],\"ZR1dJ4\":[\"Invitaciones\"],\"ZdWg0V\":[\"Abrir en el navegador\"],\"ZhRBbl\":[\"Buscar mensajes…\"],\"Zmcu3y\":[\"Filtros avanzados\"],\"a0bHay\":[\"¿Desconectar <0>\",[\"0\"],\"0>?\"],\"a2/8e5\":[\"Tema establecido hace más de (min)\"],\"aHKcKc\":[\"Página anterior\"],\"aJTbXX\":[\"Contraseña de oper\"],\"aQryQv\":[\"El patrón ya existe\"],\"aW9pLN\":[\"Número máximo de usuarios permitidos en el canal. Deja vacío para no establecer límite.\"],\"ah4fmZ\":[\"También muestra vistas previas de YouTube, Vimeo, SoundCloud y servicios conocidos similares.\"],\"aifXak\":[\"No hay medios en este canal\"],\"ap2zBz\":[\"Relajado\"],\"az8lvo\":[\"Desactivado\"],\"azXSNo\":[\"Expandir lista de miembros\"],\"azdliB\":[\"Iniciar sesión en una cuenta\"],\"b26wlF\":[\"ella/la\"],\"bD/+Ei\":[\"Estricto\"],\"bQ6BJn\":[\"Configura reglas detalladas de protección contra flood. Cada regla especifica qué tipo de actividad monitorear y qué acción tomar cuando se superan los umbrales.\"],\"beV7+y\":[\"El usuario recibirá una invitación para unirse a \",[\"channelName\"],\".\"],\"bk84cH\":[\"Mensaje de ausencia\"],\"bkHdLj\":[\"Agregar servidor IRC\"],\"bmQLn5\":[\"Añadir regla\"],\"bv4cFj\":[\"Transporte\"],\"bwRvnp\":[\"Acción\"],\"c8+EVZ\":[\"Cuenta verificada\"],\"cGYUlD\":[\"No se carga ninguna vista previa de medios.\"],\"cLF98o\":[\"Mostrar comentarios (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"No hay usuarios disponibles\"],\"cSgpoS\":[\"Fijar chat privado\"],\"cXeEKu\":[\"Esto también cerrará las \",[\"0\"],\" redes vinculadas siguientes.\"],\"cde3ce\":[\"Mensaje a <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Copiar salida formateada\"],\"cl/A5J\":[\"¡Bienvenido a \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Eliminar\"],\"coPLXT\":[\"No almacenamos tus comunicaciones IRC en nuestros servidores\"],\"crYH/6\":[\"Reproductor de SoundCloud\"],\"cv5DQb\":[\"sin host configurado\"],\"d3sis4\":[\"Agregar servidor\"],\"d9aN5k\":[\"Eliminar a \",[\"username\"],\" del canal\"],\"dEgA5A\":[\"Cancelar\"],\"dGi1We\":[\"Desfijar esta conversación de mensaje privado\"],\"dJVuyC\":[\"salió de \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"a\"],\"dXqxlh\":[\"<0>⚠️ ¡Riesgo de seguridad!0> Esta conexión puede ser vulnerable a intercepciones o ataques de intermediario.\"],\"da9Q/R\":[\"Modos del canal cambiados\"],\"dhJN3N\":[\"Mostrar comentarios\"],\"dj2xTE\":[\"Descartar notificación\"],\"dpCzmC\":[\"Configuración de protección contra flood\"],\"e9dQpT\":[\"¿Deseas abrir este enlace en una nueva pestaña?\"],\"ePK91l\":[\"Editar\"],\"eYBDuB\":[\"Sube una imagen o proporciona una URL con sustitución opcional de \",[\"size\"],\" para tamaño dinámico\"],\"edBbee\":[\"Banear a \",[\"username\"],\" por hostmask (impide que vuelva a unirse desde la misma IP/host)\"],\"ekfzWq\":[\"Configuración de usuario\"],\"elPDWs\":[\"Personaliza tu experiencia con el cliente IRC\"],\"eu2osY\":[\"<0>💡 Recomendación:0> Continúa solo si confías en este servidor y entiendes los riesgos. Evita compartir información sensible o contraseñas a través de esta conexión.\"],\"euEhbr\":[\"Haz clic para unirte a \",[\"channel\"]],\"ez3vLd\":[\"Activar entrada multilínea\"],\"f0J5Ki\":[\"La comunicación entre servidores puede usar conexiones sin cifrar\"],\"f9BHJk\":[\"Advertir al usuario\"],\"fDOLLd\":[\"No se encontraron canales.\"],\"ffzDkB\":[\"Analíticas anónimas:\"],\"fq1GF9\":[\"Mostrar cuando los usuarios se desconectan del servidor\"],\"gCldcN\":[\"Cambiar color de acento\"],\"gEF57C\":[\"Este servidor solo admite un tipo de conexión\"],\"gJuLUI\":[\"Lista de ignorados\"],\"gNzMrk\":[\"Avatar actual\"],\"gjPWyO\":[\"Ingresa tu apodo...\"],\"gz6UQ3\":[\"Maximizar\"],\"h6/IMX\":[\"Agrega tu primera red\"],\"h6razj\":[\"Excluir máscara de nombre de canal\"],\"hG6jnw\":[\"Sin tema establecido\"],\"hG89Ed\":[\"Imagen\"],\"hYgDIe\":[\"Crear\"],\"hZ6znB\":[\"Puerto\"],\"ha+Bz5\":[\"ej., 100:1440\"],\"he3ygx\":[\"Copiar\"],\"hehnjM\":[\"Cantidad\"],\"hzdLuQ\":[\"Solo los usuarios con Voice o superior pueden hablar\"],\"i0qMbr\":[\"Inicio\"],\"iDNBZe\":[\"Notificaciones\"],\"iH8pgl\":[\"Atrás\"],\"iL9SZg\":[\"Banear usuario (por apodo)\"],\"iNt+3c\":[\"Volver a la imagen\"],\"iQvi+a\":[\"No advertirme sobre la baja seguridad de enlaces para este servidor\"],\"iSLIjg\":[\"Conectar\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Host del servidor\"],\"idD8Ev\":[\"Guardado\"],\"iivqkW\":[\"Conectado desde\"],\"ij+Elv\":[\"Vista previa de imagen\"],\"ilIWp7\":[\"Alternar notificaciones\"],\"iuaqvB\":[\"Usa * como comodín. Ejemplos: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Banear por hostmask\"],\"jA4uoI\":[\"Tema:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Motivo (opcional)\"],\"jUV7CU\":[\"Subir avatar\"],\"jW5Uwh\":[\"Controla cuántos medios externos se cargan. Desactivado / Seguro / Fuentes confiables / Todo el contenido.\"],\"jXzms5\":[\"Opciones de adjunto\"],\"jZlrte\":[\"Color\"],\"jfC/xh\":[\"Contacto\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Cargar mensajes anteriores\"],\"k3ID0F\":[\"Filtrar miembros…\"],\"k65gsE\":[\"Ver en detalle\"],\"k7Zgob\":[\"Cancelar conexión\"],\"kAVx5h\":[\"No se encontraron invitaciones\"],\"kCLEPU\":[\"Conectado a\"],\"kF5LKb\":[\"Patrones ignorados:\"],\"kGeOx/\":[\"Unirse a \",[\"0\"]],\"kITKr8\":[\"Cargando modos del canal...\"],\"kPpPsw\":[\"Eres un IRC Operator\"],\"kWJmRL\":[\"Tú\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Copiar JSON\"],\"krViRy\":[\"Clic para copiar como JSON\"],\"ks71ra\":[\"Excepciones\"],\"kw4lRv\":[\"Medio operador del canal\"],\"kxgIRq\":[\"Selecciona o agrega un canal para comenzar.\"],\"ky6dWe\":[\"Vista previa del avatar\"],\"l+GxCv\":[\"Cargando canales...\"],\"l+IUVW\":[\"Verificación de cuenta exitosa para \",[\"account\"],\": \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"se reconectó\"],\"other\":[\"se reconectó \",[\"reconnectCount\"],\" veces\"]}]],\"l1l8sj\":[\"hace \",[\"0\"],\"d\"],\"l5NhnV\":[\"#canal (opcional)\"],\"l5jmzx\":[[\"0\"],\" y \",[\"1\"],\" están escribiendo...\"],\"lCF0wC\":[\"Actualizar\"],\"lHy8N5\":[\"Cargando más canales...\"],\"lasgrr\":[\"usado\"],\"lbpf14\":[\"Unirse a \",[\"value\"]],\"lfFsZ4\":[\"Canales\"],\"lkNdiH\":[\"Nombre de cuenta\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Subir imagen\"],\"loQxaJ\":[\"He vuelto\"],\"lvfaxv\":[\"INICIO\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Agregar\"],\"m8flAk\":[\"Vista previa (aún no subido)\"],\"mEPxTp\":[\"<0>⚠️ ¡Ten cuidado!0> Solo abre enlaces de fuentes de confianza. Los enlaces maliciosos pueden comprometer tu seguridad o privacidad.\"],\"mHGdhG\":[\"Información del servidor\"],\"mHS8lb\":[\"Mensaje en #\",[\"0\"]],\"mMYBD9\":[\"Amplio – Ámbito de protección más amplio\"],\"mTGsPd\":[\"Tema del canal\"],\"mU8j6O\":[\"Sin mensajes externos (+n)\"],\"mZp8FL\":[\"Retorno automático a línea única\"],\"mdQu8G\":[\"TuApodo\"],\"miSSBQ\":[\"Comentarios (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"El usuario está autenticado\"],\"mwtcGl\":[\"Cerrar comentarios\"],\"myL0MR\":[\"¿Eliminar esta red?\"],\"mzI/c+\":[\"Descargar\"],\"n3fGRk\":[\"establecido por \",[\"0\"]],\"n5+j9l\":[\"p. ej. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Relajado – Protección menos agresiva\"],\"nNflMD\":[\"Salir del canal\"],\"nPXkBi\":[\"Cargando datos WHOIS...\"],\"nQnxxF\":[\"Mensaje en #\",[\"0\"],\" (Mayús+Intro para nueva línea)\"],\"nWMRxa\":[\"Desfijar\"],\"nkC032\":[\"Sin perfil de flood\"],\"o69z4d\":[\"Enviar un mensaje de advertencia a \",[\"username\"]],\"o9ylQi\":[\"Busca GIFs para empezar\"],\"oFGkER\":[\"Avisos del servidor\"],\"oOi11l\":[\"Ir al final\"],\"oPYIL5\":[\"red\"],\"oQEzQR\":[\"Nuevo mensaje directo\"],\"oXOSPE\":[\"En línea\"],\"oal760\":[\"Son posibles ataques de intermediario en los enlaces del servidor\"],\"oeqmmJ\":[\"Fuentes de confianza\"],\"optX0N\":[\"hace \",[\"0\"],\"h\"],\"ovBPCi\":[\"Predeterminado\"],\"p0Z69r\":[\"El patrón no puede estar vacío\"],\"p1KgtK\":[\"Error al cargar el audio\"],\"p59pEv\":[\"Detalles adicionales\"],\"p7sRI6\":[\"Avisar a otros cuando estás escribiendo\"],\"pBm1od\":[\"Canal secreto\"],\"pNmiXx\":[\"Tu apodo predeterminado para todos los servidores\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Contraseña de la cuenta\"],\"peNE68\":[\"Permanente\"],\"plhHQt\":[\"Sin datos\"],\"pm6+q5\":[\"Advertencia de seguridad\"],\"pn5qSs\":[\"Información adicional\"],\"q0cR4S\":[\"ahora se llama **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"El canal no aparecerá en los comandos LIST ni NAMES\"],\"qLpTm/\":[\"Eliminar reacción \",[\"emoji\"]],\"qVkGWK\":[\"Fijar\"],\"qY8wNa\":[\"Página de inicio\"],\"qb0xJ7\":[\"Usa comodines: * coincide con cualquier secuencia, ? coincide con cualquier carácter individual. Ejemplos: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Clave del canal (+k)\"],\"qtoOYG\":[\"Sin límite\"],\"r1W2AS\":[\"Imagen del servidor de archivos\"],\"rIPR2O\":[\"Tema establecido hace menos de (min)\"],\"rMMSYo\":[\"La longitud máxima es \",[\"0\"]],\"rWtzQe\":[\"La red se dividió y se reconectó. ✅\"],\"rYG2u6\":[\"Por favor espera...\"],\"rdUucN\":[\"Vista previa\"],\"rjGI/Q\":[\"Privacidad\"],\"rk8iDX\":[\"Cargando GIFs...\"],\"rn6SBY\":[\"Activar sonido\"],\"s/UKqq\":[\"Fue expulsado del canal\"],\"s7oqXR\":[\"Selecciona una red\"],\"s8cATI\":[\"se unió a \",[\"channelName\"]],\"sCO9ue\":[\"La conexión a <0>\",[\"serverName\"],\"0> presenta los siguientes problemas de seguridad:\"],\"sGH11W\":[\"Servidor\"],\"sHI1H+\":[\"ahora se llama **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" te ha invitado a unirte a \",[\"channel\"]],\"sUBSbK\":[\"Aún no hay redes ascendentes.\"],\"sby+1/\":[\"Haz clic para copiar\"],\"sfN25C\":[\"Tu nombre real o completo\"],\"sliuzR\":[\"Abrir enlace\"],\"sqrO9R\":[\"Menciones personalizadas\"],\"sr6RdJ\":[\"Multilínea con Shift+Enter\"],\"swrCpB\":[\"El canal ha sido renombrado de \",[\"oldName\"],\" a \",[\"newName\"],\" por \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Avanzado\"],\"t/YqKh\":[\"Eliminar\"],\"t47eHD\":[\"Tu identificador único en este servidor\"],\"tAkAh0\":[\"URL con sustitución opcional de \",[\"size\"],\" para tamaño dinámico. Ejemplo: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Mostrar u ocultar la barra lateral de canales\"],\"tfDRzk\":[\"Guardar\"],\"tiBsJk\":[\"salió de \",[\"channelName\"]],\"tt4/UD\":[\"salió (\",[\"reason\"],\")\"],\"u0TcnO\":[\"El apodo {nick} ya está en uso, reintentando con {newNick}\"],\"u0a8B4\":[\"Autenticarse como operador IRC para acceso administrativo\"],\"u0rWFU\":[\"Creado hace más de (min)\"],\"u72w3t\":[\"Usuarios y patrones a ignorar\"],\"u7jc2L\":[\"salió\"],\"uAQUqI\":[\"Estado\"],\"uB85T3\":[\"Error al guardar: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"Servidores IRC:\"],\"ukyW4o\":[\"Tus enlaces de invitación\"],\"usSSr/\":[\"Nivel de zoom\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Shift+Enter para nuevas líneas (Enter envía)\"],\"vERlcd\":[\"Perfil\"],\"vK0RL8\":[\"Sin tema\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Idioma\"],\"vaHYxN\":[\"Nombre real\"],\"vhjbKr\":[\"Ausente\"],\"w/nogd\":[[\"0\"],\" red\",[\"1\"],\" — elige una para unirte\"],\"w4NYox\":[\"cliente \",[\"title\"]],\"w8xQRx\":[\"Valor no válido\"],\"wFjjxZ\":[\"fue expulsado de \",[\"channelName\"],\" por \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"No se encontraron excepciones de ban\"],\"wPrGnM\":[\"Administrador del canal\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Mostrar cuando los usuarios entran o salen de canales\"],\"whqZ9r\":[\"Palabras o frases adicionales para resaltar\"],\"wm7RV4\":[\"Sonido de notificación\"],\"wz/Yoq\":[\"Tus mensajes podrían ser interceptados al retransmitirse entre servidores\"],\"x3+y8b\":[\"Esta cantidad de personas se ha registrado mediante este enlace\"],\"xCJdfg\":[\"Limpiar\"],\"xOTzt5\":[\"ahora mismo\"],\"xUHRTR\":[\"Autenticarse automáticamente como operador al conectar\"],\"xWHwwQ\":[\"Bans\"],\"xYilR2\":[\"Medios\"],\"xbi8D6\":[\"Este servidor no admite enlaces de invitación (no se anuncia la capacidad<0>obby.world/invitation0>). Puedes seguir chateando con normalidad; este panel es para redes basadas en obbyircd.\"],\"xceQrO\":[\"Solo se admiten websockets seguros\"],\"xdtXa+\":[\"nombre-del-canal\"],\"xfXC7q\":[\"Canales de texto\"],\"xlCYOE\":[\"Cargando más mensajes...\"],\"xlhswE\":[\"El valor mínimo es \",[\"0\"]],\"xq97Ci\":[\"Agregar una palabra o frase...\"],\"xuRqRq\":[\"Límite de usuarios (+l)\"],\"xwF+7J\":[[\"0\"],\" está escribiendo...\"],\"y1eoq1\":[\"Copiar enlace\"],\"yJztBY\":[\"Eliminar red\"],\"yNeucF\":[\"Este servidor no admite metadatos de perfil extendido (extensión IRCv3 METADATA). Los campos adicionales como avatar, nombre a mostrar y estado no están disponibles.\"],\"yPlrca\":[\"Avatar del canal\"],\"yQE2r9\":[\"Cargando\"],\"ySU+JY\":[\"tu@correo.com\"],\"yTX1Rt\":[\"Nombre de usuario Oper\"],\"yYOzWD\":[\"registros\"],\"yfx9Re\":[\"Contraseña de operador IRC\"],\"ygCKqB\":[\"Detener\"],\"ymDxJx\":[\"Nombre de usuario de operador IRC\"],\"yrpRsQ\":[\"Ordenar por nombre\"],\"yz7wBu\":[\"Cerrar\"],\"zJw+jA\":[\"establece modo: \",[\"0\"]],\"zbymaY\":[\"hace \",[\"0\"],\"m\"],\"zebeLu\":[\"Ingresa el nombre de usuario de oper\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/es/messages.po b/src/locales/es/messages.po
index b5732ddb..4ee930ac 100644
--- a/src/locales/es/messages.po
+++ b/src/locales/es/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - Llevando IRC al futuro"
msgid "— open in viewer"
msgstr "— abrir en visor"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(heredar)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(sin cambios)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} y {1} están escribiendo..."
msgid "{0} is typing..."
msgstr "{0} está escribiendo..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} red{1} — elige una para unirte"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -205,6 +221,12 @@ msgstr "Agregar máscara de invitación (p. ej., nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "Agregar servidor IRC"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Agregar red"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Añadir regla"
msgid "Add Server"
msgstr "Agregar servidor"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Agrega tu primera red"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Detalles adicionales"
@@ -377,6 +403,10 @@ msgstr "Atrás"
msgid "Back to image"
msgstr "Volver a la imagen"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Volver a la lista de redes"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "Banear a {username} por hostmask (impide que vuelva a unirse desde la misma IP/host)"
@@ -424,6 +454,10 @@ msgstr "Ver todos los canales del servidor"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Cancelar conexión"
msgid "Cancel reply"
msgstr "Cancelar respuesta"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Cambiar color de acento"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Cambiar el nombre del canal (solo operadores)"
@@ -606,6 +645,8 @@ msgstr "Límite de usuarios (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -666,9 +707,10 @@ msgstr "Configurar sonidos de notificación y resaltados"
#: src/components/ui/InvitationsPanel.tsx
msgid "Confirm?"
-msgstr "¿Confirmar?"
+msgstr "¿Confirmar?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Conectar"
@@ -743,11 +785,11 @@ msgstr "Crear"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "Crear un nuevo enlace de invitación"
+msgstr "Crear un nuevo enlace de invitación"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "Crea y gestiona tus enlaces de invitación"
+msgstr "Crea y gestiona tus enlaces de invitación"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,27 +856,52 @@ msgstr "Eliminar canal"
msgid "Delete message"
msgstr "Eliminar mensaje"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Eliminar red"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Eliminar chat privado"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "Eliminar esta invitación"
+msgstr "Eliminar esta invitación"
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "¿Eliminar este mensaje? Esta acción no se puede deshacer."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "¿Eliminar esta red?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "Descripción (opcional, p. ej. \"Probadores beta T3\")"
+msgstr "Descripción (opcional, p. ej. \"Probadores beta T3\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Desconectar"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "¿Desconectar <0>{0}0>?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "¿Desconectar del bouncer soju?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "¿Desconectar red?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Explorar"
@@ -893,18 +960,29 @@ msgstr "Descargar"
msgid "Drop files to upload"
msgstr "Suelta archivos para subirlos"
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "p. ej. <0>wss://host:port/socket0>"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "ej., 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Editar"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "Editar {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Editar perfil"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "INICIO"
msgid "Homepage"
msgstr "Página de inicio"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Host"
@@ -1323,7 +1402,7 @@ msgstr "Salir del canal"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Deja el canal en blanco para una invitación genérica a la red. La descripción es solo para tus registros — visible únicamente para ti en esta lista."
+msgstr "Deja el canal en blanco para una invitación genérica a la red. La descripción es solo para tus registros â visible únicamente para ti en esta lista."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "Abandonó el canal"
msgid "Let others know when you are typing"
msgstr "Avisar a otros cuando estás escribiendo"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Vista previa del enlace"
@@ -1375,6 +1458,10 @@ msgstr "Cargando GIFs..."
msgid "Loading more channels..."
msgstr "Cargando más canales..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Cargando redes desde tu bouncer…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "Cargando datos WHOIS..."
@@ -1565,10 +1652,21 @@ msgstr "Nombre:"
msgid "network"
msgstr "red"
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Red vinculada a través del bouncer soju"
+
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Nombre de red"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Redes en {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Nuevo mensaje directo"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (ej., spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Ningún archivo elegido"
msgid "No flood profile"
msgstr "Sin perfil de flood"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "sin host configurado"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "No se encontraron invitaciones"
@@ -1672,7 +1775,7 @@ msgstr "No se carga ninguna vista previa de medios."
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "Aún no se ha capturado tráfico IRC sin procesar. Prueba a conectarte o enviar un mensaje."
+msgstr "Aún no se ha capturado tráfico IRC sin procesar. Prueba a conectarte o enviar un mensaje."
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "No se encontraron resultados"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "No hay ningún servidor seleccionado. Elige primero un servidor en la barra lateral; los enlaces de invitación se gestionan por servidor."
+msgstr "No hay ningún servidor seleccionado. Elige primero un servidor en la barra lateral; los enlaces de invitación se gestionan por servidor."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "Sin tema establecido"
msgid "No unread mentions or messages"
msgstr "Sin menciones ni mensajes sin leer"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Aún no hay redes ascendentes."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "No hay usuarios disponibles"
@@ -1784,6 +1891,10 @@ msgstr "¡Vaya! ¡División de red! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Abrir"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Abrir configuración del canal"
@@ -1887,6 +1998,10 @@ msgstr "Fijar chat privado"
msgid "Pin this private message conversation"
msgstr "Fijar esta conversación de mensaje privado"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Texto plano"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "MP al usuario"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Puerto"
@@ -2006,6 +2122,7 @@ msgstr "reaccionó a este mensaje"
msgid "Read more"
msgstr "Leer más"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Motivo"
msgid "Reason (optional)"
msgstr "Motivo (opcional)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Reconectar al servidor"
@@ -2095,6 +2213,7 @@ msgstr "Reglas"
msgid "Safe"
msgstr "Seguro"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Buscar posición"
msgid "Select a channel"
msgstr "Selecciona un canal"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Selecciona una red"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Seleccionar miembro"
@@ -2276,6 +2399,10 @@ msgstr "Los operadores del servidor en la red podrían leer tus mensajes"
msgid "Server Password"
msgstr "Contraseña del servidor"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Contraseña del servidor (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "La comunicación entre servidores puede usar conexiones sin cifrar"
@@ -2380,6 +2507,12 @@ msgstr "Conectado desde"
msgid "Software:"
msgstr "Software:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "bouncer soju (control)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Ordenar por nombre"
@@ -2459,18 +2592,31 @@ msgstr "Esta imagen ha expirado"
msgid "This many people registered through this link"
msgstr "Esta cantidad de personas se ha registrado mediante este enlace"
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Esto elimina la red de tu bouncer soju. Para usarla de nuevo, deberás añadirla otra vez."
+
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
msgstr "Este servidor no admite metadatos de perfil extendido (extensión IRCv3 METADATA). Los campos adicionales como avatar, nombre a mostrar y estado no están disponibles."
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Este servidor no admite enlaces de invitación (no se anuncia la capacidad<0>obby.world/invitation0>). Puedes seguir chateando con normalidad; este panel es para redes basadas en obbyircd."
+msgstr "Este servidor no admite enlaces de invitación (no se anuncia la capacidad<0>obby.world/invitation0>). Puedes seguir chateando con normalidad; este panel es para redes basadas en obbyircd."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Este servidor solo admite un tipo de conexión"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Esto también cerrará las {0} redes vinculadas siguientes."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Esto también cerrará la red vinculada siguiente."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Tiempo (min)"
@@ -2479,6 +2625,10 @@ msgstr "Tiempo (min)"
msgid "Time Window (seconds)"
msgstr "Ventana de tiempo (segundos)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Tema:"
msgid "Total: {0}"
msgstr "Total: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Transporte"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Fuentes de confianza"
@@ -2641,6 +2795,7 @@ msgstr "Perfil de usuario"
msgid "User Settings"
msgstr "Configuración de usuario"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Amplio – Ámbito de protección más amplio"
msgid "Will default to 'no reason' if left empty"
msgstr "Por defecto será 'sin motivo' si se deja vacío"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Sí, eliminar"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "Tienes cambios sin guardar. ¿Seguro que deseas cerrar sin guardar?"
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "Aún no has creado ningún enlace de invitación. Usa el formulario de arriba para generar el primero."
+msgstr "Aún no has creado ningún enlace de invitación. Usa el formulario de arriba para generar el primero."
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "Has invitado a {target} a unirse a {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Estás conectado a <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Tu contraseña de cuenta para autenticación"
@@ -2822,13 +2986,17 @@ msgstr "Tu contraseña de cuenta para autenticación"
msgid "Your account username for authentication"
msgstr "Tu nombre de usuario de cuenta para autenticación"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Tu bouncer aún no tiene ninguna red. Agrega una para empezar."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Tu apodo predeterminado para todos los servidores"
#: src/components/ui/InvitationsPanel.tsx
msgid "Your invite links"
-msgstr "Tus enlaces de invitación"
+msgstr "Tus enlaces de invitación"
#: src/components/ui/UserSettings.tsx
msgid "Your messages and settings are stored locally on your device"
diff --git a/src/locales/fi/messages.mjs b/src/locales/fi/messages.mjs
index c9947bc5..1271f517 100644
--- a/src/locales/fi/messages.mjs
+++ b/src/locales/fi/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Virheellinen mallimuoto. Käytä nick!käyttäjä@isäntä-muotoa (jokerimerkit * sallittu)\"],\"+6NQQA\":[\"Yleinen tukikanava\"],\"+6NyRG\":[\"Asiakasohjelma\"],\"+K0AvT\":[\"Katkaise yhteys\"],\"+cyFdH\":[\"Oletusviesti poissaoloilmoitukselle\"],\"+mVPqU\":[\"Näytä Markdown-muotoilu viesteissä\"],\"+vqCJH\":[\"Tilin käyttäjänimi tunnistautumista varten\"],\"+yPBXI\":[\"Valitse tiedosto\"],\"+zy2Nq\":[\"Tyyppi\"],\"/09cao\":[\"Heikko linkkiturvallisuus (taso \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Kanavan ulkopuoliset käyttäjät eivät voi lähettää viestejä sille\"],\"/4C8U0\":[\"Kopioi kaikki\"],\"/6BzZF\":[\"Näytä/piilota jäsenlista\"],\"/AkXyp\":[\"Vahvista?\"],\"/TNOPk\":[\"Käyttäjä on poissa\"],\"/XQgft\":[\"Selaa\"],\"/cF7Rs\":[\"Äänenvoimakkuus\"],\"/dqduX\":[\"Seuraava sivu\"],\"/fc3q4\":[\"Kaikki sisältö\"],\"/kISDh\":[\"Ota ilmoitusäänet käyttöön\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Ääni\"],\"/rfkZe\":[\"Toista ääniä maininnoista ja viesteistä\"],\"0/0ZGA\":[\"Kanavan nimimaski\"],\"0D6j7U\":[\"Lue lisää mukautetuista säännöistä →\"],\"0XsHcR\":[\"Poista käyttäjä\"],\"0ZpE//\":[\"Lajittele käyttäjämäärän mukaan\"],\"0bEPwz\":[\"Aseta poissa\"],\"0dGkPt\":[\"Laajenna kanavaluettelo\"],\"0gS7M5\":[\"Näyttönimi\"],\"0kS+M8\":[\"EsimerkKi\"],\"0rgoY7\":[\"Yhdistä vain valitsemiisi palvelimiin\"],\"0wdd7X\":[\"Liity\"],\"0wkVYx\":[\"Yksityisviestit\"],\"111uHX\":[\"Linkin esikatselu\"],\"196EG4\":[\"Poista yksityiskeskustelu\"],\"1DSr1i\":[\"Rekisteröidy tilille\"],\"1O/24y\":[\"Näytä/piilota kanavaluettelo\"],\"1VPJJ2\":[\"Ulkoisen linkin varoitus\"],\"1ZC/dv\":[\"Ei lukemattomia mainintoja tai viestejä\"],\"1pO1zi\":[\"Palvelimen nimi on pakollinen\"],\"1uwfzQ\":[\"Näytä kanavan aihe\"],\"268g7c\":[\"Syötä näyttönimi\"],\"2F9+AZ\":[\"Raakaa IRC-liikennettä ei ole vielä tallennettu. Yritä yhdistää tai lähettää viesti.\"],\"2FOFq1\":[\"Verkon palvelinoperaattorit voisivat mahdollisesti lukea viestisi\"],\"2FYpfJ\":[\"Lisää\"],\"2HF1Y2\":[[\"inviter\"],\" kutsui \",[\"target\"],\" liittymään kanavalle \",[\"channel\"]],\"2I70QL\":[\"Näytä käyttäjäprofiilitiedot\"],\"2QYdmE\":[\"Käyttäjät:\"],\"2QpEjG\":[\"poistui\"],\"2YE223\":[\"Viesti #\",[\"0\"],\" (Enter = uusi rivi, Shift+Enter = lähetä)\"],\"2bimFY\":[\"Käytä palvelimen salasanaa\"],\"2iTmdZ\":[\"Paikallinen tallennustila:\"],\"2odkwe\":[\"Tiukka – aggressiivisempi suojaus\"],\"2uDhbA\":[\"Syötä kutsuttavan käyttäjänimi\"],\"2ygf/L\":[\"← Takaisin\"],\"2zEgxj\":[\"Hae GIF-kuvia...\"],\"3RdPhl\":[\"Nimeä kanava uudelleen\"],\"3THokf\":[\"Voice-käyttäjä\"],\"3TSz9S\":[\"Pienennä\"],\"3jBDvM\":[\"Kanavan näyttönimi\"],\"3ryuFU\":[\"Valinnaiset kaatumisraportit sovelluksen parantamiseksi\"],\"3uBF/8\":[\"Sulje katseluohjelma\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Syötä tilin nimi...\"],\"4/Rr0R\":[\"Kutsu käyttäjä nykyiselle kanavalle\"],\"4EZrJN\":[\"Säännöt\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Tulvasuojausprofiili (+F)\"],\"4RZQRK\":[\"Mitä olet tekemässä?\"],\"4hfTrB\":[\"Nimimerkki\"],\"4n99LO\":[\"Jo kanavalla \",[\"0\"]],\"4t6vMV\":[\"Vaihda automaattisesti yksiriviseen lyhyille viesteille\"],\"4vsHmf\":[\"Aika (min)\"],\"5+INAX\":[\"Korosta viestit, joissa mainitaan sinut\"],\"5R5Pv/\":[\"Oper-nimi\"],\"678PKt\":[\"Verkon nimi\"],\"6Aih4U\":[\"Poissa verkosta\"],\"6CO3WE\":[\"Salasana vaaditaan kanavalle liittymiseen. Jätä tyhjäksi poistaaksesi avaimen.\"],\"6HhMs3\":[\"Lähtöviesti\"],\"6V3Ea3\":[\"Kopioitu\"],\"6lGV3K\":[\"Näytä vähemmän\"],\"6yFOEi\":[\"Syötä oper-salasana...\"],\"7+IHTZ\":[\"Ei valittua tiedostoa\"],\"73hrRi\":[\"nick!käyttäjä@isäntä (esim. spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Lähetä yksityisviesti\"],\"7U1W7c\":[\"Hyvin löysä\"],\"7Y1YQj\":[\"Oikea nimi:\"],\"7YHArF\":[\"— avaa katseluohjelmassa\"],\"7fjnVl\":[\"Hae käyttäjiä...\"],\"7jL88x\":[\"Poistetaanko tämä viesti? Toimintoa ei voi kumota.\"],\"7nGhhM\":[\"Mitä ajattelet?\"],\"7sEpu1\":[\"Jäsenet — \",[\"0\"]],\"7sNhEz\":[\"Käyttäjänimi\"],\"8H0Q+x\":[\"Lue lisää profiileista →\"],\"8Phu0A\":[\"Näytä kun käyttäjät vaihtavat nimimerkkinsä\"],\"8XTG9e\":[\"Syötä oper-salasana\"],\"8XsV2J\":[\"Yritä lähettää uudelleen\"],\"8ZsakT\":[\"Salasana\"],\"8kR84m\":[\"Olet avaamassa ulkoista linkkiä:\"],\"8lCgih\":[\"Poista sääntö\"],\"8o3dPc\":[\"Pudota tiedostot lähettääksesi\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"liittyi\"],\"other\":[\"liittyi \",[\"joinCount\"],\" kertaa\"]}]],\"9BMLnJ\":[\"Yhdistä uudelleen palvelimeen\"],\"9OEgyT\":[\"Lisää reaktio\"],\"9PQ8m2\":[\"G-Line (maailmanlaajuinen esto)\"],\"9Qs99X\":[\"Sähköposti:\"],\"9QupBP\":[\"Poista malli\"],\"9bG48P\":[\"Lähetetään\"],\"9f5f0u\":[\"Yksityisyyteen liittyviä kysymyksiä? Ota yhteyttä:\"],\"9unqs3\":[\"Poissa:\"],\"9v3hwv\":[\"Palvelimia ei löydetty.\"],\"9zb2WA\":[\"Yhdistetään\"],\"A1taO8\":[\"Hae\"],\"A2adVi\":[\"Lähetä kirjoitusilmoituksia\"],\"A9Rhec\":[\"Kanavan nimi\"],\"AWOSPo\":[\"Lähennä\"],\"AXSpEQ\":[\"Oper yhdistäessä\"],\"AeXO77\":[\"Tili\"],\"AhNP40\":[\"Selaa\"],\"Ai2U7L\":[\"Isäntä\"],\"AjBQnf\":[\"Vaihtoi nimimerkin\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Peruuta vastaus\"],\"ApSx0O\":[\"Löydettiin \",[\"0\"],\" viestiä, jotka vastaavat \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Tuloksia ei löydetty\"],\"AyNqAB\":[\"Näytä kaikki palvelintapahtumat chatissa\"],\"B/QqGw\":[\"Poissa näppäimistöltä\"],\"B8AaMI\":[\"Tämä kenttä on pakollinen\"],\"BA2c49\":[\"Palvelin ei tue kehittynyttä LIST-suodatusta\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" ja \",[\"3\"],\" muuta kirjoittavat...\"],\"BGul2A\":[\"Sinulla on tallentamattomia muutoksia. Haluatko varmasti sulkea tallentamatta?\"],\"BIf9fi\":[\"Tilaviestisi\"],\"BPm98R\":[\"Yhtään palvelinta ei ole valittu. Valitse ensin palvelin sivupalkista; kutsulinkkejä hallitaan palvelinkohtaisesti.\"],\"BZz3md\":[\"Henkilökohtainen verkkosivustosi\"],\"Bgm/H7\":[\"Salli monirivisyöttö\"],\"BiQIl1\":[\"Kiinnitä tämä yksityisviesteistä\"],\"BlNZZ2\":[\"Siirry viestiin napsauttamalla\"],\"Bowq3c\":[\"Vain operaattorit voivat muuttaa kanavan aihetta\"],\"Btozzp\":[\"Tämä kuva on vanhentunut\"],\"Bycfjm\":[\"Yhteensä: \",[\"0\"]],\"C6IBQc\":[\"Kopioi koko JSON\"],\"C9L9wL\":[\"Tiedonkeruu\"],\"CDq4wC\":[\"Moderoi käyttäjää\"],\"CHVRxG\":[\"Viesti @\",[\"0\"],\" (Shift+Enter = uusi rivi)\"],\"CN9zdR\":[\"Oper-nimi ja salasana ovat pakollisia\"],\"CW3sYa\":[\"Lisää reaktio \",[\"emoji\"]],\"CaAkqd\":[\"Näytä poistumisviestit\"],\"CbvaYj\":[\"Estä nimimerkin perusteella\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Valitse kanava\"],\"CsekCi\":[\"Normaali\"],\"D+NlUC\":[\"Järjestelmä\"],\"D28t6+\":[\"liittyi ja poistui\"],\"DB8zMK\":[\"Käytä\"],\"DBcWHr\":[\"Mukautettu ilmoitusäänitiedosto\"],\"DTy9Xw\":[\"Median esikatselut\"],\"Dj4pSr\":[\"Valitse turvallinen salasana\"],\"Du+zn+\":[\"Haetaan...\"],\"Du2T2f\":[\"Asetusta ei löydetty\"],\"DwsSVQ\":[\"Käytä suodattimet ja päivitä\"],\"E3W/zd\":[\"Oletusnimimerkki\"],\"E6nRW7\":[\"Kopioi URL\"],\"E703RG\":[\"Tilat:\"],\"EAeu1Z\":[\"Lähetä kutsu\"],\"EFKJQT\":[\"Asetus\"],\"EGPQBv\":[\"Mukautetut tulvasäännöt (+f)\"],\"ELik0r\":[\"Lue koko tietosuojakäytäntö\"],\"EPbeC2\":[\"Näytä tai muokkaa kanavan aihetta\"],\"EQCDNT\":[\"Syötä oper-käyttäjätunnus...\"],\"EUvulZ\":[\"Löydettiin 1 viesti, joka vastaa \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Seuraava kuva\"],\"EdQY6l\":[\"Ei mitään\"],\"EnqLYU\":[\"Hae palvelimia...\"],\"F0OKMc\":[\"Muokkaa palvelinta\"],\"F6Int2\":[\"Ota korostukset käyttöön\"],\"FDoLyE\":[\"Enimmäiskäyttäjämäärä\"],\"FUU/hZ\":[\"Hallitsee, kuinka paljon ulkoista mediaa ladataan chatissa.\"],\"Fdp03t\":[\"päälle\"],\"FfPWR0\":[\"Ikkuna\"],\"FjkaiT\":[\"Loitonna\"],\"FlqOE9\":[\"Mitä tämä tarkoittaa:\"],\"FolHNl\":[\"Hallinnoi tiliäsi ja tunnistautumista\"],\"Fp2Dif\":[\"Poistui palvelimelta\"],\"G5KmCc\":[\"GZ-Line (maailmanlaajuinen Z-Line)\"],\"GDs0lz\":[\"<0>Riski:0> Arkaluonteiset tiedot (viestit, yksityiskeskustelut, tunnistautumistiedot) voisivat paljastua verkon ylläpitäjille tai hyökkääjille, jotka sijaitsevat IRC-palvelimien välissä.\"],\"GR+2I3\":[\"Lisää kutsumask (esim. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Sulje irrotettu palvelintiedotteet-näkymä\"],\"GdhD7H\":[\"Vahvista napsauttamalla uudelleen\"],\"GlHnXw\":[\"Nimen vaihto epäonnistui: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Esikatselu:\"],\"GtmO8/\":[\"lähettäjä\"],\"GtuHUQ\":[\"Nimeä tämä kanava uudelleen palvelimella. Kaikki käyttäjät näkevät uuden nimen.\"],\"GuGfFX\":[\"Näytä/piilota haku\"],\"GxkJXS\":[\"Ladataan...\"],\"GzbwnK\":[\"Liittyi kanavalle\"],\"GzsUDB\":[\"Laajennettu profiili\"],\"H/PnT8\":[\"Lisää emoji\"],\"H6Izzl\":[\"Suosikki värikoodisi\"],\"H9jIv+\":[\"Näytä liittymiset/poistumiset\"],\"HAKBY9\":[\"Lataa tiedostoja\"],\"HdE1If\":[\"Kanava\"],\"Hk4AW9\":[\"Suosikki näyttönimesi\"],\"HmHDk7\":[\"Valitse jäsen\"],\"HrQzPU\":[\"Kanavat verkossa \",[\"networkName\"]],\"I2tXQ5\":[\"Viesti @\",[\"0\"],\" (Enter = uusi rivi, Shift+Enter = lähetä)\"],\"I6bw/h\":[\"Estä käyttäjä\"],\"I92Z+b\":[\"Ota ilmoitukset käyttöön\"],\"I9D72S\":[\"Haluatko varmasti poistaa tämän viestin? Tätä toimintoa ei voi kumota.\"],\"IA+1wo\":[\"Näytä kun käyttäjiä poistetaan kanavilta\"],\"IDwkJx\":[\"IRC-operaattori\"],\"ILlU+s\":[\"Tiedot:\"],\"IUwGEM\":[\"Tallenna muutokset\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" ja \",[\"2\"],\" kirjoittavat...\"],\"IgrLD/\":[\"Tauko\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Vastaa\"],\"IoHMnl\":[\"Enimmäisarvo on \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Yhdistetään...\"],\"J5T9NW\":[\"Käyttäjätiedot\"],\"J8Y5+z\":[\"Hups! Verkon jako! ⚠️\"],\"JBHkBA\":[\"Poistui kanavalta\"],\"JCwL0Q\":[\"Syötä syy (valinnainen)\"],\"JFciKP\":[\"Vaihda\"],\"JXGkhG\":[\"Muuta kanavan nimeä (vain operaattorit)\"],\"JcD7qf\":[\"Lisää toimintoja\"],\"JdkA+c\":[\"Salainen (+s)\"],\"Jmu12l\":[\"Palvelimen kanavat\"],\"JvQ++s\":[\"Ota Markdown käyttöön\"],\"K2jwh/\":[\"WHOIS-tietoja ei saatavilla\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Poista viesti\"],\"KKBlUU\":[\"Upotus\"],\"KM0pLb\":[\"Tervetuloa kanavalle!\"],\"KR6W2h\":[\"Poista esto käyttäjältä\"],\"KV+Bi1\":[\"Vain kutsulla (+i)\"],\"KdCtwE\":[\"Kuinka monta sekuntia tulvatoimintaa seurataan ennen laskurien nollaamista\"],\"Kkezga\":[\"Palvelimen salasana\"],\"KsiQ/8\":[\"Käyttäjät täytyy kutsua kanavalle\"],\"L+gB/D\":[\"Kanavan tiedot\"],\"LC1a7n\":[\"IRC-palvelin on ilmoittanut, että sen palvelimien väliset linkit ovat heikosti suojattuja. Tämä tarkoittaa, että verkossa välitettäviä viestejäsi ei välttämättä salata asianmukaisesti tai SSL/TLS-sertifikaatteja ei tarkisteta oikein.\"],\"LNfLR5\":[\"Näytä poistamiset\"],\"LQb0W/\":[\"Näytä kaikki tapahtumat\"],\"LU7/yA\":[\"Vaihtoehtoinen nimi käyttöliittymässä näytettäväksi. Voi sisältää välilyöntejä, emojeja ja erikoismerkkejä. Todellista kanavan nimeä (\",[\"channelName\"],\") käytetään edelleen IRC-komennoissa.\"],\"LUb9O7\":[\"Kelvollinen palvelinportti on pakollinen\"],\"LV4fT6\":[\"Kuvaus (valinnainen, esim. \\\"Beta-testaajat Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Tietosuojakäytäntö\"],\"LcuSDR\":[\"Hallinnoi profiilitietojasi ja metatietoja\"],\"LqLS9B\":[\"Näytä nimimerkinvaihdot\"],\"LsDQt2\":[\"Kanavan asetukset\"],\"LtI9AS\":[\"Omistaja\"],\"LuNhhL\":[\"reagoi tähän viestiin\"],\"M/AZNG\":[\"URL avatar-kuvaasi\"],\"M/WIer\":[\"Lähetä viesti\"],\"M8er/5\":[\"Nimi:\"],\"MHk+7g\":[\"Edellinen kuva\"],\"MRorGe\":[\"Lähetä viesti käyttäjälle\"],\"MVbSGP\":[\"Aikaikkuna (sekuntia)\"],\"MkpcsT\":[\"Viestisi ja asetuksesi tallennetaan paikallisesti laitteellesi\"],\"N/hDSy\":[\"Merkitse botiksi – yleensä 'on' tai tyhjä\"],\"N7TQbE\":[\"Kutsu käyttäjä kanavalle \",[\"channelName\"]],\"NCca/o\":[\"Syötä oletusnimimerkki...\"],\"Nqs6B9\":[\"Näyttää kaiken ulkoisen median. Mikä tahansa URL voi aiheuttaa yhteyspyynnön tuntemattomalle palvelimelle.\"],\"Nt+9O7\":[\"Käytä WebSocket-yhteyttä TCP:n sijaan\"],\"NxIHzc\":[\"Katkaise yhteys\"],\"O+v/cL\":[\"Selaa kaikkia palvelimen kanavia\"],\"ODwSCk\":[\"Lähetä GIF\"],\"OGQ5kK\":[\"Määritä ilmoitusäänet ja korostukset\"],\"OIPt1Z\":[\"Näytä tai piilota jäsenlistan sivupalkki\"],\"OKSNq/\":[\"Hyvin tiukka\"],\"ONWvwQ\":[\"Lähetä\"],\"OVKoQO\":[\"Tilin salasana tunnistautumista varten\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Viedään IRC tulevaisuuteen\"],\"OhCpra\":[\"Aseta aihe…\"],\"OkltoQ\":[\"Estä \",[\"username\"],\" nimimerkin perusteella (estää liittymisen samalla nimimerkillä)\"],\"P+t/Te\":[\"Ei lisätietoja\"],\"P42Wcc\":[\"Turvallinen\"],\"PD38l0\":[\"Kanavan avatarin esikatselu\"],\"PD9mEt\":[\"Kirjoita viesti...\"],\"PPqfdA\":[\"Avaa kanavan asetukset\"],\"PSCjfZ\":[\"Aihe, joka näytetään tälle kanavalle. Kaikki käyttäjät voivat nähdä aiheen.\"],\"PZCecv\":[\"PDF-esikatselu\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 kerta\"],\"other\":[[\"c\"],\" kertaa\"]}]],\"PguS2C\":[\"Lisää poikkeusmaski (esim. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Näytetään \",[\"displayedChannelsCount\"],\"/\",[\"0\"],\" kanavaa\"],\"PqhVlJ\":[\"Estä käyttäjä (hostmaskin perusteella)\"],\"Q+chwU\":[\"Käyttäjätunnus:\"],\"Q2QY4/\":[\"Poista tämä kutsu\"],\"Q6hhn8\":[\"Asetukset\"],\"QF4a34\":[\"Syötä käyttäjänimi\"],\"QGqSZ2\":[\"Väri ja muotoilu\"],\"QJQd1J\":[\"Muokkaa profiilia\"],\"QSzGDE\":[\"Toimeton\"],\"QUlny5\":[\"Tervetuloa palvelimeen \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Lue lisää\"],\"QuSkCF\":[\"Suodata kanavia...\"],\"QwUrDZ\":[\"vaihtoi aiheen: \",[\"topic\"]],\"R0UH07\":[\"Kuva \",[\"0\"],\"/\",[\"1\"]],\"R7SsBE\":[\"Mykistä\"],\"R8rf1X\":[\"Aseta aihe napsauttamalla\"],\"RArB3D\":[\"potkittiin kanavalta \",[\"channelName\"],\" käyttäjän \",[\"username\"],\" toimesta\"],\"RI3cWd\":[\"Tutustu IRC:n maailmaan ObsidianIRC:llä\"],\"RIfHS5\":[\"Luo uusi kutsulinkki\"],\"RMMaN5\":[\"Moderoitu (+m)\"],\"RWw9Lg\":[\"Sulje ikkuna\"],\"RZ2BuZ\":[\"Tilin \",[\"account\"],\" rekisteröinti vaatii vahvistuksen: \",[\"message\"]],\"RySp6q\":[\"Piilota kommentit\"],\"SPKQTd\":[\"Nimimerkki on pakollinen\"],\"SPVjfj\":[\"Oletusarvo on 'ei syytä', jos jätetään tyhjäksi\"],\"SQKPvQ\":[\"Kutsu käyttäjä\"],\"SkZcl+\":[\"Valitse valmis tulvasuojausprofiili. Nämä profiilit tarjoavat tasapainoisia suojausasetuksia eri käyttötarkoituksiin.\"],\"Slr+3C\":[\"Vähimmäiskäyttäjämäärä\"],\"Spnlre\":[\"Kutsuit \",[\"target\"],\" liittymään kanavalle \",[\"channel\"]],\"T/ckN5\":[\"Avaa katseluohjelmassa\"],\"T91vKp\":[\"Toista\"],\"TV2Wdu\":[\"Lue, miten käsittelemme tietojasi ja suojelemme yksityisyyttäsi.\"],\"TgFpwD\":[\"Käytetään...\"],\"TkzSFB\":[\"Ei muutoksia\"],\"TtserG\":[\"Syötä oikea nimi\"],\"Ttz9J1\":[\"Syötä salasana...\"],\"Tz0i8g\":[\"Asetukset\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reagoi\"],\"UE4KO5\":[\"*kanava*\"],\"UETAwW\":[\"Et ole vielä luonut yhtään kutsulinkkiä. Käytä yllä olevaa lomaketta luodaksesi ensimmäisen.\"],\"UGT5vp\":[\"Tallenna asetukset\"],\"UV5hLB\":[\"Estoja ei löydetty\"],\"Uaj3Nd\":[\"Tilaviestit\"],\"Ue3uny\":[\"Oletus (ei profiilia)\"],\"UkARhe\":[\"Normaali – tavallinen suojaus\"],\"Umn7Cj\":[\"Ei vielä kommentteja. Ole ensimmäinen!\"],\"UtUIRh\":[[\"0\"],\" vanhempaa viestiä\"],\"UwzP+U\":[\"Suojattu yhteys\"],\"V0/A4O\":[\"Kanavan omistaja\"],\"V4qgxE\":[\"Luotu aiemmin kuin (min sitten)\"],\"V8yTm6\":[\"Tyhjennä haku\"],\"VJMMyz\":[\"ObsidianIRC – IRC:n tulevaisuuteen\"],\"VJScHU\":[\"Syy\"],\"VLsmVV\":[\"Mykistä ilmoitukset\"],\"VbyRUy\":[\"Kommentit\"],\"Vmx0mQ\":[\"Asettanut:\"],\"VqnIZz\":[\"Lue tietosuojakäytäntömme ja tiedonkäsittelytapamme\"],\"VrMygG\":[\"Vähimmäispituus on \",[\"0\"]],\"VrnTui\":[\"Pronominisi, näytetään profiilissasi\"],\"W8E3qn\":[\"Tunnistautunut tili\"],\"WAakm9\":[\"Poista kanava\"],\"WFxTHC\":[\"Lisää porttikieltomaski (esim. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Palvelimen osoite on pakollinen\"],\"WRYdXW\":[\"Äänentoistoasento\"],\"WUOH5B\":[\"Estä käyttäjä\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Näytä 1 kohde lisää\"],\"other\":[\"Näytä \",[\"1\"],\" kohdetta lisää\"]}]],\"WYxRzo\":[\"Luo ja hallinnoi kutsulinkkejäsi\"],\"Wd38W1\":[\"Jätä kanava tyhjäksi yleistä verkkokutsua varten. Kuvaus on vain omaa kirjanpitoasi varten — näkyvissä vain sinulle tässä listassa.\"],\"Weq9zb\":[\"Yleiset\"],\"Wfj7Sk\":[\"Mykistä tai poista mykistys ilmoitusäänistä\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Käyttäjäprofiili\"],\"X6S3lt\":[\"Hae asetuksia, kanavia, palvelimia...\"],\"XEHan5\":[\"Jatka silti\"],\"XI1+wb\":[\"Virheellinen muoto\"],\"XIXeuC\":[\"Viesti @\",[\"0\"]],\"XMS+k4\":[\"Aloita yksityiskeskustelu\"],\"XWgxXq\":[\"Albumi\"],\"Xd7+IT\":[\"Irrota yksityiskeskustelu\"],\"Xm/s+u\":[\"Näyttö\"],\"Xp2n93\":[\"Näyttää mediaa palvelimesi luotetusta tiedostoisännästä. Ulkoisille palveluille ei lähetetä pyyntöjä.\"],\"XvjC4F\":[\"Tallennetaan...\"],\"Y/qryO\":[\"Hakua vastaavia käyttäjiä ei löydetty\"],\"YAqRpI\":[\"Tilin \",[\"account\"],\" rekisteröinti onnistui: \",[\"message\"]],\"YEfzvP\":[\"Suojattu aihe (+t)\"],\"YQOn6a\":[\"Pienennä jäsenlista\"],\"YRCoE9\":[\"Kanavan operaattori\"],\"YURQaF\":[\"Näytä profiili\"],\"YdBSvr\":[\"Hallinnoi median näyttöä ja ulkoista sisältöä\"],\"Yj6U3V\":[\"Ei keskuspalvelinta:\"],\"YjvpGx\":[\"Pronominit\"],\"YqH4l4\":[\"Ei avainta\"],\"YyUPpV\":[\"Tili:\"],\"ZJSWfw\":[\"Viesti, joka näytetään katkaistessasi yhteyden palvelimeen\"],\"ZR1dJ4\":[\"Kutsut\"],\"ZdWg0V\":[\"Avaa selaimessa\"],\"ZhRBbl\":[\"Hae viestejä…\"],\"Zmcu3y\":[\"Lisäsuodattimet\"],\"a2/8e5\":[\"Aihe asetettu myöhemmin kuin (min sitten)\"],\"aHKcKc\":[\"Edellinen sivu\"],\"aJTbXX\":[\"Oper-salasana\"],\"aQryQv\":[\"Malli on jo olemassa\"],\"aW9pLN\":[\"Kanavalla sallittu enimmäiskäyttäjämäärä. Jätä tyhjäksi, jos rajaa ei haluta.\"],\"ah4fmZ\":[\"Näyttää myös esikatselut YouTubesta, Vimeosta, SoundCloudista ja vastaavista tunnetuista palveluista.\"],\"aifXak\":[\"Tällä kanavalla ei ole mediaa\"],\"ap2zBz\":[\"Löysä\"],\"az8lvo\":[\"Pois\"],\"azXSNo\":[\"Laajenna jäsenlista\"],\"azdliB\":[\"Kirjaudu tilille\"],\"b26wlF\":[\"hän/hänen\"],\"bD/+Ei\":[\"Tiukka\"],\"bQ6BJn\":[\"Määritä yksityiskohtaiset tulvasuojaussäännöt. Kukin sääntö määrittää, mitä toimintaa seurataan ja mitä tehdään kun raja-arvot ylitetään.\"],\"beV7+y\":[\"Käyttäjä saa kutsun liittyä kanavalle \",[\"channelName\"],\".\"],\"bk84cH\":[\"Poissaoloviesti\"],\"bkHdLj\":[\"Lisää IRC-palvelin\"],\"bmQLn5\":[\"Lisää sääntö\"],\"bwRvnp\":[\"Toiminto\"],\"c8+EVZ\":[\"Vahvistettu tili\"],\"cGYUlD\":[\"Median esikatseluja ei ladata.\"],\"cLF98o\":[\"Näytä kommentit (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Ei käyttäjiä saatavilla\"],\"cSgpoS\":[\"Kiinnitä yksityiskeskustelu\"],\"cde3ce\":[\"Viesti <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Kopioi muotoiltu tuloste\"],\"cl/A5J\":[\"Tervetuloa palvelimeen \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Poista\"],\"coPLXT\":[\"Emme tallenna IRC-viestintääsi palvelimillemme\"],\"crYH/6\":[\"SoundCloud-soitin\"],\"d3sis4\":[\"Lisää palvelin\"],\"d9aN5k\":[\"Poista \",[\"username\"],\" kanavalta\"],\"dEgA5A\":[\"Peruuta\"],\"dGi1We\":[\"Irrota tämä yksityisviestiketju\"],\"dJVuyC\":[\"poistui kanavalta \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"vastaanottaja\"],\"dXqxlh\":[\"<0>⚠️ Tietoturvariski!0> Tämä yhteys voi olla alttiina salakuuntelulle tai välimieshyökkäyksille.\"],\"da9Q/R\":[\"Muutti kanavan asetuksia\"],\"dhJN3N\":[\"Näytä kommentit\"],\"dj2xTE\":[\"Hylkää ilmoitus\"],\"dpCzmC\":[\"Tulvasuojausasetukset\"],\"e9dQpT\":[\"Haluatko avata tämän linkin uudessa välilehdessä?\"],\"ePK91l\":[\"Muokkaa\"],\"eYBDuB\":[\"Lataa kuva tai anna URL, jossa voi käyttää valinnaista \",[\"size\"],\"-muuttujaa dynaamiseen kokoon\"],\"edBbee\":[\"Estä \",[\"username\"],\" hostmaskin perusteella (estää liittymisen samasta IP-osoitteesta/hostista)\"],\"ekfzWq\":[\"Käyttäjäasetukset\"],\"elPDWs\":[\"Mukauta IRC-asiakasohjelmaasi\"],\"eu2osY\":[\"<0>💡 Suositus:0> Jatka vain jos luotat tähän palvelimeen ja ymmärrät riskit. Vältä arkaluonteisten tietojen tai salasanojen jakamista tämän yhteyden kautta.\"],\"euEhbr\":[\"Liity kanavalle \",[\"channel\"],\" napsauttamalla\"],\"ez3vLd\":[\"Ota monirivisyöttö käyttöön\"],\"f0J5Ki\":[\"Palvelimien välinen viestintä voi käyttää salaamattomia yhteyksiä\"],\"f9BHJk\":[\"Varoita käyttäjää\"],\"fDOLLd\":[\"Kanavia ei löydetty.\"],\"ffzDkB\":[\"Anonyymi analytiikka:\"],\"fq1GF9\":[\"Näytä kun käyttäjät katkaisevat yhteyden palvelimeen\"],\"gEF57C\":[\"Tämä palvelin tukee vain yhtä yhteystyyppiä\"],\"gJuLUI\":[\"Estolistа\"],\"gNzMrk\":[\"Nykyinen avatar\"],\"gjPWyO\":[\"Syötä nimimerkki...\"],\"gz6UQ3\":[\"Suurenna\"],\"h6razj\":[\"Sulje pois kanavan nimimaski\"],\"hG6jnw\":[\"Aihetta ei ole asetettu\"],\"hG89Ed\":[\"Kuva\"],\"hYgDIe\":[\"Luo\"],\"hZ6znB\":[\"Portti\"],\"ha+Bz5\":[\"esim. 100:1440\"],\"he3ygx\":[\"Kopioi\"],\"hehnjM\":[\"Määrä\"],\"hzdLuQ\":[\"Vain käyttäjät, joilla on voice tai korkeampi, voivat puhua\"],\"i0qMbr\":[\"Koti\"],\"iDNBZe\":[\"Ilmoitukset\"],\"iH8pgl\":[\"Takaisin\"],\"iL9SZg\":[\"Estä käyttäjä (nimimerkin perusteella)\"],\"iNt+3c\":[\"Takaisin kuvaan\"],\"iQvi+a\":[\"Älä varoita minua tämän palvelimen heikosta linkkiturvallisuudesta\"],\"iSLIjg\":[\"Yhdistä\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Palvelimen osoite\"],\"idD8Ev\":[\"Tallennettu\"],\"iivqkW\":[\"Kirjautunut\"],\"ij+Elv\":[\"Kuvan esikatselu\"],\"ilIWp7\":[\"Näytä/piilota ilmoitukset\"],\"iuaqvB\":[\"Käytä * jokerimerkkinä. Esimerkkejä: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Botti\"],\"j2DGR0\":[\"Estä hostmaskin perusteella\"],\"jA4uoI\":[\"Aihe:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Syy (valinnainen)\"],\"jUV7CU\":[\"Lataa avatar\"],\"jW5Uwh\":[\"Hallinnoi ulkoisen median lataamista. Pois / Turvallinen / Luotetut lähteet / Kaikki sisältö.\"],\"jXzms5\":[\"Liitteet-valinnat\"],\"jZlrte\":[\"Väri\"],\"jfC/xh\":[\"Yhteystiedot\"],\"jywMpv\":[\"#uusi-kanavan-nimi\"],\"k112DD\":[\"Lataa vanhempia viestejä\"],\"k3ID0F\":[\"Suodata jäseniä…\"],\"k65gsE\":[\"Syvempi tarkastelu\"],\"k7Zgob\":[\"Peruuta yhteys\"],\"kAVx5h\":[\"Kutsuja ei löydetty\"],\"kCLEPU\":[\"Yhdistetty palvelimeen\"],\"kF5LKb\":[\"Estetyt mallit:\"],\"kGeOx/\":[\"Liity kanavalle \",[\"0\"]],\"kITKr8\":[\"Ladataan kanavan tiloja...\"],\"kPpPsw\":[\"Olet IRC-operaattori\"],\"kWJmRL\":[\"Sinä\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Kopioi JSON\"],\"krViRy\":[\"Napsauta kopioidaksesi JSON-muodossa\"],\"ks71ra\":[\"Poikkeukset\"],\"kw4lRv\":[\"Kanavan puolioperaattori\"],\"kxgIRq\":[\"Valitse kanava tai lisää uusi päästäksesi alkuun.\"],\"ky6dWe\":[\"Avatarin esikatselu\"],\"l+GxCv\":[\"Ladataan kanavia...\"],\"l+IUVW\":[\"Tilin \",[\"account\"],\" vahvistus onnistui: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"yhdisti uudelleen\"],\"other\":[\"yhdisti uudelleen \",[\"reconnectCount\"],\" kertaa\"]}]],\"l1l8sj\":[[\"0\"],\"pv sitten\"],\"l5NhnV\":[\"#kanava (valinnainen)\"],\"l5jmzx\":[[\"0\"],\" ja \",[\"1\"],\" kirjoittavat...\"],\"lCF0wC\":[\"Päivitä\"],\"lHy8N5\":[\"Ladataan lisää kanavia...\"],\"lasgrr\":[\"käytetty\"],\"lbpf14\":[\"Liity kanavaan \",[\"value\"]],\"lfFsZ4\":[\"Kanavat\"],\"lkNdiH\":[\"Tilin nimi\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Lataa kuva\"],\"loQxaJ\":[\"Olen takaisin\"],\"lvfaxv\":[\"KOTI\"],\"m16xKo\":[\"Lisää\"],\"m8flAk\":[\"Esikatselu (ei vielä lähetetty)\"],\"mEPxTp\":[\"<0>⚠️ Ole varovainen!0> Avaa linkkejä vain luotetuista lähteistä. Haitalliset linkit voivat vaarantaa tietoturvasi tai yksityisyytesi.\"],\"mHGdhG\":[\"Palvelimen tiedot\"],\"mHS8lb\":[\"Viesti #\",[\"0\"]],\"mMYBD9\":[\"Laaja – laajempi suojauslaajuus\"],\"mTGsPd\":[\"Kanavan aihe\"],\"mU8j6O\":[\"Ei ulkoisia viestejä (+n)\"],\"mZp8FL\":[\"Automaattinen palautuminen yksiriviseksi\"],\"mdQu8G\":[\"SinunNimimerkkisi\"],\"miSSBQ\":[\"Kommentit (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Käyttäjä on tunnistautunut\"],\"mwtcGl\":[\"Sulje kommentit\"],\"mzI/c+\":[\"Lataa\"],\"n3fGRk\":[\"asettaja: \",[\"0\"]],\"nE9jsU\":[\"Löysä – vähemmän aggressiivinen suojaus\"],\"nNflMD\":[\"Poistu kanavalta\"],\"nPXkBi\":[\"Ladataan WHOIS-tietoja...\"],\"nQnxxF\":[\"Viesti #\",[\"0\"],\" (Shift+Enter = uusi rivi)\"],\"nWMRxa\":[\"Irrota kiinnitys\"],\"nkC032\":[\"Ei tulvasuojausprofiilia\"],\"o69z4d\":[\"Lähetä varoitusviesti käyttäjälle \",[\"username\"]],\"o9ylQi\":[\"Hae GIF-kuvia aloittaaksesi\"],\"oFGkER\":[\"Palvelintiedotteet\"],\"oOi11l\":[\"Siirry loppuun\"],\"oPYIL5\":[\"verkko\"],\"oQEzQR\":[\"Uusi DM\"],\"oXOSPE\":[\"Verkossa\"],\"oal760\":[\"Välimieshyökkäykset palvelinlinkeissä ovat mahdollisia\"],\"oeqmmJ\":[\"Luotetut lähteet\"],\"optX0N\":[[\"0\"],\"t sitten\"],\"ovBPCi\":[\"Oletus\"],\"p0Z69r\":[\"Malli ei voi olla tyhjä\"],\"p1KgtK\":[\"Äänen lataaminen epäonnistui\"],\"p59pEv\":[\"Lisätiedot\"],\"p7sRI6\":[\"Ilmoita muille, kun kirjoitat\"],\"pBm1od\":[\"Salainen kanava\"],\"pNmiXx\":[\"Oletusnimimerkkisi kaikille palvelimille\"],\"pUUo9G\":[\"Isäntänimi:\"],\"pVGPmz\":[\"Tilin salasana\"],\"peNE68\":[\"Pysyvä\"],\"plhHQt\":[\"Ei tietoja\"],\"pm6+q5\":[\"Tietoturvavaroitus\"],\"pn5qSs\":[\"Lisätiedot\"],\"q0cR4S\":[\"on nyt tunnettu nimellä **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanava ei näy LIST- tai NAMES-komennoissa\"],\"qLpTm/\":[\"Poista reaktio \",[\"emoji\"]],\"qVkGWK\":[\"Kiinnitä\"],\"qY8wNa\":[\"Kotisivu\"],\"qb0xJ7\":[\"Käytä jokerimerkkejä: * vastaa mitä tahansa merkkijonoa, ? vastaa yhtä merkkiä. Esimerkkejä: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Kanavan avain (+k)\"],\"qtoOYG\":[\"Ei rajaa\"],\"r1W2AS\":[\"Tiedostopalvelimen kuva\"],\"rIPR2O\":[\"Aihe asetettu aiemmin kuin (min sitten)\"],\"rMMSYo\":[\"Enimmäispituus on \",[\"0\"]],\"rWtzQe\":[\"Verkko jakautui ja yhdistyi uudelleen. ✅\"],\"rYG2u6\":[\"Odota hetki...\"],\"rdUucN\":[\"Esikatselu\"],\"rjGI/Q\":[\"Yksityisyys\"],\"rk8iDX\":[\"Ladataan GIF-kuvia...\"],\"rn6SBY\":[\"Poista mykistys\"],\"s/UKqq\":[\"Poistettiin kanavalta\"],\"s8cATI\":[\"liittyi kanavalle \",[\"channelName\"]],\"sCO9ue\":[\"Yhteydessä palvelimeen <0>\",[\"serverName\"],\"0> on seuraavia tietoturvaongelmia:\"],\"sGH11W\":[\"Palvelin\"],\"sHI1H+\":[\"on nyt tunnettu nimellä **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" kutsui sinut liittymään kanavalle \",[\"channel\"]],\"sby+1/\":[\"Kopioi napsauttamalla\"],\"sfN25C\":[\"Oikea nimesi tai koko nimesi\"],\"sliuzR\":[\"Avaa linkki\"],\"sqrO9R\":[\"Mukautetut maininnat\"],\"sr6RdJ\":[\"Monirivinen Shift+Enter-painikkeella\"],\"swrCpB\":[\"Kanava on nimetty uudelleen nimestä \",[\"oldName\"],\" nimeen \",[\"newName\"],\" käyttäjän \",[\"user\"],\" toimesta\",[\"0\"]],\"sxkWRg\":[\"Lisäasetukset\"],\"t/YqKh\":[\"Poista\"],\"t47eHD\":[\"Yksilöllinen tunnuksesi tällä palvelimella\"],\"tAkAh0\":[\"URL, jossa valinnainen \",[\"size\"],\"-muuttuja dynaamista kokoa varten. Esimerkki: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Näytä tai piilota kanavaluettelon sivupalkki\"],\"tfDRzk\":[\"Tallenna\"],\"tiBsJk\":[\"poistui kanavalta \",[\"channelName\"]],\"tt4/UD\":[\"poistui (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Nimimerkki {nick} on jo käytössä, yritetään uudelleen nimellä {newNick}\"],\"u0a8B4\":[\"Tunnistaudu IRC-operaattoriksi ylläpito-oikeuksia varten\"],\"u0rWFU\":[\"Luotu myöhemmin kuin (min sitten)\"],\"u72w3t\":[\"Estettävät käyttäjät ja mallit\"],\"u7jc2L\":[\"poistui\"],\"uAQUqI\":[\"Tila\"],\"uB85T3\":[\"Tallennus epäonnistui: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC-palvelimet:\"],\"ukyW4o\":[\"Kutsulinkkisi\"],\"usSSr/\":[\"Zoomaustaso\"],\"v7uvcf\":[\"Ohjelmisto:\"],\"vE8kb+\":[\"Käytä Shift+Enter uudelle riville (Enter lähettää)\"],\"vERlcd\":[\"Profiili\"],\"vK0RL8\":[\"Ei aihetta\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Kieli\"],\"vaHYxN\":[\"Oikea nimi\"],\"vhjbKr\":[\"Poissa\"],\"w4NYox\":[[\"title\"],\" asiakasohjelma\"],\"w8xQRx\":[\"Virheellinen arvo\"],\"wFjjxZ\":[\"potkittiin kanavalta \",[\"channelName\"],\" käyttäjän \",[\"username\"],\" toimesta (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Porttikieltopoikkeuksia ei löydetty\"],\"wPrGnM\":[\"Kanavan ylläpitäjä\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Näytä kun käyttäjät liittyvät kanavalle tai poistuvat\"],\"whqZ9r\":[\"Lisäsanat tai -lauseet korostettavaksi\"],\"wm7RV4\":[\"Ilmoitusääni\"],\"wz/Yoq\":[\"Viestisi voidaan siepata palvelimien välillä välitettäessä\"],\"x3+y8b\":[\"Tämän verran ihmisiä on rekisteröitynyt tämän linkin kautta\"],\"xCJdfg\":[\"Tyhjennä\"],\"xOTzt5\":[\"juuri nyt\"],\"xUHRTR\":[\"Tunnistaudu automaattisesti operaattoriksi yhdistäessä\"],\"xWHwwQ\":[\"Estot\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"Tämä palvelin ei tue kutsulinkkejä (<0>obby.world/invitation0>-kykyä ei ilmoiteta). Voit silti chattailla normaalisti; tämä paneeli on obbyircd-pohjaisia verkkoja varten.\"],\"xceQrO\":[\"Vain suojatut WebSocket-yhteydet ovat tuettuja\"],\"xdtXa+\":[\"kanavan-nimi\"],\"xfXC7q\":[\"Tekstikanavat\"],\"xlCYOE\":[\"Haetaan lisää viestejä...\"],\"xlhswE\":[\"Vähimmäisarvo on \",[\"0\"]],\"xq97Ci\":[\"Lisää sana tai lause...\"],\"xuRqRq\":[\"Käyttäjäraja (+l)\"],\"xwF+7J\":[[\"0\"],\" kirjoittaa...\"],\"y1eoq1\":[\"Kopioi linkki\"],\"yNeucF\":[\"Tämä palvelin ei tue laajennettua profiilimetadataa (IRCv3 METADATA -laajennus). Lisäkentät kuten avatar, näyttönimi ja tila eivät ole käytettävissä.\"],\"yPlrca\":[\"Kanavan avatar\"],\"yQE2r9\":[\"Ladataan\"],\"ySU+JY\":[\"sinun@sahkoposti.fi\"],\"yTX1Rt\":[\"Oper-käyttäjänimi\"],\"yYOzWD\":[\"lokit\"],\"yfx9Re\":[\"IRC-operaattorin salasana\"],\"ygCKqB\":[\"Pysäytä\"],\"ymDxJx\":[\"IRC-operaattorin käyttäjänimi\"],\"yrpRsQ\":[\"Lajittele nimen mukaan\"],\"yz7wBu\":[\"Sulje\"],\"zJw+jA\":[\"asettaa tilan: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"min sitten\"],\"zebeLu\":[\"Syötä oper-käyttäjänimi\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Virheellinen mallimuoto. Käytä nick!käyttäjä@isäntä-muotoa (jokerimerkit * sallittu)\"],\"+6NQQA\":[\"Yleinen tukikanava\"],\"+6NyRG\":[\"Asiakasohjelma\"],\"+K0AvT\":[\"Katkaise yhteys\"],\"+cyFdH\":[\"Oletusviesti poissaoloilmoitukselle\"],\"+mVPqU\":[\"Näytä Markdown-muotoilu viesteissä\"],\"+vqCJH\":[\"Tilin käyttäjänimi tunnistautumista varten\"],\"+yPBXI\":[\"Valitse tiedosto\"],\"+zy2Nq\":[\"Tyyppi\"],\"/09cao\":[\"Heikko linkkiturvallisuus (taso \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Kanavan ulkopuoliset käyttäjät eivät voi lähettää viestejä sille\"],\"/4C8U0\":[\"Kopioi kaikki\"],\"/6BzZF\":[\"Näytä/piilota jäsenlista\"],\"/AkXyp\":[\"Vahvista?\"],\"/TNOPk\":[\"Käyttäjä on poissa\"],\"/XQgft\":[\"Selaa\"],\"/cF7Rs\":[\"Äänenvoimakkuus\"],\"/dqduX\":[\"Seuraava sivu\"],\"/fc3q4\":[\"Kaikki sisältö\"],\"/kISDh\":[\"Ota ilmoitusäänet käyttöön\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Ääni\"],\"/rfkZe\":[\"Toista ääniä maininnoista ja viesteistä\"],\"0/0ZGA\":[\"Kanavan nimimaski\"],\"0D6j7U\":[\"Lue lisää mukautetuista säännöistä →\"],\"0XsHcR\":[\"Poista käyttäjä\"],\"0ZpE//\":[\"Lajittele käyttäjämäärän mukaan\"],\"0bEPwz\":[\"Aseta poissa\"],\"0dGkPt\":[\"Laajenna kanavaluettelo\"],\"0gS7M5\":[\"Näyttönimi\"],\"0kS+M8\":[\"EsimerkKi\"],\"0rgoY7\":[\"Yhdistä vain valitsemiisi palvelimiin\"],\"0wdd7X\":[\"Liity\"],\"0wkVYx\":[\"Yksityisviestit\"],\"111uHX\":[\"Linkin esikatselu\"],\"196EG4\":[\"Poista yksityiskeskustelu\"],\"1DSr1i\":[\"Rekisteröidy tilille\"],\"1O/24y\":[\"Näytä/piilota kanavaluettelo\"],\"1TNIig\":[\"Avaa\"],\"1VPJJ2\":[\"Ulkoisen linkin varoitus\"],\"1ZC/dv\":[\"Ei lukemattomia mainintoja tai viestejä\"],\"1pO1zi\":[\"Palvelimen nimi on pakollinen\"],\"1uwfzQ\":[\"Näytä kanavan aihe\"],\"268g7c\":[\"Syötä näyttönimi\"],\"2CEOW6\":[\"Verkko sidottu soju-bouncerin kautta\"],\"2F9+AZ\":[\"Raakaa IRC-liikennettä ei ole vielä tallennettu. Yritä yhdistää tai lähettää viesti.\"],\"2FOFq1\":[\"Verkon palvelinoperaattorit voisivat mahdollisesti lukea viestisi\"],\"2FYpfJ\":[\"Lisää\"],\"2HF1Y2\":[[\"inviter\"],\" kutsui \",[\"target\"],\" liittymään kanavalle \",[\"channel\"]],\"2I70QL\":[\"Näytä käyttäjäprofiilitiedot\"],\"2QYdmE\":[\"Käyttäjät:\"],\"2QpEjG\":[\"poistui\"],\"2YE223\":[\"Viesti #\",[\"0\"],\" (Enter = uusi rivi, Shift+Enter = lähetä)\"],\"2bimFY\":[\"Käytä palvelimen salasanaa\"],\"2iTmdZ\":[\"Paikallinen tallennustila:\"],\"2odkwe\":[\"Tiukka – aggressiivisempi suojaus\"],\"2uDhbA\":[\"Syötä kutsuttavan käyttäjänimi\"],\"2ygf/L\":[\"← Takaisin\"],\"2zEgxj\":[\"Hae GIF-kuvia...\"],\"3RdPhl\":[\"Nimeä kanava uudelleen\"],\"3THokf\":[\"Voice-käyttäjä\"],\"3TSz9S\":[\"Pienennä\"],\"3jBDvM\":[\"Kanavan näyttönimi\"],\"3ryuFU\":[\"Valinnaiset kaatumisraportit sovelluksen parantamiseksi\"],\"3uBF/8\":[\"Sulje katseluohjelma\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Syötä tilin nimi...\"],\"4/Rr0R\":[\"Kutsu käyttäjä nykyiselle kanavalle\"],\"4EZrJN\":[\"Säännöt\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Tulvasuojausprofiili (+F)\"],\"4RZQRK\":[\"Mitä olet tekemässä?\"],\"4hfTrB\":[\"Nimimerkki\"],\"4n99LO\":[\"Jo kanavalla \",[\"0\"]],\"4t6vMV\":[\"Vaihda automaattisesti yksiriviseen lyhyille viesteille\"],\"4vsHmf\":[\"Aika (min)\"],\"4x/Axu\":[\"Bouncerissasi ei ole vielä verkkoja. Lisää yksi päästäksesi alkuun.\"],\"5+INAX\":[\"Korosta viestit, joissa mainitaan sinut\"],\"5R5Pv/\":[\"Oper-nimi\"],\"678PKt\":[\"Verkon nimi\"],\"6Aih4U\":[\"Poissa verkosta\"],\"6CO3WE\":[\"Salasana vaaditaan kanavalle liittymiseen. Jätä tyhjäksi poistaaksesi avaimen.\"],\"6HhMs3\":[\"Lähtöviesti\"],\"6V3Ea3\":[\"Kopioitu\"],\"6lGV3K\":[\"Näytä vähemmän\"],\"6yFOEi\":[\"Syötä oper-salasana...\"],\"7+IHTZ\":[\"Ei valittua tiedostoa\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!käyttäjä@isäntä (esim. spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Lähetä yksityisviesti\"],\"7U1W7c\":[\"Hyvin löysä\"],\"7Y1YQj\":[\"Oikea nimi:\"],\"7YHArF\":[\"— avaa katseluohjelmassa\"],\"7fjnVl\":[\"Hae käyttäjiä...\"],\"7jL88x\":[\"Poistetaanko tämä viesti? Toimintoa ei voi kumota.\"],\"7nGhhM\":[\"Mitä ajattelet?\"],\"7sEpu1\":[\"Jäsenet — \",[\"0\"]],\"7sNhEz\":[\"Käyttäjänimi\"],\"8H0Q+x\":[\"Lue lisää profiileista →\"],\"8Phu0A\":[\"Näytä kun käyttäjät vaihtavat nimimerkkinsä\"],\"8XTG9e\":[\"Syötä oper-salasana\"],\"8XsV2J\":[\"Yritä lähettää uudelleen\"],\"8ZsakT\":[\"Salasana\"],\"8kR84m\":[\"Olet avaamassa ulkoista linkkiä:\"],\"8lCgih\":[\"Poista sääntö\"],\"8o3dPc\":[\"Pudota tiedostot lähettääksesi\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"liittyi\"],\"other\":[\"liittyi \",[\"joinCount\"],\" kertaa\"]}]],\"9BMLnJ\":[\"Yhdistä uudelleen palvelimeen\"],\"9OEgyT\":[\"Lisää reaktio\"],\"9PQ8m2\":[\"G-Line (maailmanlaajuinen esto)\"],\"9Qs99X\":[\"Sähköposti:\"],\"9QupBP\":[\"Poista malli\"],\"9W7tl5\":[\"(muuttumaton)\"],\"9bG48P\":[\"Lähetetään\"],\"9f5f0u\":[\"Yksityisyyteen liittyviä kysymyksiä? Ota yhteyttä:\"],\"9iweoP\":[\"Verkot palvelimella \",[\"0\"]],\"9unqs3\":[\"Poissa:\"],\"9v3hwv\":[\"Palvelimia ei löydetty.\"],\"9zb2WA\":[\"Yhdistetään\"],\"A1taO8\":[\"Hae\"],\"A2adVi\":[\"Lähetä kirjoitusilmoituksia\"],\"A9Rhec\":[\"Kanavan nimi\"],\"AWOSPo\":[\"Lähennä\"],\"AXSpEQ\":[\"Oper yhdistäessä\"],\"AdKRCX\":[\"Olet yhteydessä palvelimeen <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Tili\"],\"AhNP40\":[\"Selaa\"],\"Ai2U7L\":[\"Isäntä\"],\"AjBQnf\":[\"Vaihtoi nimimerkin\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Peruuta vastaus\"],\"ApSx0O\":[\"Löydettiin \",[\"0\"],\" viestiä, jotka vastaavat \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Tuloksia ei löydetty\"],\"AyNqAB\":[\"Näytä kaikki palvelintapahtumat chatissa\"],\"B/QqGw\":[\"Poissa näppäimistöltä\"],\"B0sB2k\":[\"Salaamaton\"],\"B8AaMI\":[\"Tämä kenttä on pakollinen\"],\"BA2c49\":[\"Palvelin ei tue kehittynyttä LIST-suodatusta\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" ja \",[\"3\"],\" muuta kirjoittavat...\"],\"BGul2A\":[\"Sinulla on tallentamattomia muutoksia. Haluatko varmasti sulkea tallentamatta?\"],\"BIf9fi\":[\"Tilaviestisi\"],\"BOJWfb\":[\"Katkaistaanko soju-bouncerin yhteys?\"],\"BPm98R\":[\"Yhtään palvelinta ei ole valittu. Valitse ensin palvelin sivupalkista; kutsulinkkejä hallitaan palvelinkohtaisesti.\"],\"BZz3md\":[\"Henkilökohtainen verkkosivustosi\"],\"Bgm/H7\":[\"Salli monirivisyöttö\"],\"BiQIl1\":[\"Kiinnitä tämä yksityisviesteistä\"],\"BlNZZ2\":[\"Siirry viestiin napsauttamalla\"],\"Bowq3c\":[\"Vain operaattorit voivat muuttaa kanavan aihetta\"],\"Btozzp\":[\"Tämä kuva on vanhentunut\"],\"Bycfjm\":[\"Yhteensä: \",[\"0\"]],\"C6IBQc\":[\"Kopioi koko JSON\"],\"C9L9wL\":[\"Tiedonkeruu\"],\"CDq4wC\":[\"Moderoi käyttäjää\"],\"CHVRxG\":[\"Viesti @\",[\"0\"],\" (Shift+Enter = uusi rivi)\"],\"CN9zdR\":[\"Oper-nimi ja salasana ovat pakollisia\"],\"CW3sYa\":[\"Lisää reaktio \",[\"emoji\"]],\"CaAkqd\":[\"Näytä poistumisviestit\"],\"CbvaYj\":[\"Estä nimimerkin perusteella\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Valitse kanava\"],\"CsekCi\":[\"Normaali\"],\"D+NlUC\":[\"Järjestelmä\"],\"D28t6+\":[\"liittyi ja poistui\"],\"DB8zMK\":[\"Käytä\"],\"DBcWHr\":[\"Mukautettu ilmoitusäänitiedosto\"],\"DTy9Xw\":[\"Median esikatselut\"],\"Dj4pSr\":[\"Valitse turvallinen salasana\"],\"Du+zn+\":[\"Haetaan...\"],\"Du2T2f\":[\"Asetusta ei löydetty\"],\"DwsSVQ\":[\"Käytä suodattimet ja päivitä\"],\"E3W/zd\":[\"Oletusnimimerkki\"],\"E6nRW7\":[\"Kopioi URL\"],\"E703RG\":[\"Tilat:\"],\"EAeu1Z\":[\"Lähetä kutsu\"],\"EFKJQT\":[\"Asetus\"],\"EGPQBv\":[\"Mukautetut tulvasäännöt (+f)\"],\"ELik0r\":[\"Lue koko tietosuojakäytäntö\"],\"EPbeC2\":[\"Näytä tai muokkaa kanavan aihetta\"],\"EQCDNT\":[\"Syötä oper-käyttäjätunnus...\"],\"EUvulZ\":[\"Löydettiin 1 viesti, joka vastaa \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Seuraava kuva\"],\"EdQY6l\":[\"Ei mitään\"],\"EnqLYU\":[\"Hae palvelimia...\"],\"F0OKMc\":[\"Muokkaa palvelinta\"],\"F6Int2\":[\"Ota korostukset käyttöön\"],\"FDoLyE\":[\"Enimmäiskäyttäjämäärä\"],\"FUU/hZ\":[\"Hallitsee, kuinka paljon ulkoista mediaa ladataan chatissa.\"],\"Fdp03t\":[\"päälle\"],\"FfPWR0\":[\"Ikkuna\"],\"FjkaiT\":[\"Loitonna\"],\"FlqOE9\":[\"Mitä tämä tarkoittaa:\"],\"FolHNl\":[\"Hallinnoi tiliäsi ja tunnistautumista\"],\"Fp2Dif\":[\"Poistui palvelimelta\"],\"G5KmCc\":[\"GZ-Line (maailmanlaajuinen Z-Line)\"],\"GDs0lz\":[\"<0>Riski:0> Arkaluonteiset tiedot (viestit, yksityiskeskustelut, tunnistautumistiedot) voisivat paljastua verkon ylläpitäjille tai hyökkääjille, jotka sijaitsevat IRC-palvelimien välissä.\"],\"GR+2I3\":[\"Lisää kutsumask (esim. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Sulje irrotettu palvelintiedotteet-näkymä\"],\"GdhD7H\":[\"Vahvista napsauttamalla uudelleen\"],\"GjRZex\":[\"Katkaistaanko verkon yhteys?\"],\"GlHnXw\":[\"Nimen vaihto epäonnistui: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Esikatselu:\"],\"GtmO8/\":[\"lähettäjä\"],\"GtuHUQ\":[\"Nimeä tämä kanava uudelleen palvelimella. Kaikki käyttäjät näkevät uuden nimen.\"],\"GuGfFX\":[\"Näytä/piilota haku\"],\"GxkJXS\":[\"Ladataan...\"],\"GzbwnK\":[\"Liittyi kanavalle\"],\"GzsUDB\":[\"Laajennettu profiili\"],\"H/PnT8\":[\"Lisää emoji\"],\"H6Izzl\":[\"Suosikki värikoodisi\"],\"H9jIv+\":[\"Näytä liittymiset/poistumiset\"],\"HAKBY9\":[\"Lataa tiedostoja\"],\"HdE1If\":[\"Kanava\"],\"Hk4AW9\":[\"Suosikki näyttönimesi\"],\"HmHDk7\":[\"Valitse jäsen\"],\"HrQzPU\":[\"Kanavat verkossa \",[\"networkName\"]],\"I2tXQ5\":[\"Viesti @\",[\"0\"],\" (Enter = uusi rivi, Shift+Enter = lähetä)\"],\"I6bw/h\":[\"Estä käyttäjä\"],\"I92Z+b\":[\"Ota ilmoitukset käyttöön\"],\"I9D72S\":[\"Haluatko varmasti poistaa tämän viestin? Tätä toimintoa ei voi kumota.\"],\"IA+1wo\":[\"Näytä kun käyttäjiä poistetaan kanavilta\"],\"IDwkJx\":[\"IRC-operaattori\"],\"ILlU+s\":[\"Tiedot:\"],\"IUwGEM\":[\"Tallenna muutokset\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" ja \",[\"2\"],\" kirjoittavat...\"],\"IgrLD/\":[\"Tauko\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Vastaa\"],\"IoHMnl\":[\"Enimmäisarvo on \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Yhdistetään...\"],\"J5T9NW\":[\"Käyttäjätiedot\"],\"J8Y5+z\":[\"Hups! Verkon jako! ⚠️\"],\"JBHkBA\":[\"Poistui kanavalta\"],\"JCwL0Q\":[\"Syötä syy (valinnainen)\"],\"JFciKP\":[\"Vaihda\"],\"JXGkhG\":[\"Muuta kanavan nimeä (vain operaattorit)\"],\"JcD7qf\":[\"Lisää toimintoja\"],\"JdkA+c\":[\"Salainen (+s)\"],\"Jmu12l\":[\"Palvelimen kanavat\"],\"JoQY+E\":[\"Tämä poistaa verkon soju-bouncerista. Käyttääksesi sitä uudelleen, sinun täytyy lisätä se takaisin.\"],\"JvQ++s\":[\"Ota Markdown käyttöön\"],\"K2jwh/\":[\"WHOIS-tietoja ei saatavilla\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Poista viesti\"],\"KKBlUU\":[\"Upotus\"],\"KM0pLb\":[\"Tervetuloa kanavalle!\"],\"KR6W2h\":[\"Poista esto käyttäjältä\"],\"KV+Bi1\":[\"Vain kutsulla (+i)\"],\"KdCtwE\":[\"Kuinka monta sekuntia tulvatoimintaa seurataan ennen laskurien nollaamista\"],\"Kkezga\":[\"Palvelimen salasana\"],\"KsiQ/8\":[\"Käyttäjät täytyy kutsua kanavalle\"],\"L+gB/D\":[\"Kanavan tiedot\"],\"LC1a7n\":[\"IRC-palvelin on ilmoittanut, että sen palvelimien väliset linkit ovat heikosti suojattuja. Tämä tarkoittaa, että verkossa välitettäviä viestejäsi ei välttämättä salata asianmukaisesti tai SSL/TLS-sertifikaatteja ei tarkisteta oikein.\"],\"LEwpeL\":[\"soju-bouncer (hallinta)\"],\"LNfLR5\":[\"Näytä poistamiset\"],\"LP+1Z7\":[\"Lisää verkko\"],\"LQb0W/\":[\"Näytä kaikki tapahtumat\"],\"LU7/yA\":[\"Vaihtoehtoinen nimi käyttöliittymässä näytettäväksi. Voi sisältää välilyöntejä, emojeja ja erikoismerkkejä. Todellista kanavan nimeä (\",[\"channelName\"],\") käytetään edelleen IRC-komennoissa.\"],\"LUb9O7\":[\"Kelvollinen palvelinportti on pakollinen\"],\"LV4fT6\":[\"Kuvaus (valinnainen, esim. \\\"Beta-testaajat Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Tietosuojakäytäntö\"],\"LcuSDR\":[\"Hallinnoi profiilitietojasi ja metatietoja\"],\"LqLS9B\":[\"Näytä nimimerkinvaihdot\"],\"LsDQt2\":[\"Kanavan asetukset\"],\"LtI9AS\":[\"Omistaja\"],\"LuNhhL\":[\"reagoi tähän viestiin\"],\"M/AZNG\":[\"URL avatar-kuvaasi\"],\"M/WIer\":[\"Lähetä viesti\"],\"M8er/5\":[\"Nimi:\"],\"MHk+7g\":[\"Edellinen kuva\"],\"MRorGe\":[\"Lähetä viesti käyttäjälle\"],\"MVbSGP\":[\"Aikaikkuna (sekuntia)\"],\"MkpcsT\":[\"Viestisi ja asetuksesi tallennetaan paikallisesti laitteellesi\"],\"MzPdC2\":[\"Palvelimen salasana (PASS)\"],\"N/hDSy\":[\"Merkitse botiksi – yleensä 'on' tai tyhjä\"],\"N6j2JH\":[\"Muokkaa: \",[\"0\"]],\"N7TQbE\":[\"Kutsu käyttäjä kanavalle \",[\"channelName\"]],\"NCca/o\":[\"Syötä oletusnimimerkki...\"],\"Nqs6B9\":[\"Näyttää kaiken ulkoisen median. Mikä tahansa URL voi aiheuttaa yhteyspyynnön tuntemattomalle palvelimelle.\"],\"Nt+9O7\":[\"Käytä WebSocket-yhteyttä TCP:n sijaan\"],\"NxIHzc\":[\"Katkaise yhteys\"],\"O+v/cL\":[\"Selaa kaikkia palvelimen kanavia\"],\"OCGpR4\":[\"(peritty)\"],\"ODwSCk\":[\"Lähetä GIF\"],\"OGQ5kK\":[\"Määritä ilmoitusäänet ja korostukset\"],\"OIPt1Z\":[\"Näytä tai piilota jäsenlistan sivupalkki\"],\"OKSNq/\":[\"Hyvin tiukka\"],\"ONWvwQ\":[\"Lähetä\"],\"OVKoQO\":[\"Tilin salasana tunnistautumista varten\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Viedään IRC tulevaisuuteen\"],\"OhCpra\":[\"Aseta aihe…\"],\"OkltoQ\":[\"Estä \",[\"username\"],\" nimimerkin perusteella (estää liittymisen samalla nimimerkillä)\"],\"P+t/Te\":[\"Ei lisätietoja\"],\"P42Wcc\":[\"Turvallinen\"],\"PD38l0\":[\"Kanavan avatarin esikatselu\"],\"PD9mEt\":[\"Kirjoita viesti...\"],\"PPqfdA\":[\"Avaa kanavan asetukset\"],\"PSCjfZ\":[\"Aihe, joka näytetään tälle kanavalle. Kaikki käyttäjät voivat nähdä aiheen.\"],\"PZCecv\":[\"PDF-esikatselu\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 kerta\"],\"other\":[[\"c\"],\" kertaa\"]}]],\"PguS2C\":[\"Lisää poikkeusmaski (esim. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Näytetään \",[\"displayedChannelsCount\"],\"/\",[\"0\"],\" kanavaa\"],\"PqhVlJ\":[\"Estä käyttäjä (hostmaskin perusteella)\"],\"Q+chwU\":[\"Käyttäjätunnus:\"],\"Q2QY4/\":[\"Poista tämä kutsu\"],\"Q3v9Wc\":[\"Kyllä, poista\"],\"Q6hhn8\":[\"Asetukset\"],\"QF4a34\":[\"Syötä käyttäjänimi\"],\"QGqSZ2\":[\"Väri ja muotoilu\"],\"QJQd1J\":[\"Muokkaa profiilia\"],\"QSzGDE\":[\"Toimeton\"],\"QUlny5\":[\"Tervetuloa palvelimeen \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Lue lisää\"],\"QuSkCF\":[\"Suodata kanavia...\"],\"QwUrDZ\":[\"vaihtoi aiheen: \",[\"topic\"]],\"R0UH07\":[\"Kuva \",[\"0\"],\"/\",[\"1\"]],\"R7SsBE\":[\"Mykistä\"],\"R8rf1X\":[\"Aseta aihe napsauttamalla\"],\"RArB3D\":[\"potkittiin kanavalta \",[\"channelName\"],\" käyttäjän \",[\"username\"],\" toimesta\"],\"RI3cWd\":[\"Tutustu IRC:n maailmaan ObsidianIRC:llä\"],\"RIfHS5\":[\"Luo uusi kutsulinkki\"],\"RMMaN5\":[\"Moderoitu (+m)\"],\"RWw9Lg\":[\"Sulje ikkuna\"],\"RZ2BuZ\":[\"Tilin \",[\"account\"],\" rekisteröinti vaatii vahvistuksen: \",[\"message\"]],\"RySp6q\":[\"Piilota kommentit\"],\"S5Togi\":[\"Ladataan verkkoja bouncerista…\"],\"SPKQTd\":[\"Nimimerkki on pakollinen\"],\"SPVjfj\":[\"Oletusarvo on 'ei syytä', jos jätetään tyhjäksi\"],\"SQKPvQ\":[\"Kutsu käyttäjä\"],\"STmlpb\":[\"Takaisin verkkolistalle\"],\"SkZcl+\":[\"Valitse valmis tulvasuojausprofiili. Nämä profiilit tarjoavat tasapainoisia suojausasetuksia eri käyttötarkoituksiin.\"],\"Slr+3C\":[\"Vähimmäiskäyttäjämäärä\"],\"Spnlre\":[\"Kutsuit \",[\"target\"],\" liittymään kanavalle \",[\"channel\"]],\"T/ckN5\":[\"Avaa katseluohjelmassa\"],\"T91vKp\":[\"Toista\"],\"TV2Wdu\":[\"Lue, miten käsittelemme tietojasi ja suojelemme yksityisyyttäsi.\"],\"TgFpwD\":[\"Käytetään...\"],\"TkzSFB\":[\"Ei muutoksia\"],\"TtserG\":[\"Syötä oikea nimi\"],\"Ttz9J1\":[\"Syötä salasana...\"],\"Tz0i8g\":[\"Asetukset\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reagoi\"],\"UE4KO5\":[\"*kanava*\"],\"UETAwW\":[\"Et ole vielä luonut yhtään kutsulinkkiä. Käytä yllä olevaa lomaketta luodaksesi ensimmäisen.\"],\"UGT5vp\":[\"Tallenna asetukset\"],\"UV5hLB\":[\"Estoja ei löydetty\"],\"Uaj3Nd\":[\"Tilaviestit\"],\"Ue3uny\":[\"Oletus (ei profiilia)\"],\"UkARhe\":[\"Normaali – tavallinen suojaus\"],\"Umn7Cj\":[\"Ei vielä kommentteja. Ole ensimmäinen!\"],\"UtUIRh\":[[\"0\"],\" vanhempaa viestiä\"],\"UwzP+U\":[\"Suojattu yhteys\"],\"V0/A4O\":[\"Kanavan omistaja\"],\"V0zZWc\":[\"Tämä sulkee myös alla olevan sidotun verkon.\"],\"V4qgxE\":[\"Luotu aiemmin kuin (min sitten)\"],\"V8yTm6\":[\"Tyhjennä haku\"],\"VJMMyz\":[\"ObsidianIRC – IRC:n tulevaisuuteen\"],\"VJScHU\":[\"Syy\"],\"VLsmVV\":[\"Mykistä ilmoitukset\"],\"VbyRUy\":[\"Kommentit\"],\"Vmx0mQ\":[\"Asettanut:\"],\"VqnIZz\":[\"Lue tietosuojakäytäntömme ja tiedonkäsittelytapamme\"],\"VrMygG\":[\"Vähimmäispituus on \",[\"0\"]],\"VrnTui\":[\"Pronominisi, näytetään profiilissasi\"],\"W8E3qn\":[\"Tunnistautunut tili\"],\"WAakm9\":[\"Poista kanava\"],\"WFxTHC\":[\"Lisää porttikieltomaski (esim. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Palvelimen osoite on pakollinen\"],\"WRYdXW\":[\"Äänentoistoasento\"],\"WUOH5B\":[\"Estä käyttäjä\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Näytä 1 kohde lisää\"],\"other\":[\"Näytä \",[\"1\"],\" kohdetta lisää\"]}]],\"WYxRzo\":[\"Luo ja hallinnoi kutsulinkkejäsi\"],\"Wd38W1\":[\"Jätä kanava tyhjäksi yleistä verkkokutsua varten. Kuvaus on vain omaa kirjanpitoasi varten â näkyvissä vain sinulle tässä listassa.\"],\"Weq9zb\":[\"Yleiset\"],\"Wfj7Sk\":[\"Mykistä tai poista mykistys ilmoitusäänistä\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Käyttäjäprofiili\"],\"X6S3lt\":[\"Hae asetuksia, kanavia, palvelimia...\"],\"XEHan5\":[\"Jatka silti\"],\"XI1+wb\":[\"Virheellinen muoto\"],\"XIXeuC\":[\"Viesti @\",[\"0\"]],\"XMS+k4\":[\"Aloita yksityiskeskustelu\"],\"XWgxXq\":[\"Albumi\"],\"Xd7+IT\":[\"Irrota yksityiskeskustelu\"],\"Xm/s+u\":[\"Näyttö\"],\"Xp2n93\":[\"Näyttää mediaa palvelimesi luotetusta tiedostoisännästä. Ulkoisille palveluille ei lähetetä pyyntöjä.\"],\"XvjC4F\":[\"Tallennetaan...\"],\"Y/qryO\":[\"Hakua vastaavia käyttäjiä ei löydetty\"],\"YAqRpI\":[\"Tilin \",[\"account\"],\" rekisteröinti onnistui: \",[\"message\"]],\"YEfzvP\":[\"Suojattu aihe (+t)\"],\"YQOn6a\":[\"Pienennä jäsenlista\"],\"YRCoE9\":[\"Kanavan operaattori\"],\"YURQaF\":[\"Näytä profiili\"],\"YdBSvr\":[\"Hallinnoi median näyttöä ja ulkoista sisältöä\"],\"Yj6U3V\":[\"Ei keskuspalvelinta:\"],\"YjvpGx\":[\"Pronominit\"],\"YqH4l4\":[\"Ei avainta\"],\"YyUPpV\":[\"Tili:\"],\"ZJSWfw\":[\"Viesti, joka näytetään katkaistessasi yhteyden palvelimeen\"],\"ZR1dJ4\":[\"Kutsut\"],\"ZdWg0V\":[\"Avaa selaimessa\"],\"ZhRBbl\":[\"Hae viestejä…\"],\"Zmcu3y\":[\"Lisäsuodattimet\"],\"a0bHay\":[\"Katkaistaanko <0>\",[\"0\"],\"0> -yhteys?\"],\"a2/8e5\":[\"Aihe asetettu myöhemmin kuin (min sitten)\"],\"aHKcKc\":[\"Edellinen sivu\"],\"aJTbXX\":[\"Oper-salasana\"],\"aQryQv\":[\"Malli on jo olemassa\"],\"aW9pLN\":[\"Kanavalla sallittu enimmäiskäyttäjämäärä. Jätä tyhjäksi, jos rajaa ei haluta.\"],\"ah4fmZ\":[\"Näyttää myös esikatselut YouTubesta, Vimeosta, SoundCloudista ja vastaavista tunnetuista palveluista.\"],\"aifXak\":[\"Tällä kanavalla ei ole mediaa\"],\"ap2zBz\":[\"Löysä\"],\"az8lvo\":[\"Pois\"],\"azXSNo\":[\"Laajenna jäsenlista\"],\"azdliB\":[\"Kirjaudu tilille\"],\"b26wlF\":[\"hän/hänen\"],\"bD/+Ei\":[\"Tiukka\"],\"bQ6BJn\":[\"Määritä yksityiskohtaiset tulvasuojaussäännöt. Kukin sääntö määrittää, mitä toimintaa seurataan ja mitä tehdään kun raja-arvot ylitetään.\"],\"beV7+y\":[\"Käyttäjä saa kutsun liittyä kanavalle \",[\"channelName\"],\".\"],\"bk84cH\":[\"Poissaoloviesti\"],\"bkHdLj\":[\"Lisää IRC-palvelin\"],\"bmQLn5\":[\"Lisää sääntö\"],\"bv4cFj\":[\"Siirtoprotokolla\"],\"bwRvnp\":[\"Toiminto\"],\"c8+EVZ\":[\"Vahvistettu tili\"],\"cGYUlD\":[\"Median esikatseluja ei ladata.\"],\"cLF98o\":[\"Näytä kommentit (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Ei käyttäjiä saatavilla\"],\"cSgpoS\":[\"Kiinnitä yksityiskeskustelu\"],\"cXeEKu\":[\"Tämä sulkee myös alla olevat \",[\"0\"],\" sidottua verkkoa.\"],\"cde3ce\":[\"Viesti <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Kopioi muotoiltu tuloste\"],\"cl/A5J\":[\"Tervetuloa palvelimeen \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Poista\"],\"coPLXT\":[\"Emme tallenna IRC-viestintääsi palvelimillemme\"],\"crYH/6\":[\"SoundCloud-soitin\"],\"cv5DQb\":[\"isäntää ei asetettu\"],\"d3sis4\":[\"Lisää palvelin\"],\"d9aN5k\":[\"Poista \",[\"username\"],\" kanavalta\"],\"dEgA5A\":[\"Peruuta\"],\"dGi1We\":[\"Irrota tämä yksityisviestiketju\"],\"dJVuyC\":[\"poistui kanavalta \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"vastaanottaja\"],\"dXqxlh\":[\"<0>⚠️ Tietoturvariski!0> Tämä yhteys voi olla alttiina salakuuntelulle tai välimieshyökkäyksille.\"],\"da9Q/R\":[\"Muutti kanavan asetuksia\"],\"dhJN3N\":[\"Näytä kommentit\"],\"dj2xTE\":[\"Hylkää ilmoitus\"],\"dpCzmC\":[\"Tulvasuojausasetukset\"],\"e9dQpT\":[\"Haluatko avata tämän linkin uudessa välilehdessä?\"],\"ePK91l\":[\"Muokkaa\"],\"eYBDuB\":[\"Lataa kuva tai anna URL, jossa voi käyttää valinnaista \",[\"size\"],\"-muuttujaa dynaamiseen kokoon\"],\"edBbee\":[\"Estä \",[\"username\"],\" hostmaskin perusteella (estää liittymisen samasta IP-osoitteesta/hostista)\"],\"ekfzWq\":[\"Käyttäjäasetukset\"],\"elPDWs\":[\"Mukauta IRC-asiakasohjelmaasi\"],\"eu2osY\":[\"<0>💡 Suositus:0> Jatka vain jos luotat tähän palvelimeen ja ymmärrät riskit. Vältä arkaluonteisten tietojen tai salasanojen jakamista tämän yhteyden kautta.\"],\"euEhbr\":[\"Liity kanavalle \",[\"channel\"],\" napsauttamalla\"],\"ez3vLd\":[\"Ota monirivisyöttö käyttöön\"],\"f0J5Ki\":[\"Palvelimien välinen viestintä voi käyttää salaamattomia yhteyksiä\"],\"f9BHJk\":[\"Varoita käyttäjää\"],\"fDOLLd\":[\"Kanavia ei löydetty.\"],\"ffzDkB\":[\"Anonyymi analytiikka:\"],\"fq1GF9\":[\"Näytä kun käyttäjät katkaisevat yhteyden palvelimeen\"],\"gCldcN\":[\"Vaihda korostusväriä\"],\"gEF57C\":[\"Tämä palvelin tukee vain yhtä yhteystyyppiä\"],\"gJuLUI\":[\"Estolistа\"],\"gNzMrk\":[\"Nykyinen avatar\"],\"gjPWyO\":[\"Syötä nimimerkki...\"],\"gz6UQ3\":[\"Suurenna\"],\"h6/IMX\":[\"Lisää ensimmäinen verkkosi\"],\"h6razj\":[\"Sulje pois kanavan nimimaski\"],\"hG6jnw\":[\"Aihetta ei ole asetettu\"],\"hG89Ed\":[\"Kuva\"],\"hYgDIe\":[\"Luo\"],\"hZ6znB\":[\"Portti\"],\"ha+Bz5\":[\"esim. 100:1440\"],\"he3ygx\":[\"Kopioi\"],\"hehnjM\":[\"Määrä\"],\"hzdLuQ\":[\"Vain käyttäjät, joilla on voice tai korkeampi, voivat puhua\"],\"i0qMbr\":[\"Koti\"],\"iDNBZe\":[\"Ilmoitukset\"],\"iH8pgl\":[\"Takaisin\"],\"iL9SZg\":[\"Estä käyttäjä (nimimerkin perusteella)\"],\"iNt+3c\":[\"Takaisin kuvaan\"],\"iQvi+a\":[\"Älä varoita minua tämän palvelimen heikosta linkkiturvallisuudesta\"],\"iSLIjg\":[\"Yhdistä\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Palvelimen osoite\"],\"idD8Ev\":[\"Tallennettu\"],\"iivqkW\":[\"Kirjautunut\"],\"ij+Elv\":[\"Kuvan esikatselu\"],\"ilIWp7\":[\"Näytä/piilota ilmoitukset\"],\"iuaqvB\":[\"Käytä * jokerimerkkinä. Esimerkkejä: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Botti\"],\"j2DGR0\":[\"Estä hostmaskin perusteella\"],\"jA4uoI\":[\"Aihe:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Syy (valinnainen)\"],\"jUV7CU\":[\"Lataa avatar\"],\"jW5Uwh\":[\"Hallinnoi ulkoisen median lataamista. Pois / Turvallinen / Luotetut lähteet / Kaikki sisältö.\"],\"jXzms5\":[\"Liitteet-valinnat\"],\"jZlrte\":[\"Väri\"],\"jfC/xh\":[\"Yhteystiedot\"],\"jywMpv\":[\"#uusi-kanavan-nimi\"],\"k112DD\":[\"Lataa vanhempia viestejä\"],\"k3ID0F\":[\"Suodata jäseniä…\"],\"k65gsE\":[\"Syvempi tarkastelu\"],\"k7Zgob\":[\"Peruuta yhteys\"],\"kAVx5h\":[\"Kutsuja ei löydetty\"],\"kCLEPU\":[\"Yhdistetty palvelimeen\"],\"kF5LKb\":[\"Estetyt mallit:\"],\"kGeOx/\":[\"Liity kanavalle \",[\"0\"]],\"kITKr8\":[\"Ladataan kanavan tiloja...\"],\"kPpPsw\":[\"Olet IRC-operaattori\"],\"kWJmRL\":[\"Sinä\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Kopioi JSON\"],\"krViRy\":[\"Napsauta kopioidaksesi JSON-muodossa\"],\"ks71ra\":[\"Poikkeukset\"],\"kw4lRv\":[\"Kanavan puolioperaattori\"],\"kxgIRq\":[\"Valitse kanava tai lisää uusi päästäksesi alkuun.\"],\"ky6dWe\":[\"Avatarin esikatselu\"],\"l+GxCv\":[\"Ladataan kanavia...\"],\"l+IUVW\":[\"Tilin \",[\"account\"],\" vahvistus onnistui: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"yhdisti uudelleen\"],\"other\":[\"yhdisti uudelleen \",[\"reconnectCount\"],\" kertaa\"]}]],\"l1l8sj\":[[\"0\"],\"pv sitten\"],\"l5NhnV\":[\"#kanava (valinnainen)\"],\"l5jmzx\":[[\"0\"],\" ja \",[\"1\"],\" kirjoittavat...\"],\"lCF0wC\":[\"Päivitä\"],\"lHy8N5\":[\"Ladataan lisää kanavia...\"],\"lasgrr\":[\"käytetty\"],\"lbpf14\":[\"Liity kanavaan \",[\"value\"]],\"lfFsZ4\":[\"Kanavat\"],\"lkNdiH\":[\"Tilin nimi\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Lataa kuva\"],\"loQxaJ\":[\"Olen takaisin\"],\"lvfaxv\":[\"KOTI\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Lisää\"],\"m8flAk\":[\"Esikatselu (ei vielä lähetetty)\"],\"mEPxTp\":[\"<0>⚠️ Ole varovainen!0> Avaa linkkejä vain luotetuista lähteistä. Haitalliset linkit voivat vaarantaa tietoturvasi tai yksityisyytesi.\"],\"mHGdhG\":[\"Palvelimen tiedot\"],\"mHS8lb\":[\"Viesti #\",[\"0\"]],\"mMYBD9\":[\"Laaja – laajempi suojauslaajuus\"],\"mTGsPd\":[\"Kanavan aihe\"],\"mU8j6O\":[\"Ei ulkoisia viestejä (+n)\"],\"mZp8FL\":[\"Automaattinen palautuminen yksiriviseksi\"],\"mdQu8G\":[\"SinunNimimerkkisi\"],\"miSSBQ\":[\"Kommentit (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Käyttäjä on tunnistautunut\"],\"mwtcGl\":[\"Sulje kommentit\"],\"myL0MR\":[\"Poistetaanko tämä verkko?\"],\"mzI/c+\":[\"Lataa\"],\"n3fGRk\":[\"asettaja: \",[\"0\"]],\"n5+j9l\":[\"esim. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Löysä – vähemmän aggressiivinen suojaus\"],\"nNflMD\":[\"Poistu kanavalta\"],\"nPXkBi\":[\"Ladataan WHOIS-tietoja...\"],\"nQnxxF\":[\"Viesti #\",[\"0\"],\" (Shift+Enter = uusi rivi)\"],\"nWMRxa\":[\"Irrota kiinnitys\"],\"nkC032\":[\"Ei tulvasuojausprofiilia\"],\"o69z4d\":[\"Lähetä varoitusviesti käyttäjälle \",[\"username\"]],\"o9ylQi\":[\"Hae GIF-kuvia aloittaaksesi\"],\"oFGkER\":[\"Palvelintiedotteet\"],\"oOi11l\":[\"Siirry loppuun\"],\"oPYIL5\":[\"verkko\"],\"oQEzQR\":[\"Uusi DM\"],\"oXOSPE\":[\"Verkossa\"],\"oal760\":[\"Välimieshyökkäykset palvelinlinkeissä ovat mahdollisia\"],\"oeqmmJ\":[\"Luotetut lähteet\"],\"optX0N\":[[\"0\"],\"t sitten\"],\"ovBPCi\":[\"Oletus\"],\"p0Z69r\":[\"Malli ei voi olla tyhjä\"],\"p1KgtK\":[\"Äänen lataaminen epäonnistui\"],\"p59pEv\":[\"Lisätiedot\"],\"p7sRI6\":[\"Ilmoita muille, kun kirjoitat\"],\"pBm1od\":[\"Salainen kanava\"],\"pNmiXx\":[\"Oletusnimimerkkisi kaikille palvelimille\"],\"pUUo9G\":[\"Isäntänimi:\"],\"pVGPmz\":[\"Tilin salasana\"],\"peNE68\":[\"Pysyvä\"],\"plhHQt\":[\"Ei tietoja\"],\"pm6+q5\":[\"Tietoturvavaroitus\"],\"pn5qSs\":[\"Lisätiedot\"],\"q0cR4S\":[\"on nyt tunnettu nimellä **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanava ei näy LIST- tai NAMES-komennoissa\"],\"qLpTm/\":[\"Poista reaktio \",[\"emoji\"]],\"qVkGWK\":[\"Kiinnitä\"],\"qY8wNa\":[\"Kotisivu\"],\"qb0xJ7\":[\"Käytä jokerimerkkejä: * vastaa mitä tahansa merkkijonoa, ? vastaa yhtä merkkiä. Esimerkkejä: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Kanavan avain (+k)\"],\"qtoOYG\":[\"Ei rajaa\"],\"r1W2AS\":[\"Tiedostopalvelimen kuva\"],\"rIPR2O\":[\"Aihe asetettu aiemmin kuin (min sitten)\"],\"rMMSYo\":[\"Enimmäispituus on \",[\"0\"]],\"rWtzQe\":[\"Verkko jakautui ja yhdistyi uudelleen. ✅\"],\"rYG2u6\":[\"Odota hetki...\"],\"rdUucN\":[\"Esikatselu\"],\"rjGI/Q\":[\"Yksityisyys\"],\"rk8iDX\":[\"Ladataan GIF-kuvia...\"],\"rn6SBY\":[\"Poista mykistys\"],\"s/UKqq\":[\"Poistettiin kanavalta\"],\"s7oqXR\":[\"Valitse verkko\"],\"s8cATI\":[\"liittyi kanavalle \",[\"channelName\"]],\"sCO9ue\":[\"Yhteydessä palvelimeen <0>\",[\"serverName\"],\"0> on seuraavia tietoturvaongelmia:\"],\"sGH11W\":[\"Palvelin\"],\"sHI1H+\":[\"on nyt tunnettu nimellä **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" kutsui sinut liittymään kanavalle \",[\"channel\"]],\"sUBSbK\":[\"Ei vielä ylävirran verkkoja.\"],\"sby+1/\":[\"Kopioi napsauttamalla\"],\"sfN25C\":[\"Oikea nimesi tai koko nimesi\"],\"sliuzR\":[\"Avaa linkki\"],\"sqrO9R\":[\"Mukautetut maininnat\"],\"sr6RdJ\":[\"Monirivinen Shift+Enter-painikkeella\"],\"swrCpB\":[\"Kanava on nimetty uudelleen nimestä \",[\"oldName\"],\" nimeen \",[\"newName\"],\" käyttäjän \",[\"user\"],\" toimesta\",[\"0\"]],\"sxkWRg\":[\"Lisäasetukset\"],\"t/YqKh\":[\"Poista\"],\"t47eHD\":[\"Yksilöllinen tunnuksesi tällä palvelimella\"],\"tAkAh0\":[\"URL, jossa valinnainen \",[\"size\"],\"-muuttuja dynaamista kokoa varten. Esimerkki: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Näytä tai piilota kanavaluettelon sivupalkki\"],\"tfDRzk\":[\"Tallenna\"],\"tiBsJk\":[\"poistui kanavalta \",[\"channelName\"]],\"tt4/UD\":[\"poistui (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Nimimerkki {nick} on jo käytössä, yritetään uudelleen nimellä {newNick}\"],\"u0a8B4\":[\"Tunnistaudu IRC-operaattoriksi ylläpito-oikeuksia varten\"],\"u0rWFU\":[\"Luotu myöhemmin kuin (min sitten)\"],\"u72w3t\":[\"Estettävät käyttäjät ja mallit\"],\"u7jc2L\":[\"poistui\"],\"uAQUqI\":[\"Tila\"],\"uB85T3\":[\"Tallennus epäonnistui: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC-palvelimet:\"],\"ukyW4o\":[\"Kutsulinkkisi\"],\"usSSr/\":[\"Zoomaustaso\"],\"v7uvcf\":[\"Ohjelmisto:\"],\"vE8kb+\":[\"Käytä Shift+Enter uudelle riville (Enter lähettää)\"],\"vERlcd\":[\"Profiili\"],\"vK0RL8\":[\"Ei aihetta\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Kieli\"],\"vaHYxN\":[\"Oikea nimi\"],\"vhjbKr\":[\"Poissa\"],\"w/nogd\":[[\"0\"],\" verkko\",[\"1\"],\" — valitse yksi liittyäksesi\"],\"w4NYox\":[[\"title\"],\" asiakasohjelma\"],\"w8xQRx\":[\"Virheellinen arvo\"],\"wFjjxZ\":[\"potkittiin kanavalta \",[\"channelName\"],\" käyttäjän \",[\"username\"],\" toimesta (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Porttikieltopoikkeuksia ei löydetty\"],\"wPrGnM\":[\"Kanavan ylläpitäjä\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Näytä kun käyttäjät liittyvät kanavalle tai poistuvat\"],\"whqZ9r\":[\"Lisäsanat tai -lauseet korostettavaksi\"],\"wm7RV4\":[\"Ilmoitusääni\"],\"wz/Yoq\":[\"Viestisi voidaan siepata palvelimien välillä välitettäessä\"],\"x3+y8b\":[\"Tämän verran ihmisiä on rekisteröitynyt tämän linkin kautta\"],\"xCJdfg\":[\"Tyhjennä\"],\"xOTzt5\":[\"juuri nyt\"],\"xUHRTR\":[\"Tunnistaudu automaattisesti operaattoriksi yhdistäessä\"],\"xWHwwQ\":[\"Estot\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"Tämä palvelin ei tue kutsulinkkejä (<0>obby.world/invitation0>-kykyä ei ilmoiteta). Voit silti chattailla normaalisti; tämä paneeli on obbyircd-pohjaisia verkkoja varten.\"],\"xceQrO\":[\"Vain suojatut WebSocket-yhteydet ovat tuettuja\"],\"xdtXa+\":[\"kanavan-nimi\"],\"xfXC7q\":[\"Tekstikanavat\"],\"xlCYOE\":[\"Haetaan lisää viestejä...\"],\"xlhswE\":[\"Vähimmäisarvo on \",[\"0\"]],\"xq97Ci\":[\"Lisää sana tai lause...\"],\"xuRqRq\":[\"Käyttäjäraja (+l)\"],\"xwF+7J\":[[\"0\"],\" kirjoittaa...\"],\"y1eoq1\":[\"Kopioi linkki\"],\"yJztBY\":[\"Poista verkko\"],\"yNeucF\":[\"Tämä palvelin ei tue laajennettua profiilimetadataa (IRCv3 METADATA -laajennus). Lisäkentät kuten avatar, näyttönimi ja tila eivät ole käytettävissä.\"],\"yPlrca\":[\"Kanavan avatar\"],\"yQE2r9\":[\"Ladataan\"],\"ySU+JY\":[\"sinun@sahkoposti.fi\"],\"yTX1Rt\":[\"Oper-käyttäjänimi\"],\"yYOzWD\":[\"lokit\"],\"yfx9Re\":[\"IRC-operaattorin salasana\"],\"ygCKqB\":[\"Pysäytä\"],\"ymDxJx\":[\"IRC-operaattorin käyttäjänimi\"],\"yrpRsQ\":[\"Lajittele nimen mukaan\"],\"yz7wBu\":[\"Sulje\"],\"zJw+jA\":[\"asettaa tilan: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"min sitten\"],\"zebeLu\":[\"Syötä oper-käyttäjänimi\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/fi/messages.po b/src/locales/fi/messages.po
index 44b632c1..5570e27c 100644
--- a/src/locales/fi/messages.po
+++ b/src/locales/fi/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - Viedään IRC tulevaisuuteen"
msgid "— open in viewer"
msgstr "— avaa katseluohjelmassa"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(peritty)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(muuttumaton)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} ja {1} kirjoittavat..."
msgid "{0} is typing..."
msgstr "{0} kirjoittaa..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} verkko{1} — valitse yksi liittyäksesi"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -205,6 +221,12 @@ msgstr "Lisää kutsumask (esim. nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "Lisää IRC-palvelin"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Lisää verkko"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Lisää sääntö"
msgid "Add Server"
msgstr "Lisää palvelin"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Lisää ensimmäinen verkkosi"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Lisätiedot"
@@ -377,6 +403,10 @@ msgstr "Takaisin"
msgid "Back to image"
msgstr "Takaisin kuvaan"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Takaisin verkkolistalle"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "Estä {username} hostmaskin perusteella (estää liittymisen samasta IP-osoitteesta/hostista)"
@@ -424,6 +454,10 @@ msgstr "Selaa kaikkia palvelimen kanavia"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Peruuta yhteys"
msgid "Cancel reply"
msgstr "Peruuta vastaus"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Vaihda korostusväriä"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Muuta kanavan nimeä (vain operaattorit)"
@@ -606,6 +645,8 @@ msgstr "Käyttäjäraja (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -669,6 +710,7 @@ msgid "Confirm?"
msgstr "Vahvista?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Yhdistä"
@@ -747,7 +789,7 @@ msgstr "Luo uusi kutsulinkki"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "Luo ja hallinnoi kutsulinkkejäsi"
+msgstr "Luo ja hallinnoi kutsulinkkejäsi"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,27 +856,52 @@ msgstr "Poista kanava"
msgid "Delete message"
msgstr "Poista viesti"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Poista verkko"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Poista yksityiskeskustelu"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "Poista tämä kutsu"
+msgstr "Poista tämä kutsu"
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "Poistetaanko tämä viesti? Toimintoa ei voi kumota."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Poistetaanko tämä verkko?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
msgstr "Kuvaus (valinnainen, esim. \"Beta-testaajat Q3\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Katkaise yhteys"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "Katkaistaanko <0>{0}0> -yhteys?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Katkaistaanko soju-bouncerin yhteys?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Katkaistaanko verkon yhteys?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Selaa"
@@ -891,20 +958,31 @@ msgstr "Lataa"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "Pudota tiedostot lähettääksesi"
+msgstr "Pudota tiedostot lähettääksesi"
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "esim. <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "esim. 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Muokkaa"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "Muokkaa: {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Muokkaa profiilia"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "KOTI"
msgid "Homepage"
msgstr "Kotisivu"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Isäntä"
@@ -1323,7 +1402,7 @@ msgstr "Poistu kanavalta"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Jätä kanava tyhjäksi yleistä verkkokutsua varten. Kuvaus on vain omaa kirjanpitoasi varten — näkyvissä vain sinulle tässä listassa."
+msgstr "Jätä kanava tyhjäksi yleistä verkkokutsua varten. Kuvaus on vain omaa kirjanpitoasi varten â näkyvissä vain sinulle tässä listassa."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "Poistui kanavalta"
msgid "Let others know when you are typing"
msgstr "Ilmoita muille, kun kirjoitat"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Linkin esikatselu"
@@ -1375,6 +1458,10 @@ msgstr "Ladataan GIF-kuvia..."
msgid "Loading more channels..."
msgstr "Ladataan lisää kanavia..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Ladataan verkkoja bouncerista…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "Ladataan WHOIS-tietoja..."
@@ -1565,10 +1652,21 @@ msgstr "Nimi:"
msgid "network"
msgstr "verkko"
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Verkko sidottu soju-bouncerin kautta"
+
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Verkon nimi"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Verkot palvelimella {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Uusi DM"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!käyttäjä@isäntä (esim. spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Ei valittua tiedostoa"
msgid "No flood profile"
msgstr "Ei tulvasuojausprofiilia"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "isäntää ei asetettu"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Kutsuja ei löydetty"
@@ -1672,7 +1775,7 @@ msgstr "Median esikatseluja ei ladata."
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "Raakaa IRC-liikennettä ei ole vielä tallennettu. Yritä yhdistää tai lähettää viesti."
+msgstr "Raakaa IRC-liikennettä ei ole vielä tallennettu. Yritä yhdistää tai lähettää viesti."
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "Tuloksia ei löydetty"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "Yhtään palvelinta ei ole valittu. Valitse ensin palvelin sivupalkista; kutsulinkkejä hallitaan palvelinkohtaisesti."
+msgstr "Yhtään palvelinta ei ole valittu. Valitse ensin palvelin sivupalkista; kutsulinkkejä hallitaan palvelinkohtaisesti."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "Aihetta ei ole asetettu"
msgid "No unread mentions or messages"
msgstr "Ei lukemattomia mainintoja tai viestejä"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Ei vielä ylävirran verkkoja."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Ei käyttäjiä saatavilla"
@@ -1784,6 +1891,10 @@ msgstr "Hups! Verkon jako! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Avaa"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Avaa kanavan asetukset"
@@ -1887,6 +1998,10 @@ msgstr "Kiinnitä yksityiskeskustelu"
msgid "Pin this private message conversation"
msgstr "Kiinnitä tämä yksityisviesteistä"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Salaamaton"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "Lähetä viesti käyttäjälle"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Portti"
@@ -2006,6 +2122,7 @@ msgstr "reagoi tähän viestiin"
msgid "Read more"
msgstr "Lue lisää"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Syy"
msgid "Reason (optional)"
msgstr "Syy (valinnainen)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Yhdistä uudelleen palvelimeen"
@@ -2031,7 +2149,7 @@ msgstr "Yhdistä uudelleen palvelimeen"
#: src/components/ui/InvitationsPanel.tsx
#: src/components/ui/InvitationsPanel.tsx
msgid "Refresh"
-msgstr "Päivitä"
+msgstr "Päivitä"
#: src/components/ui/AddServerModal.tsx
msgid "Register for an account"
@@ -2095,6 +2213,7 @@ msgstr "Säännöt"
msgid "Safe"
msgstr "Turvallinen"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Selaa"
msgid "Select a channel"
msgstr "Valitse kanava"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Valitse verkko"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Valitse jäsen"
@@ -2276,6 +2399,10 @@ msgstr "Verkon palvelinoperaattorit voisivat mahdollisesti lukea viestisi"
msgid "Server Password"
msgstr "Palvelimen salasana"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Palvelimen salasana (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Palvelimien välinen viestintä voi käyttää salaamattomia yhteyksiä"
@@ -2380,6 +2507,12 @@ msgstr "Kirjautunut"
msgid "Software:"
msgstr "Ohjelmisto:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "soju-bouncer (hallinta)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Lajittele nimen mukaan"
@@ -2457,7 +2590,11 @@ msgstr "Tämä kuva on vanhentunut"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "Tämän verran ihmisiä on rekisteröitynyt tämän linkin kautta"
+msgstr "Tämän verran ihmisiä on rekisteröitynyt tämän linkin kautta"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Tämä poistaa verkon soju-bouncerista. Käyttääksesi sitä uudelleen, sinun täytyy lisätä se takaisin."
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "Tämä palvelin ei tue laajennettua profiilimetadataa (IRCv3 METADATA -l
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Tämä palvelin ei tue kutsulinkkejä (<0>obby.world/invitation0>-kykyä ei ilmoiteta). Voit silti chattailla normaalisti; tämä paneeli on obbyircd-pohjaisia verkkoja varten."
+msgstr "Tämä palvelin ei tue kutsulinkkejä (<0>obby.world/invitation0>-kykyä ei ilmoiteta). Voit silti chattailla normaalisti; tämä paneeli on obbyircd-pohjaisia verkkoja varten."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Tämä palvelin tukee vain yhtä yhteystyyppiä"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Tämä sulkee myös alla olevat {0} sidottua verkkoa."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Tämä sulkee myös alla olevan sidotun verkon."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Aika (min)"
@@ -2479,6 +2625,10 @@ msgstr "Aika (min)"
msgid "Time Window (seconds)"
msgstr "Aikaikkuna (sekuntia)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Aihe:"
msgid "Total: {0}"
msgstr "Yhteensä: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Siirtoprotokolla"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Luotetut lähteet"
@@ -2615,7 +2769,7 @@ msgstr "Käytä jokerimerkkejä: * vastaa mitä tahansa merkkijonoa, ? vastaa yh
#: src/components/ui/InvitationsPanel.tsx
msgid "used"
-msgstr "käytetty"
+msgstr "käytetty"
#: src/components/message/JsonLogMessage.tsx
#: src/components/ui/UserProfileModal.tsx
@@ -2641,6 +2795,7 @@ msgstr "Käyttäjäprofiili"
msgid "User Settings"
msgstr "Käyttäjäasetukset"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Laaja – laajempi suojauslaajuus"
msgid "Will default to 'no reason' if left empty"
msgstr "Oletusarvo on 'ei syytä', jos jätetään tyhjäksi"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Kyllä, poista"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "Sinulla on tallentamattomia muutoksia. Haluatko varmasti sulkea tallenta
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "Et ole vielä luonut yhtään kutsulinkkiä. Käytä yllä olevaa lomaketta luodaksesi ensimmäisen."
+msgstr "Et ole vielä luonut yhtään kutsulinkkiä. Käytä yllä olevaa lomaketta luodaksesi ensimmäisen."
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "Kutsuit {target} liittymään kanavalle {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Olet yhteydessä palvelimeen <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Tilin salasana tunnistautumista varten"
@@ -2822,6 +2986,10 @@ msgstr "Tilin salasana tunnistautumista varten"
msgid "Your account username for authentication"
msgstr "Tilin käyttäjänimi tunnistautumista varten"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Bouncerissasi ei ole vielä verkkoja. Lisää yksi päästäksesi alkuun."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Oletusnimimerkkisi kaikille palvelimille"
diff --git a/src/locales/fr/messages.mjs b/src/locales/fr/messages.mjs
index 26d689bd..4b01d74f 100644
--- a/src/locales/fr/messages.mjs
+++ b/src/locales/fr/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Format de modèle invalide. Utilisez le format nick!user@host (jokers * autorisés)\"],\"+6NQQA\":[\"Canal d'assistance générale\"],\"+6NyRG\":[\"Client\"],\"+K0AvT\":[\"Déconnecter\"],\"+cyFdH\":[\"Message par défaut pour le statut absent\"],\"+mVPqU\":[\"Afficher le formatage Markdown dans les messages\"],\"+vqCJH\":[\"Votre nom d'utilisateur de compte pour l'authentification\"],\"+yPBXI\":[\"Choisir un fichier\"],\"+zy2Nq\":[\"Type\"],\"/09cao\":[\"Faible sécurité du lien (niveau \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Les utilisateurs extérieurs ne peuvent pas envoyer de messages\"],\"/4C8U0\":[\"Tout copier\"],\"/6BzZF\":[\"Afficher/masquer la liste des membres\"],\"/AkXyp\":[\"Confirmer ?\"],\"/TNOPk\":[\"L'utilisateur est absent\"],\"/XQgft\":[\"Découvrir\"],\"/cF7Rs\":[\"Volume\"],\"/dqduX\":[\"Page suivante\"],\"/fc3q4\":[\"Tout le contenu\"],\"/kISDh\":[\"Activer les sons de notification\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Jouer des sons pour les mentions et messages\"],\"0/0ZGA\":[\"Masque du nom de salon\"],\"0D6j7U\":[\"En savoir plus sur les règles personnalisées →\"],\"0XsHcR\":[\"Expulser l'utilisateur\"],\"0ZpE//\":[\"Trier par utilisateurs\"],\"0bEPwz\":[\"Se mettre absent\"],\"0dGkPt\":[\"Développer la liste des canaux\"],\"0gS7M5\":[\"Nom d'affichage\"],\"0kS+M8\":[\"ExempleRÉSEAU\"],\"0rgoY7\":[\"Se connecter uniquement aux serveurs choisis\"],\"0wdd7X\":[\"Rejoindre\"],\"0wkVYx\":[\"Messages privés\"],\"111uHX\":[\"Aperçu du lien\"],\"196EG4\":[\"Supprimer la conversation privée\"],\"1DSr1i\":[\"Créer un compte\"],\"1O/24y\":[\"Afficher/masquer la liste des canaux\"],\"1VPJJ2\":[\"Avertissement de lien externe\"],\"1ZC/dv\":[\"Aucune mention ou message non lu\"],\"1pO1zi\":[\"Le nom du serveur est requis\"],\"1uwfzQ\":[\"Voir le sujet du canal\"],\"268g7c\":[\"Saisir le nom d'affichage\"],\"2F9+AZ\":[\"Aucun trafic IRC brut capturé pour le moment. Essayez de vous connecter ou d'envoyer un message.\"],\"2FOFq1\":[\"Les opérateurs du réseau pourraient potentiellement lire vos messages\"],\"2FYpfJ\":[\"Plus\"],\"2HF1Y2\":[[\"inviter\"],\" a invité \",[\"target\"],\" à rejoindre \",[\"channel\"]],\"2I70QL\":[\"Voir les informations du profil utilisateur\"],\"2QYdmE\":[\"Utilisateurs :\"],\"2QpEjG\":[\"a quitté\"],\"2YE223\":[\"Message dans #\",[\"0\"],\" (Entrée pour nouvelle ligne, Maj+Entrée pour envoyer)\"],\"2bimFY\":[\"Utiliser le mot de passe du serveur\"],\"2iTmdZ\":[\"Stockage local :\"],\"2odkwe\":[\"Strict – Protection plus agressive\"],\"2uDhbA\":[\"Saisir le nom d'utilisateur à inviter\"],\"2ygf/L\":[\"← Retour\"],\"2zEgxj\":[\"Rechercher des GIFs...\"],\"3RdPhl\":[\"Renommer le canal\"],\"3THokf\":[\"Utilisateur avec droit de parole\"],\"3TSz9S\":[\"Réduire\"],\"3jBDvM\":[\"Nom d'affichage du salon\"],\"3ryuFU\":[\"Rapports de plantage optionnels pour améliorer l'application\"],\"3uBF/8\":[\"Fermer le visualiseur\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Entrez le nom du compte...\"],\"4/Rr0R\":[\"Inviter un utilisateur dans le canal actuel\"],\"4EZrJN\":[\"Règles\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Profil de flood (+F)\"],\"4RZQRK\":[\"Qu'est-ce que vous faites ?\"],\"4hfTrB\":[\"Pseudo\"],\"4n99LO\":[\"Déjà dans \",[\"0\"]],\"4t6vMV\":[\"Passer automatiquement en mode ligne unique pour les messages courts\"],\"4vsHmf\":[\"Temps (min)\"],\"5+INAX\":[\"Surligner les messages qui vous mentionnent\"],\"5R5Pv/\":[\"Nom Oper\"],\"678PKt\":[\"Nom du réseau\"],\"6Aih4U\":[\"Hors ligne\"],\"6CO3WE\":[\"Mot de passe requis pour rejoindre le salon. Laissez vide pour supprimer la clé.\"],\"6HhMs3\":[\"Message de déconnexion\"],\"6V3Ea3\":[\"Copié\"],\"6lGV3K\":[\"Afficher moins\"],\"6yFOEi\":[\"Entrez le mot de passe oper...\"],\"7+IHTZ\":[\"Aucun fichier choisi\"],\"73hrRi\":[\"nick!user@host (ex. : spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Envoyer un message privé\"],\"7U1W7c\":[\"Très détendu\"],\"7Y1YQj\":[\"Nom réel :\"],\"7YHArF\":[\"— ouvrir dans le visualiseur\"],\"7fjnVl\":[\"Rechercher des utilisateurs...\"],\"7jL88x\":[\"Supprimer ce message ? Cette action est irréversible.\"],\"7nGhhM\":[\"À quoi pensez-vous ?\"],\"7sEpu1\":[\"Membres — \",[\"0\"]],\"7sNhEz\":[\"Nom d'utilisateur\"],\"8H0Q+x\":[\"En savoir plus sur les profils →\"],\"8Phu0A\":[\"Afficher quand des utilisateurs changent de pseudo\"],\"8XTG9e\":[\"Saisir le mot de passe oper\"],\"8XsV2J\":[\"Réessayer l'envoi\"],\"8ZsakT\":[\"Mot de passe\"],\"8kR84m\":[\"Vous êtes sur le point d'ouvrir un lien externe :\"],\"8lCgih\":[\"Supprimer la règle\"],\"8o3dPc\":[\"Déposez les fichiers pour les téléverser\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"a rejoint\"],\"other\":[\"a rejoint \",[\"joinCount\"],\" fois\"]}]],\"9BMLnJ\":[\"Se reconnecter au serveur\"],\"9OEgyT\":[\"Ajouter une réaction\"],\"9PQ8m2\":[\"G-Line (bannissement global)\"],\"9Qs99X\":[\"E-mail :\"],\"9QupBP\":[\"Supprimer le motif\"],\"9bG48P\":[\"Envoi en cours\"],\"9f5f0u\":[\"Questions sur la confidentialité ? Contactez-nous :\"],\"9unqs3\":[\"Absent :\"],\"9v3hwv\":[\"Aucun serveur trouvé.\"],\"9zb2WA\":[\"Connexion en cours\"],\"A1taO8\":[\"Rechercher\"],\"A2adVi\":[\"Envoyer des notifications de frappe\"],\"A9Rhec\":[\"Nom du salon\"],\"AWOSPo\":[\"Zoomer\"],\"AXSpEQ\":[\"Oper à la connexion\"],\"AeXO77\":[\"Compte\"],\"AhNP40\":[\"Avancer\"],\"Ai2U7L\":[\"Hôte\"],\"AjBQnf\":[\"Pseudo modifié\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Annuler la réponse\"],\"ApSx0O\":[[\"0\"],\" messages trouvés correspondant à \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Aucun résultat trouvé\"],\"AyNqAB\":[\"Afficher tous les événements serveur dans le chat\"],\"B/QqGw\":[\"Absent du clavier\"],\"B8AaMI\":[\"Ce champ est obligatoire\"],\"BA2c49\":[\"Le serveur ne supporte pas le filtrage LIST avancé\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" et \",[\"3\"],\" autres sont en train d'écrire...\"],\"BGul2A\":[\"Vous avez des modifications non enregistrées. Voulez-vous vraiment fermer sans enregistrer ?\"],\"BIf9fi\":[\"Votre message de statut\"],\"BPm98R\":[\"Aucun serveur sélectionné. Choisissez d'abord un serveur dans la barre latérale ; les liens d'invitation sont gérés par serveur.\"],\"BZz3md\":[\"Votre site web personnel\"],\"Bgm/H7\":[\"Permettre la saisie sur plusieurs lignes\"],\"BiQIl1\":[\"Épingler cette conversation privée\"],\"BlNZZ2\":[\"Cliquez pour aller au message\"],\"Bowq3c\":[\"Seuls les opérateurs peuvent modifier le sujet\"],\"Btozzp\":[\"Cette image a expiré\"],\"Bycfjm\":[\"Total : \",[\"0\"]],\"C6IBQc\":[\"Copier le JSON complet\"],\"C9L9wL\":[\"Collecte de données\"],\"CDq4wC\":[\"Modérer l'utilisateur\"],\"CHVRxG\":[\"Message à @\",[\"0\"],\" (Maj+Entrée pour nouvelle ligne)\"],\"CN9zdR\":[\"Le nom oper et le mot de passe sont requis\"],\"CW3sYa\":[\"Ajouter la réaction \",[\"emoji\"]],\"CaAkqd\":[\"Afficher les déconnexions\"],\"CbvaYj\":[\"Bannir par pseudo\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Sélectionner un canal\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"Système\"],\"D28t6+\":[\"a rejoint et quitté\"],\"DB8zMK\":[\"Appliquer\"],\"DBcWHr\":[\"Fichier son de notification personnalisé\"],\"DTy9Xw\":[\"Aperçus des médias\"],\"Dj4pSr\":[\"Choisissez un mot de passe sécurisé\"],\"Du+zn+\":[\"Recherche...\"],\"Du2T2f\":[\"Paramètre introuvable\"],\"DwsSVQ\":[\"Appliquer les filtres & Actualiser\"],\"E3W/zd\":[\"Pseudo par défaut\"],\"E6nRW7\":[\"Copier l'URL\"],\"E703RG\":[\"Modes :\"],\"EAeu1Z\":[\"Envoyer l'invitation\"],\"EFKJQT\":[\"Paramètre\"],\"EGPQBv\":[\"Règles de flood personnalisées (+f)\"],\"ELik0r\":[\"Voir la politique de confidentialité complète\"],\"EPbeC2\":[\"Voir ou modifier le sujet du canal\"],\"EQCDNT\":[\"Entrez le nom d'utilisateur oper...\"],\"EUvulZ\":[\"1 message trouvé correspondant à \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Image suivante\"],\"EdQY6l\":[\"Aucun\"],\"EnqLYU\":[\"Rechercher des serveurs...\"],\"F0OKMc\":[\"Modifier le serveur\"],\"F6Int2\":[\"Activer les surlignages\"],\"FDoLyE\":[\"Utilisateurs max.\"],\"FUU/hZ\":[\"Contrôle la quantité de médias externes chargés dans le chat.\"],\"Fdp03t\":[\"activé\"],\"FfPWR0\":[\"Modal\"],\"FjkaiT\":[\"Dézoomer\"],\"FlqOE9\":[\"Ce que cela signifie :\"],\"FolHNl\":[\"Gérez votre compte et l'authentification\"],\"Fp2Dif\":[\"A quitté le serveur\"],\"G5KmCc\":[\"GZ-Line (Z-Line globale)\"],\"GDs0lz\":[\"<0>Risque :0> Des informations sensibles (messages, conversations privées, identifiants de connexion) pourraient être exposées aux administrateurs réseau ou à des attaquants positionnés entre les serveurs IRC.\"],\"GR+2I3\":[\"Ajouter un masque d'invitation (ex. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Fermer les notifications serveur détachées\"],\"GdhD7H\":[\"Cliquez à nouveau pour confirmer\"],\"GlHnXw\":[\"Échec du changement de pseudo: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Aperçu :\"],\"GtmO8/\":[\"de\"],\"GtuHUQ\":[\"Renommer ce salon sur le serveur. Tous les utilisateurs verront le nouveau nom.\"],\"GuGfFX\":[\"Activer/désactiver la recherche\"],\"GxkJXS\":[\"Téléversement...\"],\"GzbwnK\":[\"A rejoint le canal\"],\"GzsUDB\":[\"Profil étendu\"],\"H/PnT8\":[\"Insérer un emoji\"],\"H6Izzl\":[\"Votre code couleur préféré\"],\"H9jIv+\":[\"Afficher les entrées/sorties\"],\"HAKBY9\":[\"Télécharger des fichiers\"],\"HdE1If\":[\"Canal\"],\"Hk4AW9\":[\"Votre nom d'affichage préféré\"],\"HmHDk7\":[\"Sélectionner un membre\"],\"HrQzPU\":[\"Canaux sur \",[\"networkName\"]],\"I2tXQ5\":[\"Message à @\",[\"0\"],\" (Entrée pour nouvelle ligne, Maj+Entrée pour envoyer)\"],\"I6bw/h\":[\"Bannir l'utilisateur\"],\"I92Z+b\":[\"Activer les notifications\"],\"I9D72S\":[\"Êtes-vous sûr de vouloir supprimer ce message ? Cette action est irréversible.\"],\"IA+1wo\":[\"Afficher quand des utilisateurs sont expulsés des salons\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info :\"],\"IUwGEM\":[\"Enregistrer les modifications\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" et \",[\"2\"],\" sont en train d'écrire...\"],\"IgrLD/\":[\"Pause\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Répondre\"],\"IoHMnl\":[\"La valeur maximale est \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Connexion en cours...\"],\"J5T9NW\":[\"Informations utilisateur\"],\"J8Y5+z\":[\"Oups ! La réseau s'est divisé ! ⚠️\"],\"JBHkBA\":[\"A quitté le canal\"],\"JCwL0Q\":[\"Saisir une raison (facultatif)\"],\"JFciKP\":[\"Basculer\"],\"JXGkhG\":[\"Changer le nom du canal (opérateurs uniquement)\"],\"JcD7qf\":[\"Plus d'actions\"],\"JdkA+c\":[\"Secret (+s)\"],\"Jmu12l\":[\"Canaux du serveur\"],\"JvQ++s\":[\"Activer le Markdown\"],\"K2jwh/\":[\"Aucune donnée WHOIS disponible\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Supprimer le message\"],\"KKBlUU\":[\"Intégrer\"],\"KM0pLb\":[\"Bienvenue dans le canal !\"],\"KR6W2h\":[\"Ne plus ignorer l'utilisateur\"],\"KV+Bi1\":[\"Sur invitation uniquement (+i)\"],\"KdCtwE\":[\"Nombre de secondes de surveillance de l'activité de flood avant la réinitialisation des compteurs\"],\"Kkezga\":[\"Mot de passe du serveur\"],\"KsiQ/8\":[\"Les utilisateurs doivent être invités pour rejoindre le salon\"],\"L+gB/D\":[\"Informations sur le salon\"],\"LC1a7n\":[\"Le serveur IRC a signalé que ses liens entre serveurs ont un faible niveau de sécurité. Cela signifie que lorsque vos messages sont relayés entre les serveurs IRC du réseau, ils peuvent ne pas être correctement chiffrés ou les certificats SSL/TLS peuvent ne pas être validés correctement.\"],\"LNfLR5\":[\"Afficher les expulsions\"],\"LQb0W/\":[\"Afficher tous les événements\"],\"LU7/yA\":[\"Nom alternatif pour l'affichage. Peut contenir des espaces, emojis et caractères spéciaux. Le vrai nom (\",[\"channelName\"],\") sera toujours utilisé pour les commandes IRC.\"],\"LUb9O7\":[\"Un port de serveur valide est requis\"],\"LV4fT6\":[\"Description (optionnelle, ex. « Bêta-testeurs T3 »)\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Politique de confidentialité\"],\"LcuSDR\":[\"Gérez les informations de votre profil et vos métadonnées\"],\"LqLS9B\":[\"Afficher les changements de pseudo\"],\"LsDQt2\":[\"Paramètres du canal\"],\"LtI9AS\":[\"Propriétaire\"],\"LuNhhL\":[\"a réagi à ce message\"],\"M/AZNG\":[\"URL de votre image d'avatar\"],\"M/WIer\":[\"Envoyer un message\"],\"M8er/5\":[\"Nom :\"],\"MHk+7g\":[\"Image précédente\"],\"MRorGe\":[\"MP à l'utilisateur\"],\"MVbSGP\":[\"Fenêtre temporelle (secondes)\"],\"MkpcsT\":[\"Vos messages et paramètres sont stockés localement sur votre appareil\"],\"N/hDSy\":[\"Marquer comme bot, généralement 'on' ou vide\"],\"N7TQbE\":[\"Inviter un utilisateur dans \",[\"channelName\"]],\"NCca/o\":[\"Entrez le pseudo par défaut...\"],\"Nqs6B9\":[\"Affiche tous les médias externes. Toute URL peut déclencher une requête vers un serveur inconnu.\"],\"Nt+9O7\":[\"Utiliser WebSocket au lieu de TCP brut\"],\"NxIHzc\":[\"Expulser l'utilisateur\"],\"O+v/cL\":[\"Parcourir tous les canaux du serveur\"],\"ODwSCk\":[\"Envoyer un GIF\"],\"OGQ5kK\":[\"Configurer les sons de notification et les mises en évidence\"],\"OIPt1Z\":[\"Afficher ou masquer la barre latérale de la liste des membres\"],\"OKSNq/\":[\"Très strict\"],\"ONWvwQ\":[\"Téléverser\"],\"OVKoQO\":[\"Votre mot de passe de compte pour l'authentification\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Amener IRC vers le futur\"],\"OhCpra\":[\"Définir un sujet…\"],\"OkltoQ\":[\"Bannir \",[\"username\"],\" par pseudo (l'empêche de rejoindre avec le même pseudo)\"],\"P+t/Te\":[\"Aucune donnée supplémentaire\"],\"P42Wcc\":[\"Sécurisé\"],\"PD38l0\":[\"Aperçu de l'avatar du canal\"],\"PD9mEt\":[\"Saisir un message...\"],\"PPqfdA\":[\"Ouvrir les paramètres de configuration du canal\"],\"PSCjfZ\":[\"Le sujet affiché pour ce salon. Tous les utilisateurs peuvent le voir.\"],\"PZCecv\":[\"Aperçu PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 fois\"],\"other\":[[\"c\"],\" fois\"]}]],\"PguS2C\":[\"Ajouter un masque d'exception (ex. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Affichage de \",[\"displayedChannelsCount\"],\" sur \",[\"0\"],\" canaux\"],\"PqhVlJ\":[\"Bannir l'utilisateur (par hostmask)\"],\"Q+chwU\":[\"Nom d'utilisateur :\"],\"Q2QY4/\":[\"Supprimer cette invitation\"],\"Q6hhn8\":[\"Préférences\"],\"QF4a34\":[\"Veuillez saisir un nom d'utilisateur\"],\"QGqSZ2\":[\"Couleur et mise en forme\"],\"QJQd1J\":[\"Modifier le profil\"],\"QSzGDE\":[\"Inactif\"],\"QUlny5\":[\"Bienvenue sur \",[\"0\"],\" !\"],\"Qoq+GP\":[\"Lire la suite\"],\"QuSkCF\":[\"Filtrer les canaux...\"],\"QwUrDZ\":[\"a changé le sujet en : \",[\"topic\"]],\"R0UH07\":[\"Image \",[\"0\"],\" sur \",[\"1\"]],\"R7SsBE\":[\"Couper le son\"],\"R8rf1X\":[\"Cliquez pour définir le sujet\"],\"RArB3D\":[\"a été expulsé de \",[\"channelName\"],\" par \",[\"username\"]],\"RI3cWd\":[\"Découvrez le monde de l'IRC avec ObsidianIRC\"],\"RIfHS5\":[\"Créer un nouveau lien d'invitation\"],\"RMMaN5\":[\"Modéré (+m)\"],\"RWw9Lg\":[\"Fermer la fenêtre\"],\"RZ2BuZ\":[\"L'enregistrement du compte \",[\"account\"],\" nécessite une vérification : \",[\"message\"]],\"RySp6q\":[\"Masquer les commentaires\"],\"SPKQTd\":[\"Le pseudo est requis\"],\"SPVjfj\":[\"Par défaut « aucune raison » si laissé vide\"],\"SQKPvQ\":[\"Inviter un utilisateur\"],\"SkZcl+\":[\"Choisissez un profil de protection contre le flood prédéfini. Ces profils offrent des paramètres de protection équilibrés pour différents cas d'usage.\"],\"Slr+3C\":[\"Utilisateurs min.\"],\"Spnlre\":[\"Vous avez invité \",[\"target\"],\" à rejoindre \",[\"channel\"]],\"T/ckN5\":[\"Ouvrir dans le visualiseur\"],\"T91vKp\":[\"Lire\"],\"TV2Wdu\":[\"Découvrez comment nous gérons vos données et protégeons votre vie privée.\"],\"TgFpwD\":[\"Application en cours...\"],\"TkzSFB\":[\"Aucune modification\"],\"TtserG\":[\"Saisir le vrai nom\"],\"Ttz9J1\":[\"Entrez le mot de passe...\"],\"Tz0i8g\":[\"Paramètres\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Réagir\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Vous n'avez encore créé aucun lien d'invitation. Utilisez le formulaire ci-dessus pour créer le premier.\"],\"UGT5vp\":[\"Enregistrer les paramètres\"],\"UV5hLB\":[\"Aucun bannissement trouvé\"],\"Uaj3Nd\":[\"Messages de statut\"],\"Ue3uny\":[\"Par défaut (aucun profil)\"],\"UkARhe\":[\"Normal – Protection standard\"],\"Umn7Cj\":[\"Pas encore de commentaires. Soyez le premier !\"],\"UtUIRh\":[[\"0\"],\" anciens messages\"],\"UwzP+U\":[\"Connexion sécurisée\"],\"V0/A4O\":[\"Propriétaire du canal\"],\"V4qgxE\":[\"Créé avant (min)\"],\"V8yTm6\":[\"Effacer la recherche\"],\"VJMMyz\":[\"ObsidianIRC - L'IRC vers le futur\"],\"VJScHU\":[\"Raison\"],\"VLsmVV\":[\"Couper les notifications\"],\"VbyRUy\":[\"Commentaires\"],\"Vmx0mQ\":[\"Défini par :\"],\"VqnIZz\":[\"Consulter notre politique de confidentialité et nos pratiques en matière de données\"],\"VrMygG\":[\"La longueur minimale est \",[\"0\"]],\"VrnTui\":[\"Vos pronoms, affichés dans votre profil\"],\"W8E3qn\":[\"Compte authentifié\"],\"WAakm9\":[\"Supprimer le canal\"],\"WFxTHC\":[\"Ajouter un masque de bannissement (ex. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"L'hôte du serveur est requis\"],\"WRYdXW\":[\"Position audio\"],\"WUOH5B\":[\"Ignorer l'utilisateur\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Afficher 1 élément de plus\"],\"other\":[\"Afficher \",[\"1\"],\" éléments de plus\"]}]],\"WYxRzo\":[\"Créer et gérer vos liens d'invitation\"],\"Wd38W1\":[\"Laissez le canal vide pour une invitation générique au réseau. La description sert uniquement à vos notes — visible uniquement par vous dans cette liste.\"],\"Weq9zb\":[\"Général\"],\"Wfj7Sk\":[\"Activer ou désactiver les sons de notification\"],\"Wm7gbG\":[\"GitHub :\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Profil de l'utilisateur\"],\"X6S3lt\":[\"Rechercher des paramètres, canaux, serveurs...\"],\"XEHan5\":[\"Continuer quand même\"],\"XI1+wb\":[\"Format invalide\"],\"XIXeuC\":[\"Message à @\",[\"0\"]],\"XMS+k4\":[\"Démarrer un message privé\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Désépingler la conversation privée\"],\"Xm/s+u\":[\"Affichage\"],\"Xp2n93\":[\"Affiche les médias provenant de l'hébergeur de fichiers de confiance de votre serveur. Aucune requête n'est envoyée à des services externes.\"],\"XvjC4F\":[\"Enregistrement...\"],\"Y/qryO\":[\"Aucun utilisateur ne correspond à votre recherche\"],\"YAqRpI\":[\"Enregistrement du compte réussi pour \",[\"account\"],\" : \",[\"message\"]],\"YEfzvP\":[\"Sujet protégé (+t)\"],\"YQOn6a\":[\"Réduire la liste des membres\"],\"YRCoE9\":[\"Opérateur du canal\"],\"YURQaF\":[\"Voir le profil\"],\"YdBSvr\":[\"Contrôler l'affichage des médias et du contenu externe\"],\"Yj6U3V\":[\"Pas de serveur central :\"],\"YjvpGx\":[\"Pronoms\"],\"YqH4l4\":[\"Aucune clé\"],\"YyUPpV\":[\"Compte :\"],\"ZJSWfw\":[\"Message affiché lors de la déconnexion du serveur\"],\"ZR1dJ4\":[\"Invitations\"],\"ZdWg0V\":[\"Ouvrir dans le navigateur\"],\"ZhRBbl\":[\"Rechercher des messages…\"],\"Zmcu3y\":[\"Filtres avancés\"],\"a2/8e5\":[\"Sujet défini après (min)\"],\"aHKcKc\":[\"Page précédente\"],\"aJTbXX\":[\"Mot de passe Oper\"],\"aQryQv\":[\"Le modèle existe déjà\"],\"aW9pLN\":[\"Nombre maximum d'utilisateurs autorisés. Laissez vide pour aucune limite.\"],\"ah4fmZ\":[\"Affiche également des aperçus de YouTube, Vimeo, SoundCloud et autres services connus.\"],\"aifXak\":[\"Aucun média dans ce salon\"],\"ap2zBz\":[\"Détendu\"],\"az8lvo\":[\"Désactivé\"],\"azXSNo\":[\"Développer la liste des membres\"],\"azdliB\":[\"Se connecter à un compte\"],\"b26wlF\":[\"elle/la\"],\"bD/+Ei\":[\"Strict\"],\"bQ6BJn\":[\"Configurez des règles détaillées de protection contre le flood. Chaque règle précise le type d'activité à surveiller et l'action à prendre lorsque les seuils sont dépassés.\"],\"beV7+y\":[\"L'utilisateur recevra une invitation à rejoindre \",[\"channelName\"],\".\"],\"bk84cH\":[\"Message d'absence\"],\"bkHdLj\":[\"Ajouter un serveur IRC\"],\"bmQLn5\":[\"Ajouter une règle\"],\"bwRvnp\":[\"Action\"],\"c8+EVZ\":[\"Compte vérifié\"],\"cGYUlD\":[\"Aucun aperçu de média n'est chargé.\"],\"cLF98o\":[\"Afficher les commentaires (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Aucun utilisateur disponible\"],\"cSgpoS\":[\"Épingler la conversation privée\"],\"cde3ce\":[\"Message <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Copier la sortie formatée\"],\"cl/A5J\":[\"Bienvenue sur \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\" !\"],\"cnGeoo\":[\"Supprimer\"],\"coPLXT\":[\"Nous ne stockons pas vos communications IRC sur nos serveurs\"],\"crYH/6\":[\"Lecteur SoundCloud\"],\"d3sis4\":[\"Ajouter un serveur\"],\"d9aN5k\":[\"Retirer \",[\"username\"],\" du canal\"],\"dEgA5A\":[\"Annuler\"],\"dGi1We\":[\"Désépingler cette conversation privée\"],\"dJVuyC\":[\"a quitté \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"à\"],\"dXqxlh\":[\"<0>⚠️ Risque de sécurité !0> Cette connexion peut être vulnérable à l'interception ou aux attaques de type man-in-the-middle.\"],\"da9Q/R\":[\"Modes du canal modifiés\"],\"dhJN3N\":[\"Afficher les commentaires\"],\"dj2xTE\":[\"Ignorer la notification\"],\"dpCzmC\":[\"Paramètres de protection contre le flood\"],\"e9dQpT\":[\"Voulez-vous ouvrir ce lien dans un nouvel onglet ?\"],\"ePK91l\":[\"Modifier\"],\"eYBDuB\":[\"Téléverser une image ou fournir une URL avec substitution optionnelle \",[\"size\"]],\"edBbee\":[\"Bannir \",[\"username\"],\" par hostmask (l'empêche de rejoindre depuis la même adresse IP/hôte)\"],\"ekfzWq\":[\"Paramètres utilisateur\"],\"elPDWs\":[\"Personnalisez votre expérience du client IRC\"],\"eu2osY\":[\"<0>💡 Recommandation :0> Ne continuez que si vous faites confiance à ce serveur et que vous comprenez les risques. Évitez de partager des informations sensibles ou des mots de passe via cette connexion.\"],\"euEhbr\":[\"Cliquez pour rejoindre \",[\"channel\"]],\"ez3vLd\":[\"Activer la saisie multiligne\"],\"f0J5Ki\":[\"Les communications entre serveurs peuvent utiliser des connexions non chiffrées\"],\"f9BHJk\":[\"Avertir l'utilisateur\"],\"fDOLLd\":[\"Aucun canal trouvé.\"],\"ffzDkB\":[\"Analyses anonymes :\"],\"fq1GF9\":[\"Afficher quand des utilisateurs se déconnectent du serveur\"],\"gEF57C\":[\"Ce serveur ne prend en charge qu'un seul type de connexion\"],\"gJuLUI\":[\"Liste d'ignorés\"],\"gNzMrk\":[\"Avatar actuel\"],\"gjPWyO\":[\"Entrez votre pseudo...\"],\"gz6UQ3\":[\"Agrandir\"],\"h6razj\":[\"Exclure le masque de nom de salon\"],\"hG6jnw\":[\"Aucun sujet défini\"],\"hG89Ed\":[\"Image\"],\"hYgDIe\":[\"Créer\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"ex. : 100:1440\"],\"he3ygx\":[\"Copier\"],\"hehnjM\":[\"Quantité\"],\"hzdLuQ\":[\"Seuls les utilisateurs avec voice ou plus peuvent parler\"],\"i0qMbr\":[\"Accueil\"],\"iDNBZe\":[\"Notifications\"],\"iH8pgl\":[\"Retour\"],\"iL9SZg\":[\"Bannir l'utilisateur (par pseudo)\"],\"iNt+3c\":[\"Retour à l'image\"],\"iQvi+a\":[\"Ne plus m'avertir de la faible sécurité des liens pour ce serveur\"],\"iSLIjg\":[\"Connecter\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Hôte du serveur\"],\"idD8Ev\":[\"Enregistré\"],\"iivqkW\":[\"Connecté depuis\"],\"ij+Elv\":[\"Aperçu de l'image\"],\"ilIWp7\":[\"Activer/désactiver les notifications\"],\"iuaqvB\":[\"Utilisez * comme joker. Exemples : baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Bannir par masque d'hôte\"],\"jA4uoI\":[\"Sujet :\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Raison (facultatif)\"],\"jUV7CU\":[\"Téléverser un avatar\"],\"jW5Uwh\":[\"Contrôle la quantité de médias externes chargés. Désactivé / Sûr / Sources fiables / Tout le contenu.\"],\"jXzms5\":[\"Options de pièce jointe\"],\"jZlrte\":[\"Couleur\"],\"jfC/xh\":[\"Contact\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Charger les anciens messages\"],\"k3ID0F\":[\"Filtrer les membres…\"],\"k65gsE\":[\"Analyse approfondie\"],\"k7Zgob\":[\"Annuler la connexion\"],\"kAVx5h\":[\"Aucune invitation trouvée\"],\"kCLEPU\":[\"Connecté à\"],\"kF5LKb\":[\"Modèles ignorés :\"],\"kGeOx/\":[\"Rejoindre \",[\"0\"]],\"kITKr8\":[\"Chargement des modes du salon...\"],\"kPpPsw\":[\"Vous êtes un IRC Operator\"],\"kWJmRL\":[\"Vous\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Copier JSON\"],\"krViRy\":[\"Cliquer pour copier en JSON\"],\"ks71ra\":[\"Exceptions\"],\"kw4lRv\":[\"Semi-opérateur du canal\"],\"kxgIRq\":[\"Sélectionnez ou ajoutez un canal pour commencer.\"],\"ky6dWe\":[\"Aperçu de l'avatar\"],\"l+GxCv\":[\"Chargement des canaux...\"],\"l+IUVW\":[\"Vérification du compte réussie pour \",[\"account\"],\" : \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"s'est reconnecté\"],\"other\":[\"s'est reconnecté \",[\"reconnectCount\"],\" fois\"]}]],\"l1l8sj\":[\"il y a \",[\"0\"],\" j\"],\"l5NhnV\":[\"#canal (optionnel)\"],\"l5jmzx\":[[\"0\"],\" et \",[\"1\"],\" sont en train d'écrire...\"],\"lCF0wC\":[\"Actualiser\"],\"lHy8N5\":[\"Chargement de canaux supplémentaires...\"],\"lasgrr\":[\"utilisé\"],\"lbpf14\":[\"Rejoindre \",[\"value\"]],\"lfFsZ4\":[\"Canaux\"],\"lkNdiH\":[\"Nom de compte\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Téléverser une image\"],\"loQxaJ\":[\"Je suis de retour\"],\"lvfaxv\":[\"ACCUEIL\"],\"m16xKo\":[\"Ajouter\"],\"m8flAk\":[\"Aperçu (pas encore envoyé)\"],\"mEPxTp\":[\"<0>⚠️ Attention !0> N'ouvrez que des liens provenant de sources fiables. Des liens malveillants peuvent compromettre votre sécurité ou votre vie privée.\"],\"mHGdhG\":[\"Informations sur le serveur\"],\"mHS8lb\":[\"Message dans #\",[\"0\"]],\"mMYBD9\":[\"Large – Portée de protection étendue\"],\"mTGsPd\":[\"Sujet du salon\"],\"mU8j6O\":[\"Pas de messages externes (+n)\"],\"mZp8FL\":[\"Retour automatique à une seule ligne\"],\"mdQu8G\":[\"VotrePseudo\"],\"miSSBQ\":[\"Commentaires (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"L'utilisateur est authentifié\"],\"mwtcGl\":[\"Fermer les commentaires\"],\"mzI/c+\":[\"Télécharger\"],\"n3fGRk\":[\"défini par \",[\"0\"]],\"nE9jsU\":[\"Détendu – Protection moins agressive\"],\"nNflMD\":[\"Quitter le canal\"],\"nPXkBi\":[\"Chargement des données WHOIS...\"],\"nQnxxF\":[\"Message dans #\",[\"0\"],\" (Maj+Entrée pour nouvelle ligne)\"],\"nWMRxa\":[\"Désépingler\"],\"nkC032\":[\"Aucun profil anti-flood\"],\"o69z4d\":[\"Envoyer un message d'avertissement à \",[\"username\"]],\"o9ylQi\":[\"Recherchez des GIFs pour commencer\"],\"oFGkER\":[\"Avis du serveur\"],\"oOi11l\":[\"Défiler vers le bas\"],\"oPYIL5\":[\"réseau\"],\"oQEzQR\":[\"Nouveau message privé\"],\"oXOSPE\":[\"En ligne\"],\"oal760\":[\"Des attaques man-in-the-middle sur les liens serveur sont possibles\"],\"oeqmmJ\":[\"Sources de confiance\"],\"optX0N\":[\"il y a \",[\"0\"],\" h\"],\"ovBPCi\":[\"Par défaut\"],\"p0Z69r\":[\"Le modèle ne peut pas être vide\"],\"p1KgtK\":[\"Échec du chargement audio\"],\"p59pEv\":[\"Détails supplémentaires\"],\"p7sRI6\":[\"Informer les autres que vous écrivez\"],\"pBm1od\":[\"Canal secret\"],\"pNmiXx\":[\"Votre pseudo par défaut pour tous les serveurs\"],\"pUUo9G\":[\"Nom d'hôte :\"],\"pVGPmz\":[\"Mot de passe du compte\"],\"peNE68\":[\"Permanent\"],\"plhHQt\":[\"Aucune donnée\"],\"pm6+q5\":[\"Avertissement de sécurité\"],\"pn5qSs\":[\"Informations supplémentaires\"],\"q0cR4S\":[\"est maintenant connu sous le nom de **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Le salon n'apparaîtra pas dans les commandes LIST ou NAMES\"],\"qLpTm/\":[\"Supprimer la réaction \",[\"emoji\"]],\"qVkGWK\":[\"Épingler\"],\"qY8wNa\":[\"Page d'accueil\"],\"qb0xJ7\":[\"Jokers : * correspond à toute séquence, ? à un seul caractère. Exemples : nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Clé du salon (+k)\"],\"qtoOYG\":[\"Aucune limite\"],\"r1W2AS\":[\"Image hébergée\"],\"rIPR2O\":[\"Sujet défini avant (min)\"],\"rMMSYo\":[\"La longueur maximale est \",[\"0\"]],\"rWtzQe\":[\"Le réseau s'est divisé et reconnecté. ✅\"],\"rYG2u6\":[\"Veuillez patienter...\"],\"rdUucN\":[\"Aperçu\"],\"rjGI/Q\":[\"Confidentialité\"],\"rk8iDX\":[\"Chargement des GIFs...\"],\"rn6SBY\":[\"Rétablir le son\"],\"s/UKqq\":[\"A été expulsé du canal\"],\"s8cATI\":[\"a rejoint \",[\"channelName\"]],\"sCO9ue\":[\"La connexion à <0>\",[\"serverName\"],\"0> présente les problèmes de sécurité suivants :\"],\"sGH11W\":[\"Serveur\"],\"sHI1H+\":[\"est maintenant connu sous le nom de **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" vous a invité à rejoindre \",[\"channel\"]],\"sby+1/\":[\"Cliquer pour copier\"],\"sfN25C\":[\"Votre nom réel ou complet\"],\"sliuzR\":[\"Ouvrir le lien\"],\"sqrO9R\":[\"Mentions personnalisées\"],\"sr6RdJ\":[\"Multiligne avec Shift+Entrée\"],\"swrCpB\":[\"Le canal a été renommé de \",[\"oldName\"],\" en \",[\"newName\"],\" par \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Avancé\"],\"t/YqKh\":[\"Supprimer\"],\"t47eHD\":[\"Votre identifiant unique sur ce serveur\"],\"tAkAh0\":[\"URL avec substitution optionnelle \",[\"size\"],\". Exemple : https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Afficher ou masquer la barre latérale de la liste des canaux\"],\"tfDRzk\":[\"Enregistrer\"],\"tiBsJk\":[\"a quitté \",[\"channelName\"]],\"tt4/UD\":[\"a quitté (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Le pseudo {nick} est déjà utilisé, nouvel essai avec {newNick}\"],\"u0a8B4\":[\"S'authentifier en tant qu'opérateur IRC pour l'accès administratif\"],\"u0rWFU\":[\"Créé après (min)\"],\"u72w3t\":[\"Utilisateurs et modèles à ignorer\"],\"u7jc2L\":[\"a quitté\"],\"uAQUqI\":[\"Statut\"],\"uB85T3\":[\"Échec de l'enregistrement : \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"Serveurs IRC :\"],\"ukyW4o\":[\"Vos liens d'invitation\"],\"usSSr/\":[\"Niveau de zoom\"],\"v7uvcf\":[\"Logiciel :\"],\"vE8kb+\":[\"Shift+Entrée pour les nouvelles lignes (Entrée envoie)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Pas de sujet\"],\"vSJd18\":[\"Vidéo\"],\"vXIe7J\":[\"Langue\"],\"vaHYxN\":[\"Vrai nom\"],\"vhjbKr\":[\"Absent\"],\"w4NYox\":[\"client \",[\"title\"]],\"w8xQRx\":[\"Valeur invalide\"],\"wFjjxZ\":[\"a été expulsé de \",[\"channelName\"],\" par \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Aucune exception de bannissement trouvée\"],\"wPrGnM\":[\"Administrateur du canal\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Afficher quand des utilisateurs rejoignent ou quittent des salons\"],\"whqZ9r\":[\"Mots ou phrases supplémentaires à surligner\"],\"wm7RV4\":[\"Son de notification\"],\"wz/Yoq\":[\"Vos messages pourraient être interceptés lors du relais entre serveurs\"],\"x3+y8b\":[\"Nombre de personnes inscrites via ce lien\"],\"xCJdfg\":[\"Effacer\"],\"xOTzt5\":[\"à l'instant\"],\"xUHRTR\":[\"S'authentifier automatiquement comme opérateur à la connexion\"],\"xWHwwQ\":[\"Bannissements\"],\"xYilR2\":[\"Médias\"],\"xbi8D6\":[\"Ce serveur ne prend pas en charge les liens d'invitation (la capacité<0>obby.world/invitation0>n'est pas annoncée). Vous pouvez toujours discuter normalement ; ce panneau est destiné aux réseaux propulsés par obbyircd.\"],\"xceQrO\":[\"Seuls les websockets sécurisés sont pris en charge\"],\"xdtXa+\":[\"nom-du-salon\"],\"xfXC7q\":[\"Salons textuels\"],\"xlCYOE\":[\"Chargement des messages...\"],\"xlhswE\":[\"La valeur minimale est \",[\"0\"]],\"xq97Ci\":[\"Ajouter un mot ou une expression...\"],\"xuRqRq\":[\"Limite de clients (+l)\"],\"xwF+7J\":[[\"0\"],\" est en train d'écrire...\"],\"y1eoq1\":[\"Copier le lien\"],\"yNeucF\":[\"Ce serveur ne supporte pas les métadonnées de profil étendues (extension IRCv3 METADATA). Les champs comme l'avatar, le nom d'affichage et le statut ne sont pas disponibles.\"],\"yPlrca\":[\"Avatar du salon\"],\"yQE2r9\":[\"Chargement\"],\"ySU+JY\":[\"votre@email.com\"],\"yTX1Rt\":[\"Nom d'utilisateur opérateur\"],\"yYOzWD\":[\"journaux\"],\"yfx9Re\":[\"Mot de passe opérateur IRC\"],\"ygCKqB\":[\"Arrêter\"],\"ymDxJx\":[\"Nom d'utilisateur opérateur IRC\"],\"yrpRsQ\":[\"Trier par nom\"],\"yz7wBu\":[\"Fermer\"],\"zJw+jA\":[\"définit le mode : \",[\"0\"]],\"zbymaY\":[\"il y a \",[\"0\"],\" min\"],\"zebeLu\":[\"Saisir le nom d'utilisateur oper\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Format de modèle invalide. Utilisez le format nick!user@host (jokers * autorisés)\"],\"+6NQQA\":[\"Canal d'assistance générale\"],\"+6NyRG\":[\"Client\"],\"+K0AvT\":[\"Déconnecter\"],\"+cyFdH\":[\"Message par défaut pour le statut absent\"],\"+mVPqU\":[\"Afficher le formatage Markdown dans les messages\"],\"+vqCJH\":[\"Votre nom d'utilisateur de compte pour l'authentification\"],\"+yPBXI\":[\"Choisir un fichier\"],\"+zy2Nq\":[\"Type\"],\"/09cao\":[\"Faible sécurité du lien (niveau \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Les utilisateurs extérieurs ne peuvent pas envoyer de messages\"],\"/4C8U0\":[\"Tout copier\"],\"/6BzZF\":[\"Afficher/masquer la liste des membres\"],\"/AkXyp\":[\"Confirmer ?\"],\"/TNOPk\":[\"L'utilisateur est absent\"],\"/XQgft\":[\"Découvrir\"],\"/cF7Rs\":[\"Volume\"],\"/dqduX\":[\"Page suivante\"],\"/fc3q4\":[\"Tout le contenu\"],\"/kISDh\":[\"Activer les sons de notification\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Jouer des sons pour les mentions et messages\"],\"0/0ZGA\":[\"Masque du nom de salon\"],\"0D6j7U\":[\"En savoir plus sur les règles personnalisées →\"],\"0XsHcR\":[\"Expulser l'utilisateur\"],\"0ZpE//\":[\"Trier par utilisateurs\"],\"0bEPwz\":[\"Se mettre absent\"],\"0dGkPt\":[\"Développer la liste des canaux\"],\"0gS7M5\":[\"Nom d'affichage\"],\"0kS+M8\":[\"ExempleRÉSEAU\"],\"0rgoY7\":[\"Se connecter uniquement aux serveurs choisis\"],\"0wdd7X\":[\"Rejoindre\"],\"0wkVYx\":[\"Messages privés\"],\"111uHX\":[\"Aperçu du lien\"],\"196EG4\":[\"Supprimer la conversation privée\"],\"1DSr1i\":[\"Créer un compte\"],\"1O/24y\":[\"Afficher/masquer la liste des canaux\"],\"1TNIig\":[\"Ouvrir\"],\"1VPJJ2\":[\"Avertissement de lien externe\"],\"1ZC/dv\":[\"Aucune mention ou message non lu\"],\"1pO1zi\":[\"Le nom du serveur est requis\"],\"1uwfzQ\":[\"Voir le sujet du canal\"],\"268g7c\":[\"Saisir le nom d'affichage\"],\"2CEOW6\":[\"Réseau lié via le bouncer soju\"],\"2F9+AZ\":[\"Aucun trafic IRC brut capturé pour le moment. Essayez de vous connecter ou d'envoyer un message.\"],\"2FOFq1\":[\"Les opérateurs du réseau pourraient potentiellement lire vos messages\"],\"2FYpfJ\":[\"Plus\"],\"2HF1Y2\":[[\"inviter\"],\" a invité \",[\"target\"],\" à rejoindre \",[\"channel\"]],\"2I70QL\":[\"Voir les informations du profil utilisateur\"],\"2QYdmE\":[\"Utilisateurs :\"],\"2QpEjG\":[\"a quitté\"],\"2YE223\":[\"Message dans #\",[\"0\"],\" (Entrée pour nouvelle ligne, Maj+Entrée pour envoyer)\"],\"2bimFY\":[\"Utiliser le mot de passe du serveur\"],\"2iTmdZ\":[\"Stockage local :\"],\"2odkwe\":[\"Strict – Protection plus agressive\"],\"2uDhbA\":[\"Saisir le nom d'utilisateur à inviter\"],\"2ygf/L\":[\"← Retour\"],\"2zEgxj\":[\"Rechercher des GIFs...\"],\"3RdPhl\":[\"Renommer le canal\"],\"3THokf\":[\"Utilisateur avec droit de parole\"],\"3TSz9S\":[\"Réduire\"],\"3jBDvM\":[\"Nom d'affichage du salon\"],\"3ryuFU\":[\"Rapports de plantage optionnels pour améliorer l'application\"],\"3uBF/8\":[\"Fermer le visualiseur\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Entrez le nom du compte...\"],\"4/Rr0R\":[\"Inviter un utilisateur dans le canal actuel\"],\"4EZrJN\":[\"Règles\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Profil de flood (+F)\"],\"4RZQRK\":[\"Qu'est-ce que vous faites ?\"],\"4hfTrB\":[\"Pseudo\"],\"4n99LO\":[\"Déjà dans \",[\"0\"]],\"4t6vMV\":[\"Passer automatiquement en mode ligne unique pour les messages courts\"],\"4vsHmf\":[\"Temps (min)\"],\"4x/Axu\":[\"Votre bouncer n'a aucun réseau pour le moment. Ajoutez-en un pour commencer.\"],\"5+INAX\":[\"Surligner les messages qui vous mentionnent\"],\"5R5Pv/\":[\"Nom Oper\"],\"678PKt\":[\"Nom du réseau\"],\"6Aih4U\":[\"Hors ligne\"],\"6CO3WE\":[\"Mot de passe requis pour rejoindre le salon. Laissez vide pour supprimer la clé.\"],\"6HhMs3\":[\"Message de déconnexion\"],\"6V3Ea3\":[\"Copié\"],\"6lGV3K\":[\"Afficher moins\"],\"6yFOEi\":[\"Entrez le mot de passe oper...\"],\"7+IHTZ\":[\"Aucun fichier choisi\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (ex. : spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Envoyer un message privé\"],\"7U1W7c\":[\"Très détendu\"],\"7Y1YQj\":[\"Nom réel :\"],\"7YHArF\":[\"— ouvrir dans le visualiseur\"],\"7fjnVl\":[\"Rechercher des utilisateurs...\"],\"7jL88x\":[\"Supprimer ce message ? Cette action est irréversible.\"],\"7nGhhM\":[\"À quoi pensez-vous ?\"],\"7sEpu1\":[\"Membres — \",[\"0\"]],\"7sNhEz\":[\"Nom d'utilisateur\"],\"8H0Q+x\":[\"En savoir plus sur les profils →\"],\"8Phu0A\":[\"Afficher quand des utilisateurs changent de pseudo\"],\"8XTG9e\":[\"Saisir le mot de passe oper\"],\"8XsV2J\":[\"Réessayer l'envoi\"],\"8ZsakT\":[\"Mot de passe\"],\"8kR84m\":[\"Vous êtes sur le point d'ouvrir un lien externe :\"],\"8lCgih\":[\"Supprimer la règle\"],\"8o3dPc\":[\"Déposez les fichiers pour les téléverser\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"a rejoint\"],\"other\":[\"a rejoint \",[\"joinCount\"],\" fois\"]}]],\"9BMLnJ\":[\"Se reconnecter au serveur\"],\"9OEgyT\":[\"Ajouter une réaction\"],\"9PQ8m2\":[\"G-Line (bannissement global)\"],\"9Qs99X\":[\"E-mail :\"],\"9QupBP\":[\"Supprimer le motif\"],\"9W7tl5\":[\"(inchangé)\"],\"9bG48P\":[\"Envoi en cours\"],\"9f5f0u\":[\"Questions sur la confidentialité ? Contactez-nous :\"],\"9iweoP\":[\"Réseaux sur \",[\"0\"]],\"9unqs3\":[\"Absent :\"],\"9v3hwv\":[\"Aucun serveur trouvé.\"],\"9zb2WA\":[\"Connexion en cours\"],\"A1taO8\":[\"Rechercher\"],\"A2adVi\":[\"Envoyer des notifications de frappe\"],\"A9Rhec\":[\"Nom du salon\"],\"AWOSPo\":[\"Zoomer\"],\"AXSpEQ\":[\"Oper à la connexion\"],\"AdKRCX\":[\"Vous êtes connecté à <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Compte\"],\"AhNP40\":[\"Avancer\"],\"Ai2U7L\":[\"Hôte\"],\"AjBQnf\":[\"Pseudo modifié\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Annuler la réponse\"],\"ApSx0O\":[[\"0\"],\" messages trouvés correspondant à \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Aucun résultat trouvé\"],\"AyNqAB\":[\"Afficher tous les événements serveur dans le chat\"],\"B/QqGw\":[\"Absent du clavier\"],\"B0sB2k\":[\"Texte en clair\"],\"B8AaMI\":[\"Ce champ est obligatoire\"],\"BA2c49\":[\"Le serveur ne supporte pas le filtrage LIST avancé\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" et \",[\"3\"],\" autres sont en train d'écrire...\"],\"BGul2A\":[\"Vous avez des modifications non enregistrées. Voulez-vous vraiment fermer sans enregistrer ?\"],\"BIf9fi\":[\"Votre message de statut\"],\"BOJWfb\":[\"Déconnecter du bouncer soju ?\"],\"BPm98R\":[\"Aucun serveur sélectionné. Choisissez d'abord un serveur dans la barre latérale ; les liens d'invitation sont gérés par serveur.\"],\"BZz3md\":[\"Votre site web personnel\"],\"Bgm/H7\":[\"Permettre la saisie sur plusieurs lignes\"],\"BiQIl1\":[\"Épingler cette conversation privée\"],\"BlNZZ2\":[\"Cliquez pour aller au message\"],\"Bowq3c\":[\"Seuls les opérateurs peuvent modifier le sujet\"],\"Btozzp\":[\"Cette image a expiré\"],\"Bycfjm\":[\"Total : \",[\"0\"]],\"C6IBQc\":[\"Copier le JSON complet\"],\"C9L9wL\":[\"Collecte de données\"],\"CDq4wC\":[\"Modérer l'utilisateur\"],\"CHVRxG\":[\"Message à @\",[\"0\"],\" (Maj+Entrée pour nouvelle ligne)\"],\"CN9zdR\":[\"Le nom oper et le mot de passe sont requis\"],\"CW3sYa\":[\"Ajouter la réaction \",[\"emoji\"]],\"CaAkqd\":[\"Afficher les déconnexions\"],\"CbvaYj\":[\"Bannir par pseudo\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Sélectionner un canal\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"Système\"],\"D28t6+\":[\"a rejoint et quitté\"],\"DB8zMK\":[\"Appliquer\"],\"DBcWHr\":[\"Fichier son de notification personnalisé\"],\"DTy9Xw\":[\"Aperçus des médias\"],\"Dj4pSr\":[\"Choisissez un mot de passe sécurisé\"],\"Du+zn+\":[\"Recherche...\"],\"Du2T2f\":[\"Paramètre introuvable\"],\"DwsSVQ\":[\"Appliquer les filtres & Actualiser\"],\"E3W/zd\":[\"Pseudo par défaut\"],\"E6nRW7\":[\"Copier l'URL\"],\"E703RG\":[\"Modes :\"],\"EAeu1Z\":[\"Envoyer l'invitation\"],\"EFKJQT\":[\"Paramètre\"],\"EGPQBv\":[\"Règles de flood personnalisées (+f)\"],\"ELik0r\":[\"Voir la politique de confidentialité complète\"],\"EPbeC2\":[\"Voir ou modifier le sujet du canal\"],\"EQCDNT\":[\"Entrez le nom d'utilisateur oper...\"],\"EUvulZ\":[\"1 message trouvé correspondant à \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Image suivante\"],\"EdQY6l\":[\"Aucun\"],\"EnqLYU\":[\"Rechercher des serveurs...\"],\"F0OKMc\":[\"Modifier le serveur\"],\"F6Int2\":[\"Activer les surlignages\"],\"FDoLyE\":[\"Utilisateurs max.\"],\"FUU/hZ\":[\"Contrôle la quantité de médias externes chargés dans le chat.\"],\"Fdp03t\":[\"activé\"],\"FfPWR0\":[\"Modal\"],\"FjkaiT\":[\"Dézoomer\"],\"FlqOE9\":[\"Ce que cela signifie :\"],\"FolHNl\":[\"Gérez votre compte et l'authentification\"],\"Fp2Dif\":[\"A quitté le serveur\"],\"G5KmCc\":[\"GZ-Line (Z-Line globale)\"],\"GDs0lz\":[\"<0>Risque :0> Des informations sensibles (messages, conversations privées, identifiants de connexion) pourraient être exposées aux administrateurs réseau ou à des attaquants positionnés entre les serveurs IRC.\"],\"GR+2I3\":[\"Ajouter un masque d'invitation (ex. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Fermer les notifications serveur détachées\"],\"GdhD7H\":[\"Cliquez Ã\xA0 nouveau pour confirmer\"],\"GjRZex\":[\"Déconnecter le réseau ?\"],\"GlHnXw\":[\"Échec du changement de pseudo: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Aperçu :\"],\"GtmO8/\":[\"de\"],\"GtuHUQ\":[\"Renommer ce salon sur le serveur. Tous les utilisateurs verront le nouveau nom.\"],\"GuGfFX\":[\"Activer/désactiver la recherche\"],\"GxkJXS\":[\"Téléversement...\"],\"GzbwnK\":[\"A rejoint le canal\"],\"GzsUDB\":[\"Profil étendu\"],\"H/PnT8\":[\"Insérer un emoji\"],\"H6Izzl\":[\"Votre code couleur préféré\"],\"H9jIv+\":[\"Afficher les entrées/sorties\"],\"HAKBY9\":[\"Télécharger des fichiers\"],\"HdE1If\":[\"Canal\"],\"Hk4AW9\":[\"Votre nom d'affichage préféré\"],\"HmHDk7\":[\"Sélectionner un membre\"],\"HrQzPU\":[\"Canaux sur \",[\"networkName\"]],\"I2tXQ5\":[\"Message à @\",[\"0\"],\" (Entrée pour nouvelle ligne, Maj+Entrée pour envoyer)\"],\"I6bw/h\":[\"Bannir l'utilisateur\"],\"I92Z+b\":[\"Activer les notifications\"],\"I9D72S\":[\"Êtes-vous sûr de vouloir supprimer ce message ? Cette action est irréversible.\"],\"IA+1wo\":[\"Afficher quand des utilisateurs sont expulsés des salons\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info :\"],\"IUwGEM\":[\"Enregistrer les modifications\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" et \",[\"2\"],\" sont en train d'écrire...\"],\"IgrLD/\":[\"Pause\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Répondre\"],\"IoHMnl\":[\"La valeur maximale est \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Connexion en cours...\"],\"J5T9NW\":[\"Informations utilisateur\"],\"J8Y5+z\":[\"Oups ! La réseau s'est divisé ! ⚠️\"],\"JBHkBA\":[\"A quitté le canal\"],\"JCwL0Q\":[\"Saisir une raison (facultatif)\"],\"JFciKP\":[\"Basculer\"],\"JXGkhG\":[\"Changer le nom du canal (opérateurs uniquement)\"],\"JcD7qf\":[\"Plus d'actions\"],\"JdkA+c\":[\"Secret (+s)\"],\"Jmu12l\":[\"Canaux du serveur\"],\"JoQY+E\":[\"Cela supprime le réseau de votre bouncer soju. Pour le réutiliser, vous devrez l'ajouter à nouveau.\"],\"JvQ++s\":[\"Activer le Markdown\"],\"K2jwh/\":[\"Aucune donnée WHOIS disponible\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Supprimer le message\"],\"KKBlUU\":[\"Intégrer\"],\"KM0pLb\":[\"Bienvenue dans le canal !\"],\"KR6W2h\":[\"Ne plus ignorer l'utilisateur\"],\"KV+Bi1\":[\"Sur invitation uniquement (+i)\"],\"KdCtwE\":[\"Nombre de secondes de surveillance de l'activité de flood avant la réinitialisation des compteurs\"],\"Kkezga\":[\"Mot de passe du serveur\"],\"KsiQ/8\":[\"Les utilisateurs doivent être invités pour rejoindre le salon\"],\"L+gB/D\":[\"Informations sur le salon\"],\"LC1a7n\":[\"Le serveur IRC a signalé que ses liens entre serveurs ont un faible niveau de sécurité. Cela signifie que lorsque vos messages sont relayés entre les serveurs IRC du réseau, ils peuvent ne pas être correctement chiffrés ou les certificats SSL/TLS peuvent ne pas être validés correctement.\"],\"LEwpeL\":[\"bouncer soju (contrôle)\"],\"LNfLR5\":[\"Afficher les expulsions\"],\"LP+1Z7\":[\"Ajouter un réseau\"],\"LQb0W/\":[\"Afficher tous les événements\"],\"LU7/yA\":[\"Nom alternatif pour l'affichage. Peut contenir des espaces, emojis et caractères spéciaux. Le vrai nom (\",[\"channelName\"],\") sera toujours utilisé pour les commandes IRC.\"],\"LUb9O7\":[\"Un port de serveur valide est requis\"],\"LV4fT6\":[\"Description (optionnelle, ex. « Bêta-testeurs T3 »)\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Politique de confidentialité\"],\"LcuSDR\":[\"Gérez les informations de votre profil et vos métadonnées\"],\"LqLS9B\":[\"Afficher les changements de pseudo\"],\"LsDQt2\":[\"Paramètres du canal\"],\"LtI9AS\":[\"Propriétaire\"],\"LuNhhL\":[\"a réagi à ce message\"],\"M/AZNG\":[\"URL de votre image d'avatar\"],\"M/WIer\":[\"Envoyer un message\"],\"M8er/5\":[\"Nom :\"],\"MHk+7g\":[\"Image précédente\"],\"MRorGe\":[\"MP à l'utilisateur\"],\"MVbSGP\":[\"Fenêtre temporelle (secondes)\"],\"MkpcsT\":[\"Vos messages et paramètres sont stockés localement sur votre appareil\"],\"MzPdC2\":[\"Mot de passe du serveur (PASS)\"],\"N/hDSy\":[\"Marquer comme bot, généralement 'on' ou vide\"],\"N6j2JH\":[\"Modifier \",[\"0\"]],\"N7TQbE\":[\"Inviter un utilisateur dans \",[\"channelName\"]],\"NCca/o\":[\"Entrez le pseudo par défaut...\"],\"Nqs6B9\":[\"Affiche tous les médias externes. Toute URL peut déclencher une requête vers un serveur inconnu.\"],\"Nt+9O7\":[\"Utiliser WebSocket au lieu de TCP brut\"],\"NxIHzc\":[\"Expulser l'utilisateur\"],\"O+v/cL\":[\"Parcourir tous les canaux du serveur\"],\"OCGpR4\":[\"(hériter)\"],\"ODwSCk\":[\"Envoyer un GIF\"],\"OGQ5kK\":[\"Configurer les sons de notification et les mises en évidence\"],\"OIPt1Z\":[\"Afficher ou masquer la barre latérale de la liste des membres\"],\"OKSNq/\":[\"Très strict\"],\"ONWvwQ\":[\"Téléverser\"],\"OVKoQO\":[\"Votre mot de passe de compte pour l'authentification\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Amener IRC vers le futur\"],\"OhCpra\":[\"Définir un sujet…\"],\"OkltoQ\":[\"Bannir \",[\"username\"],\" par pseudo (l'empêche de rejoindre avec le même pseudo)\"],\"P+t/Te\":[\"Aucune donnée supplémentaire\"],\"P42Wcc\":[\"Sécurisé\"],\"PD38l0\":[\"Aperçu de l'avatar du canal\"],\"PD9mEt\":[\"Saisir un message...\"],\"PPqfdA\":[\"Ouvrir les paramètres de configuration du canal\"],\"PSCjfZ\":[\"Le sujet affiché pour ce salon. Tous les utilisateurs peuvent le voir.\"],\"PZCecv\":[\"Aperçu PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 fois\"],\"other\":[[\"c\"],\" fois\"]}]],\"PguS2C\":[\"Ajouter un masque d'exception (ex. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Affichage de \",[\"displayedChannelsCount\"],\" sur \",[\"0\"],\" canaux\"],\"PqhVlJ\":[\"Bannir l'utilisateur (par hostmask)\"],\"Q+chwU\":[\"Nom d'utilisateur :\"],\"Q2QY4/\":[\"Supprimer cette invitation\"],\"Q3v9Wc\":[\"Oui, supprimer\"],\"Q6hhn8\":[\"Préférences\"],\"QF4a34\":[\"Veuillez saisir un nom d'utilisateur\"],\"QGqSZ2\":[\"Couleur et mise en forme\"],\"QJQd1J\":[\"Modifier le profil\"],\"QSzGDE\":[\"Inactif\"],\"QUlny5\":[\"Bienvenue sur \",[\"0\"],\" !\"],\"Qoq+GP\":[\"Lire la suite\"],\"QuSkCF\":[\"Filtrer les canaux...\"],\"QwUrDZ\":[\"a changé le sujet en : \",[\"topic\"]],\"R0UH07\":[\"Image \",[\"0\"],\" sur \",[\"1\"]],\"R7SsBE\":[\"Couper le son\"],\"R8rf1X\":[\"Cliquez pour définir le sujet\"],\"RArB3D\":[\"a été expulsé de \",[\"channelName\"],\" par \",[\"username\"]],\"RI3cWd\":[\"Découvrez le monde de l'IRC avec ObsidianIRC\"],\"RIfHS5\":[\"Créer un nouveau lien d'invitation\"],\"RMMaN5\":[\"Modéré (+m)\"],\"RWw9Lg\":[\"Fermer la fenêtre\"],\"RZ2BuZ\":[\"L'enregistrement du compte \",[\"account\"],\" nécessite une vérification : \",[\"message\"]],\"RySp6q\":[\"Masquer les commentaires\"],\"S5Togi\":[\"Chargement des réseaux de votre bouncer…\"],\"SPKQTd\":[\"Le pseudo est requis\"],\"SPVjfj\":[\"Par défaut « aucune raison » si laissé vide\"],\"SQKPvQ\":[\"Inviter un utilisateur\"],\"STmlpb\":[\"Retour à la liste des réseaux\"],\"SkZcl+\":[\"Choisissez un profil de protection contre le flood prédéfini. Ces profils offrent des paramètres de protection équilibrés pour différents cas d'usage.\"],\"Slr+3C\":[\"Utilisateurs min.\"],\"Spnlre\":[\"Vous avez invité \",[\"target\"],\" à rejoindre \",[\"channel\"]],\"T/ckN5\":[\"Ouvrir dans le visualiseur\"],\"T91vKp\":[\"Lire\"],\"TV2Wdu\":[\"Découvrez comment nous gérons vos données et protégeons votre vie privée.\"],\"TgFpwD\":[\"Application en cours...\"],\"TkzSFB\":[\"Aucune modification\"],\"TtserG\":[\"Saisir le vrai nom\"],\"Ttz9J1\":[\"Entrez le mot de passe...\"],\"Tz0i8g\":[\"Paramètres\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Réagir\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Vous n'avez encore créé aucun lien d'invitation. Utilisez le formulaire ci-dessus pour créer le premier.\"],\"UGT5vp\":[\"Enregistrer les paramètres\"],\"UV5hLB\":[\"Aucun bannissement trouvé\"],\"Uaj3Nd\":[\"Messages de statut\"],\"Ue3uny\":[\"Par défaut (aucun profil)\"],\"UkARhe\":[\"Normal – Protection standard\"],\"Umn7Cj\":[\"Pas encore de commentaires. Soyez le premier !\"],\"UtUIRh\":[[\"0\"],\" anciens messages\"],\"UwzP+U\":[\"Connexion sécurisée\"],\"V0/A4O\":[\"Propriétaire du canal\"],\"V0zZWc\":[\"Cela fermera également le réseau lié ci-dessous.\"],\"V4qgxE\":[\"Créé avant (min)\"],\"V8yTm6\":[\"Effacer la recherche\"],\"VJMMyz\":[\"ObsidianIRC - L'IRC vers le futur\"],\"VJScHU\":[\"Raison\"],\"VLsmVV\":[\"Couper les notifications\"],\"VbyRUy\":[\"Commentaires\"],\"Vmx0mQ\":[\"Défini par :\"],\"VqnIZz\":[\"Consulter notre politique de confidentialité et nos pratiques en matière de données\"],\"VrMygG\":[\"La longueur minimale est \",[\"0\"]],\"VrnTui\":[\"Vos pronoms, affichés dans votre profil\"],\"W8E3qn\":[\"Compte authentifié\"],\"WAakm9\":[\"Supprimer le canal\"],\"WFxTHC\":[\"Ajouter un masque de bannissement (ex. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"L'hôte du serveur est requis\"],\"WRYdXW\":[\"Position audio\"],\"WUOH5B\":[\"Ignorer l'utilisateur\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Afficher 1 élément de plus\"],\"other\":[\"Afficher \",[\"1\"],\" éléments de plus\"]}]],\"WYxRzo\":[\"Créer et gérer vos liens d'invitation\"],\"Wd38W1\":[\"Laissez le canal vide pour une invitation générique au réseau. La description sert uniquement Ã\xA0 vos notes â visible uniquement par vous dans cette liste.\"],\"Weq9zb\":[\"Général\"],\"Wfj7Sk\":[\"Activer ou désactiver les sons de notification\"],\"Wm7gbG\":[\"GitHub :\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Profil de l'utilisateur\"],\"X6S3lt\":[\"Rechercher des paramètres, canaux, serveurs...\"],\"XEHan5\":[\"Continuer quand même\"],\"XI1+wb\":[\"Format invalide\"],\"XIXeuC\":[\"Message à @\",[\"0\"]],\"XMS+k4\":[\"Démarrer un message privé\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Désépingler la conversation privée\"],\"Xm/s+u\":[\"Affichage\"],\"Xp2n93\":[\"Affiche les médias provenant de l'hébergeur de fichiers de confiance de votre serveur. Aucune requête n'est envoyée à des services externes.\"],\"XvjC4F\":[\"Enregistrement...\"],\"Y/qryO\":[\"Aucun utilisateur ne correspond à votre recherche\"],\"YAqRpI\":[\"Enregistrement du compte réussi pour \",[\"account\"],\" : \",[\"message\"]],\"YEfzvP\":[\"Sujet protégé (+t)\"],\"YQOn6a\":[\"Réduire la liste des membres\"],\"YRCoE9\":[\"Opérateur du canal\"],\"YURQaF\":[\"Voir le profil\"],\"YdBSvr\":[\"Contrôler l'affichage des médias et du contenu externe\"],\"Yj6U3V\":[\"Pas de serveur central :\"],\"YjvpGx\":[\"Pronoms\"],\"YqH4l4\":[\"Aucune clé\"],\"YyUPpV\":[\"Compte :\"],\"ZJSWfw\":[\"Message affiché lors de la déconnexion du serveur\"],\"ZR1dJ4\":[\"Invitations\"],\"ZdWg0V\":[\"Ouvrir dans le navigateur\"],\"ZhRBbl\":[\"Rechercher des messages…\"],\"Zmcu3y\":[\"Filtres avancés\"],\"a0bHay\":[\"Déconnecter <0>\",[\"0\"],\"0> ?\"],\"a2/8e5\":[\"Sujet défini après (min)\"],\"aHKcKc\":[\"Page précédente\"],\"aJTbXX\":[\"Mot de passe Oper\"],\"aQryQv\":[\"Le modèle existe déjà\"],\"aW9pLN\":[\"Nombre maximum d'utilisateurs autorisés. Laissez vide pour aucune limite.\"],\"ah4fmZ\":[\"Affiche également des aperçus de YouTube, Vimeo, SoundCloud et autres services connus.\"],\"aifXak\":[\"Aucun média dans ce salon\"],\"ap2zBz\":[\"Détendu\"],\"az8lvo\":[\"Désactivé\"],\"azXSNo\":[\"Développer la liste des membres\"],\"azdliB\":[\"Se connecter à un compte\"],\"b26wlF\":[\"elle/la\"],\"bD/+Ei\":[\"Strict\"],\"bQ6BJn\":[\"Configurez des règles détaillées de protection contre le flood. Chaque règle précise le type d'activité à surveiller et l'action à prendre lorsque les seuils sont dépassés.\"],\"beV7+y\":[\"L'utilisateur recevra une invitation à rejoindre \",[\"channelName\"],\".\"],\"bk84cH\":[\"Message d'absence\"],\"bkHdLj\":[\"Ajouter un serveur IRC\"],\"bmQLn5\":[\"Ajouter une règle\"],\"bv4cFj\":[\"Transport\"],\"bwRvnp\":[\"Action\"],\"c8+EVZ\":[\"Compte vérifié\"],\"cGYUlD\":[\"Aucun aperçu de média n'est chargé.\"],\"cLF98o\":[\"Afficher les commentaires (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Aucun utilisateur disponible\"],\"cSgpoS\":[\"Épingler la conversation privée\"],\"cXeEKu\":[\"Cela fermera également les \",[\"0\"],\" réseaux liés ci-dessous.\"],\"cde3ce\":[\"Message <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Copier la sortie formatée\"],\"cl/A5J\":[\"Bienvenue sur \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\" !\"],\"cnGeoo\":[\"Supprimer\"],\"coPLXT\":[\"Nous ne stockons pas vos communications IRC sur nos serveurs\"],\"crYH/6\":[\"Lecteur SoundCloud\"],\"cv5DQb\":[\"aucun hôte défini\"],\"d3sis4\":[\"Ajouter un serveur\"],\"d9aN5k\":[\"Retirer \",[\"username\"],\" du canal\"],\"dEgA5A\":[\"Annuler\"],\"dGi1We\":[\"Désépingler cette conversation privée\"],\"dJVuyC\":[\"a quitté \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"à\"],\"dXqxlh\":[\"<0>⚠️ Risque de sécurité !0> Cette connexion peut être vulnérable à l'interception ou aux attaques de type man-in-the-middle.\"],\"da9Q/R\":[\"Modes du canal modifiés\"],\"dhJN3N\":[\"Afficher les commentaires\"],\"dj2xTE\":[\"Ignorer la notification\"],\"dpCzmC\":[\"Paramètres de protection contre le flood\"],\"e9dQpT\":[\"Voulez-vous ouvrir ce lien dans un nouvel onglet ?\"],\"ePK91l\":[\"Modifier\"],\"eYBDuB\":[\"Téléverser une image ou fournir une URL avec substitution optionnelle \",[\"size\"]],\"edBbee\":[\"Bannir \",[\"username\"],\" par hostmask (l'empêche de rejoindre depuis la même adresse IP/hôte)\"],\"ekfzWq\":[\"Paramètres utilisateur\"],\"elPDWs\":[\"Personnalisez votre expérience du client IRC\"],\"eu2osY\":[\"<0>💡 Recommandation :0> Ne continuez que si vous faites confiance à ce serveur et que vous comprenez les risques. Évitez de partager des informations sensibles ou des mots de passe via cette connexion.\"],\"euEhbr\":[\"Cliquez pour rejoindre \",[\"channel\"]],\"ez3vLd\":[\"Activer la saisie multiligne\"],\"f0J5Ki\":[\"Les communications entre serveurs peuvent utiliser des connexions non chiffrées\"],\"f9BHJk\":[\"Avertir l'utilisateur\"],\"fDOLLd\":[\"Aucun canal trouvé.\"],\"ffzDkB\":[\"Analyses anonymes :\"],\"fq1GF9\":[\"Afficher quand des utilisateurs se déconnectent du serveur\"],\"gCldcN\":[\"Changer la couleur d'accent\"],\"gEF57C\":[\"Ce serveur ne prend en charge qu'un seul type de connexion\"],\"gJuLUI\":[\"Liste d'ignorés\"],\"gNzMrk\":[\"Avatar actuel\"],\"gjPWyO\":[\"Entrez votre pseudo...\"],\"gz6UQ3\":[\"Agrandir\"],\"h6/IMX\":[\"Ajoutez votre premier réseau\"],\"h6razj\":[\"Exclure le masque de nom de salon\"],\"hG6jnw\":[\"Aucun sujet défini\"],\"hG89Ed\":[\"Image\"],\"hYgDIe\":[\"Créer\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"ex. : 100:1440\"],\"he3ygx\":[\"Copier\"],\"hehnjM\":[\"Quantité\"],\"hzdLuQ\":[\"Seuls les utilisateurs avec voice ou plus peuvent parler\"],\"i0qMbr\":[\"Accueil\"],\"iDNBZe\":[\"Notifications\"],\"iH8pgl\":[\"Retour\"],\"iL9SZg\":[\"Bannir l'utilisateur (par pseudo)\"],\"iNt+3c\":[\"Retour à l'image\"],\"iQvi+a\":[\"Ne plus m'avertir de la faible sécurité des liens pour ce serveur\"],\"iSLIjg\":[\"Connecter\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Hôte du serveur\"],\"idD8Ev\":[\"Enregistré\"],\"iivqkW\":[\"Connecté depuis\"],\"ij+Elv\":[\"Aperçu de l'image\"],\"ilIWp7\":[\"Activer/désactiver les notifications\"],\"iuaqvB\":[\"Utilisez * comme joker. Exemples : baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Bannir par masque d'hôte\"],\"jA4uoI\":[\"Sujet :\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Raison (facultatif)\"],\"jUV7CU\":[\"Téléverser un avatar\"],\"jW5Uwh\":[\"Contrôle la quantité de médias externes chargés. Désactivé / Sûr / Sources fiables / Tout le contenu.\"],\"jXzms5\":[\"Options de pièce jointe\"],\"jZlrte\":[\"Couleur\"],\"jfC/xh\":[\"Contact\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Charger les anciens messages\"],\"k3ID0F\":[\"Filtrer les membres…\"],\"k65gsE\":[\"Analyse approfondie\"],\"k7Zgob\":[\"Annuler la connexion\"],\"kAVx5h\":[\"Aucune invitation trouvée\"],\"kCLEPU\":[\"Connecté à\"],\"kF5LKb\":[\"Modèles ignorés :\"],\"kGeOx/\":[\"Rejoindre \",[\"0\"]],\"kITKr8\":[\"Chargement des modes du salon...\"],\"kPpPsw\":[\"Vous êtes un IRC Operator\"],\"kWJmRL\":[\"Vous\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Copier JSON\"],\"krViRy\":[\"Cliquer pour copier en JSON\"],\"ks71ra\":[\"Exceptions\"],\"kw4lRv\":[\"Semi-opérateur du canal\"],\"kxgIRq\":[\"Sélectionnez ou ajoutez un canal pour commencer.\"],\"ky6dWe\":[\"Aperçu de l'avatar\"],\"l+GxCv\":[\"Chargement des canaux...\"],\"l+IUVW\":[\"Vérification du compte réussie pour \",[\"account\"],\" : \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"s'est reconnecté\"],\"other\":[\"s'est reconnecté \",[\"reconnectCount\"],\" fois\"]}]],\"l1l8sj\":[\"il y a \",[\"0\"],\" j\"],\"l5NhnV\":[\"#canal (optionnel)\"],\"l5jmzx\":[[\"0\"],\" et \",[\"1\"],\" sont en train d'écrire...\"],\"lCF0wC\":[\"Actualiser\"],\"lHy8N5\":[\"Chargement de canaux supplémentaires...\"],\"lasgrr\":[\"utilisé\"],\"lbpf14\":[\"Rejoindre \",[\"value\"]],\"lfFsZ4\":[\"Canaux\"],\"lkNdiH\":[\"Nom de compte\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Téléverser une image\"],\"loQxaJ\":[\"Je suis de retour\"],\"lvfaxv\":[\"ACCUEIL\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Ajouter\"],\"m8flAk\":[\"Aperçu (pas encore envoyé)\"],\"mEPxTp\":[\"<0>⚠️ Attention !0> N'ouvrez que des liens provenant de sources fiables. Des liens malveillants peuvent compromettre votre sécurité ou votre vie privée.\"],\"mHGdhG\":[\"Informations sur le serveur\"],\"mHS8lb\":[\"Message dans #\",[\"0\"]],\"mMYBD9\":[\"Large – Portée de protection étendue\"],\"mTGsPd\":[\"Sujet du salon\"],\"mU8j6O\":[\"Pas de messages externes (+n)\"],\"mZp8FL\":[\"Retour automatique à une seule ligne\"],\"mdQu8G\":[\"VotrePseudo\"],\"miSSBQ\":[\"Commentaires (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"L'utilisateur est authentifié\"],\"mwtcGl\":[\"Fermer les commentaires\"],\"myL0MR\":[\"Supprimer ce réseau ?\"],\"mzI/c+\":[\"Télécharger\"],\"n3fGRk\":[\"défini par \",[\"0\"]],\"n5+j9l\":[\"ex. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Détendu – Protection moins agressive\"],\"nNflMD\":[\"Quitter le canal\"],\"nPXkBi\":[\"Chargement des données WHOIS...\"],\"nQnxxF\":[\"Message dans #\",[\"0\"],\" (Maj+Entrée pour nouvelle ligne)\"],\"nWMRxa\":[\"Désépingler\"],\"nkC032\":[\"Aucun profil anti-flood\"],\"o69z4d\":[\"Envoyer un message d'avertissement à \",[\"username\"]],\"o9ylQi\":[\"Recherchez des GIFs pour commencer\"],\"oFGkER\":[\"Avis du serveur\"],\"oOi11l\":[\"Défiler vers le bas\"],\"oPYIL5\":[\"réseau\"],\"oQEzQR\":[\"Nouveau message privé\"],\"oXOSPE\":[\"En ligne\"],\"oal760\":[\"Des attaques man-in-the-middle sur les liens serveur sont possibles\"],\"oeqmmJ\":[\"Sources de confiance\"],\"optX0N\":[\"il y a \",[\"0\"],\" h\"],\"ovBPCi\":[\"Par défaut\"],\"p0Z69r\":[\"Le modèle ne peut pas être vide\"],\"p1KgtK\":[\"Échec du chargement audio\"],\"p59pEv\":[\"Détails supplémentaires\"],\"p7sRI6\":[\"Informer les autres que vous écrivez\"],\"pBm1od\":[\"Canal secret\"],\"pNmiXx\":[\"Votre pseudo par défaut pour tous les serveurs\"],\"pUUo9G\":[\"Nom d'hôte :\"],\"pVGPmz\":[\"Mot de passe du compte\"],\"peNE68\":[\"Permanent\"],\"plhHQt\":[\"Aucune donnée\"],\"pm6+q5\":[\"Avertissement de sécurité\"],\"pn5qSs\":[\"Informations supplémentaires\"],\"q0cR4S\":[\"est maintenant connu sous le nom de **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Le salon n'apparaîtra pas dans les commandes LIST ou NAMES\"],\"qLpTm/\":[\"Supprimer la réaction \",[\"emoji\"]],\"qVkGWK\":[\"Épingler\"],\"qY8wNa\":[\"Page d'accueil\"],\"qb0xJ7\":[\"Jokers : * correspond à toute séquence, ? à un seul caractère. Exemples : nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Clé du salon (+k)\"],\"qtoOYG\":[\"Aucune limite\"],\"r1W2AS\":[\"Image hébergée\"],\"rIPR2O\":[\"Sujet défini avant (min)\"],\"rMMSYo\":[\"La longueur maximale est \",[\"0\"]],\"rWtzQe\":[\"Le réseau s'est divisé et reconnecté. ✅\"],\"rYG2u6\":[\"Veuillez patienter...\"],\"rdUucN\":[\"Aperçu\"],\"rjGI/Q\":[\"Confidentialité\"],\"rk8iDX\":[\"Chargement des GIFs...\"],\"rn6SBY\":[\"Rétablir le son\"],\"s/UKqq\":[\"A été expulsé du canal\"],\"s7oqXR\":[\"Sélectionnez un réseau\"],\"s8cATI\":[\"a rejoint \",[\"channelName\"]],\"sCO9ue\":[\"La connexion à <0>\",[\"serverName\"],\"0> présente les problèmes de sécurité suivants :\"],\"sGH11W\":[\"Serveur\"],\"sHI1H+\":[\"est maintenant connu sous le nom de **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" vous a invité à rejoindre \",[\"channel\"]],\"sUBSbK\":[\"Aucun réseau en amont pour le moment.\"],\"sby+1/\":[\"Cliquer pour copier\"],\"sfN25C\":[\"Votre nom réel ou complet\"],\"sliuzR\":[\"Ouvrir le lien\"],\"sqrO9R\":[\"Mentions personnalisées\"],\"sr6RdJ\":[\"Multiligne avec Shift+Entrée\"],\"swrCpB\":[\"Le canal a été renommé de \",[\"oldName\"],\" en \",[\"newName\"],\" par \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Avancé\"],\"t/YqKh\":[\"Supprimer\"],\"t47eHD\":[\"Votre identifiant unique sur ce serveur\"],\"tAkAh0\":[\"URL avec substitution optionnelle \",[\"size\"],\". Exemple : https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Afficher ou masquer la barre latérale de la liste des canaux\"],\"tfDRzk\":[\"Enregistrer\"],\"tiBsJk\":[\"a quitté \",[\"channelName\"]],\"tt4/UD\":[\"a quitté (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Le pseudo {nick} est déjà utilisé, nouvel essai avec {newNick}\"],\"u0a8B4\":[\"S'authentifier en tant qu'opérateur IRC pour l'accès administratif\"],\"u0rWFU\":[\"Créé après (min)\"],\"u72w3t\":[\"Utilisateurs et modèles à ignorer\"],\"u7jc2L\":[\"a quitté\"],\"uAQUqI\":[\"Statut\"],\"uB85T3\":[\"Échec de l'enregistrement : \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"Serveurs IRC :\"],\"ukyW4o\":[\"Vos liens d'invitation\"],\"usSSr/\":[\"Niveau de zoom\"],\"v7uvcf\":[\"Logiciel :\"],\"vE8kb+\":[\"Shift+Entrée pour les nouvelles lignes (Entrée envoie)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Pas de sujet\"],\"vSJd18\":[\"Vidéo\"],\"vXIe7J\":[\"Langue\"],\"vaHYxN\":[\"Vrai nom\"],\"vhjbKr\":[\"Absent\"],\"w/nogd\":[[\"0\"],\" réseau\",[\"1\"],\" — choisissez-en un à rejoindre\"],\"w4NYox\":[\"client \",[\"title\"]],\"w8xQRx\":[\"Valeur invalide\"],\"wFjjxZ\":[\"a été expulsé de \",[\"channelName\"],\" par \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Aucune exception de bannissement trouvée\"],\"wPrGnM\":[\"Administrateur du canal\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Afficher quand des utilisateurs rejoignent ou quittent des salons\"],\"whqZ9r\":[\"Mots ou phrases supplémentaires à surligner\"],\"wm7RV4\":[\"Son de notification\"],\"wz/Yoq\":[\"Vos messages pourraient être interceptés lors du relais entre serveurs\"],\"x3+y8b\":[\"Nombre de personnes inscrites via ce lien\"],\"xCJdfg\":[\"Effacer\"],\"xOTzt5\":[\"Ã\xA0 l'instant\"],\"xUHRTR\":[\"S'authentifier automatiquement comme opérateur à la connexion\"],\"xWHwwQ\":[\"Bannissements\"],\"xYilR2\":[\"Médias\"],\"xbi8D6\":[\"Ce serveur ne prend pas en charge les liens d'invitation (la capacité<0>obby.world/invitation0>n'est pas annoncée). Vous pouvez toujours discuter normalement ; ce panneau est destiné aux réseaux propulsés par obbyircd.\"],\"xceQrO\":[\"Seuls les websockets sécurisés sont pris en charge\"],\"xdtXa+\":[\"nom-du-salon\"],\"xfXC7q\":[\"Salons textuels\"],\"xlCYOE\":[\"Chargement des messages...\"],\"xlhswE\":[\"La valeur minimale est \",[\"0\"]],\"xq97Ci\":[\"Ajouter un mot ou une expression...\"],\"xuRqRq\":[\"Limite de clients (+l)\"],\"xwF+7J\":[[\"0\"],\" est en train d'écrire...\"],\"y1eoq1\":[\"Copier le lien\"],\"yJztBY\":[\"Supprimer le réseau\"],\"yNeucF\":[\"Ce serveur ne supporte pas les métadonnées de profil étendues (extension IRCv3 METADATA). Les champs comme l'avatar, le nom d'affichage et le statut ne sont pas disponibles.\"],\"yPlrca\":[\"Avatar du salon\"],\"yQE2r9\":[\"Chargement\"],\"ySU+JY\":[\"votre@email.com\"],\"yTX1Rt\":[\"Nom d'utilisateur opérateur\"],\"yYOzWD\":[\"journaux\"],\"yfx9Re\":[\"Mot de passe opérateur IRC\"],\"ygCKqB\":[\"Arrêter\"],\"ymDxJx\":[\"Nom d'utilisateur opérateur IRC\"],\"yrpRsQ\":[\"Trier par nom\"],\"yz7wBu\":[\"Fermer\"],\"zJw+jA\":[\"définit le mode : \",[\"0\"]],\"zbymaY\":[\"il y a \",[\"0\"],\" min\"],\"zebeLu\":[\"Saisir le nom d'utilisateur oper\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/fr/messages.po b/src/locales/fr/messages.po
index daaf837f..296f6cff 100644
--- a/src/locales/fr/messages.po
+++ b/src/locales/fr/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - Amener IRC vers le futur"
msgid "— open in viewer"
msgstr "— ouvrir dans le visualiseur"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(hériter)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(inchangé)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} et {1} sont en train d'écrire..."
msgid "{0} is typing..."
msgstr "{0} est en train d'écrire..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} réseau{1} — choisissez-en un à rejoindre"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -205,6 +221,12 @@ msgstr "Ajouter un masque d'invitation (ex. nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "Ajouter un serveur IRC"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Ajouter un réseau"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Ajouter une règle"
msgid "Add Server"
msgstr "Ajouter un serveur"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Ajoutez votre premier réseau"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Détails supplémentaires"
@@ -377,6 +403,10 @@ msgstr "Retour"
msgid "Back to image"
msgstr "Retour à l'image"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Retour à la liste des réseaux"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "Bannir {username} par hostmask (l'empêche de rejoindre depuis la même adresse IP/hôte)"
@@ -424,6 +454,10 @@ msgstr "Parcourir tous les canaux du serveur"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Annuler la connexion"
msgid "Cancel reply"
msgstr "Annuler la réponse"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Changer la couleur d'accent"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Changer le nom du canal (opérateurs uniquement)"
@@ -564,7 +603,7 @@ msgstr "Effacer la recherche"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "Cliquez à nouveau pour confirmer"
+msgstr "Cliquez à nouveau pour confirmer"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "Limite de clients (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -669,6 +710,7 @@ msgid "Confirm?"
msgstr "Confirmer ?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Connecter"
@@ -739,15 +781,15 @@ msgstr "Copier l'URL"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create"
-msgstr "Créer"
+msgstr "Créer"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "Créer un nouveau lien d'invitation"
+msgstr "Créer un nouveau lien d'invitation"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "Créer et gérer vos liens d'invitation"
+msgstr "Créer et gérer vos liens d'invitation"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,6 +856,10 @@ msgstr "Supprimer le canal"
msgid "Delete message"
msgstr "Supprimer le message"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Supprimer le réseau"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Supprimer la conversation privée"
@@ -826,15 +872,36 @@ msgstr "Supprimer cette invitation"
msgid "Delete this message? This cannot be undone."
msgstr "Supprimer ce message ? Cette action est irréversible."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Supprimer ce réseau ?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "Description (optionnelle, ex. « Bêta-testeurs T3 »)"
+msgstr "Description (optionnelle, ex. « Bêta-testeurs T3 »)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Déconnecter"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "Déconnecter <0>{0}0> ?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Déconnecter du bouncer soju ?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Déconnecter le réseau ?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Découvrir"
@@ -891,20 +958,31 @@ msgstr "Télécharger"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "Déposez les fichiers pour les téléverser"
+msgstr "Déposez les fichiers pour les téléverser"
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "ex. <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "ex. : 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Modifier"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "Modifier {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Modifier le profil"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "ACCUEIL"
msgid "Homepage"
msgstr "Page d'accueil"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Hôte"
@@ -1285,7 +1364,7 @@ msgstr "A rejoint le canal"
#: src/components/ui/InvitationsPanel.tsx
msgid "just now"
-msgstr "à l'instant"
+msgstr "Ã l'instant"
#: src/components/ui/ModerationModal.tsx
#: src/components/ui/UserContextMenu.tsx
@@ -1323,7 +1402,7 @@ msgstr "Quitter le canal"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Laissez le canal vide pour une invitation générique au réseau. La description sert uniquement à vos notes — visible uniquement par vous dans cette liste."
+msgstr "Laissez le canal vide pour une invitation générique au réseau. La description sert uniquement à vos notes â visible uniquement par vous dans cette liste."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "A quitté le canal"
msgid "Let others know when you are typing"
msgstr "Informer les autres que vous écrivez"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Aperçu du lien"
@@ -1375,6 +1458,10 @@ msgstr "Chargement des GIFs..."
msgid "Loading more channels..."
msgstr "Chargement de canaux supplémentaires..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Chargement des réseaux de votre bouncer…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "Chargement des données WHOIS..."
@@ -1563,12 +1650,23 @@ msgstr "Nom :"
#: src/components/ui/InvitationsPanel.tsx
msgid "network"
-msgstr "réseau"
+msgstr "réseau"
+
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Réseau lié via le bouncer soju"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Nom du réseau"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Réseaux sur {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Nouveau message privé"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (ex. : spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Aucun fichier choisi"
msgid "No flood profile"
msgstr "Aucun profil anti-flood"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "aucun hôte défini"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Aucune invitation trouvée"
@@ -1672,7 +1775,7 @@ msgstr "Aucun aperçu de média n'est chargé."
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "Aucun trafic IRC brut capturé pour le moment. Essayez de vous connecter ou d'envoyer un message."
+msgstr "Aucun trafic IRC brut capturé pour le moment. Essayez de vous connecter ou d'envoyer un message."
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "Aucun résultat trouvé"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "Aucun serveur sélectionné. Choisissez d'abord un serveur dans la barre latérale ; les liens d'invitation sont gérés par serveur."
+msgstr "Aucun serveur sélectionné. Choisissez d'abord un serveur dans la barre latérale ; les liens d'invitation sont gérés par serveur."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "Aucun sujet défini"
msgid "No unread mentions or messages"
msgstr "Aucune mention ou message non lu"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Aucun réseau en amont pour le moment."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Aucun utilisateur disponible"
@@ -1784,6 +1891,10 @@ msgstr "Oups ! La réseau s'est divisé ! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Ouvrir"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Ouvrir les paramètres de configuration du canal"
@@ -1887,6 +1998,10 @@ msgstr "Épingler la conversation privée"
msgid "Pin this private message conversation"
msgstr "Épingler cette conversation privée"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Texte en clair"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "MP à l'utilisateur"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Port"
@@ -2006,6 +2122,7 @@ msgstr "a réagi à ce message"
msgid "Read more"
msgstr "Lire la suite"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Raison"
msgid "Reason (optional)"
msgstr "Raison (facultatif)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Se reconnecter au serveur"
@@ -2095,6 +2213,7 @@ msgstr "Règles"
msgid "Safe"
msgstr "Sécurisé"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Avancer"
msgid "Select a channel"
msgstr "Sélectionner un canal"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Sélectionnez un réseau"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Sélectionner un membre"
@@ -2276,6 +2399,10 @@ msgstr "Les opérateurs du réseau pourraient potentiellement lire vos messages"
msgid "Server Password"
msgstr "Mot de passe du serveur"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Mot de passe du serveur (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Les communications entre serveurs peuvent utiliser des connexions non chiffrées"
@@ -2380,6 +2507,12 @@ msgstr "Connecté depuis"
msgid "Software:"
msgstr "Logiciel :"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "bouncer soju (contrôle)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Trier par nom"
@@ -2459,18 +2592,31 @@ msgstr "Cette image a expiré"
msgid "This many people registered through this link"
msgstr "Nombre de personnes inscrites via ce lien"
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Cela supprime le réseau de votre bouncer soju. Pour le réutiliser, vous devrez l'ajouter à nouveau."
+
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
msgstr "Ce serveur ne supporte pas les métadonnées de profil étendues (extension IRCv3 METADATA). Les champs comme l'avatar, le nom d'affichage et le statut ne sont pas disponibles."
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Ce serveur ne prend pas en charge les liens d'invitation (la capacité<0>obby.world/invitation0>n'est pas annoncée). Vous pouvez toujours discuter normalement ; ce panneau est destiné aux réseaux propulsés par obbyircd."
+msgstr "Ce serveur ne prend pas en charge les liens d'invitation (la capacité<0>obby.world/invitation0>n'est pas annoncée). Vous pouvez toujours discuter normalement ; ce panneau est destiné aux réseaux propulsés par obbyircd."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Ce serveur ne prend en charge qu'un seul type de connexion"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Cela fermera également les {0} réseaux liés ci-dessous."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Cela fermera également le réseau lié ci-dessous."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Temps (min)"
@@ -2479,6 +2625,10 @@ msgstr "Temps (min)"
msgid "Time Window (seconds)"
msgstr "Fenêtre temporelle (secondes)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Sujet :"
msgid "Total: {0}"
msgstr "Total : {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Transport"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Sources de confiance"
@@ -2615,7 +2769,7 @@ msgstr "Jokers : * correspond à toute séquence, ? à un seul caractère. Exemp
#: src/components/ui/InvitationsPanel.tsx
msgid "used"
-msgstr "utilisé"
+msgstr "utilisé"
#: src/components/message/JsonLogMessage.tsx
#: src/components/ui/UserProfileModal.tsx
@@ -2641,6 +2795,7 @@ msgstr "Profil de l'utilisateur"
msgid "User Settings"
msgstr "Paramètres utilisateur"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Large – Portée de protection étendue"
msgid "Will default to 'no reason' if left empty"
msgstr "Par défaut « aucune raison » si laissé vide"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Oui, supprimer"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "Vous avez des modifications non enregistrées. Voulez-vous vraiment ferm
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "Vous n'avez encore créé aucun lien d'invitation. Utilisez le formulaire ci-dessus pour créer le premier."
+msgstr "Vous n'avez encore créé aucun lien d'invitation. Utilisez le formulaire ci-dessus pour créer le premier."
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "Vous avez invité {target} à rejoindre {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Vous êtes connecté à <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Votre mot de passe de compte pour l'authentification"
@@ -2822,6 +2986,10 @@ msgstr "Votre mot de passe de compte pour l'authentification"
msgid "Your account username for authentication"
msgstr "Votre nom d'utilisateur de compte pour l'authentification"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Votre bouncer n'a aucun réseau pour le moment. Ajoutez-en un pour commencer."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Votre pseudo par défaut pour tous les serveurs"
diff --git a/src/locales/it/messages.mjs b/src/locales/it/messages.mjs
index 2f027263..68a71f10 100644
--- a/src/locales/it/messages.mjs
+++ b/src/locales/it/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Formato pattern non valido. Usa il formato nick!user@host (wildcards * consentiti)\"],\"+6NQQA\":[\"Canale di supporto generale\"],\"+6NyRG\":[\"Client\"],\"+K0AvT\":[\"Disconnetti\"],\"+cyFdH\":[\"Messaggio predefinito quando ci si segna come assenti\"],\"+mVPqU\":[\"Mostra la formattazione Markdown nei messaggi\"],\"+vqCJH\":[\"Il tuo nome utente account per l'autenticazione\"],\"+yPBXI\":[\"Scegli file\"],\"+zy2Nq\":[\"Tipo\"],\"/09cao\":[\"Sicurezza link bassa (Livello \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Gli utenti esterni non possono inviare messaggi\"],\"/4C8U0\":[\"Copia tutto\"],\"/6BzZF\":[\"Attiva/Disattiva lista membri\"],\"/AkXyp\":[\"Confermare?\"],\"/TNOPk\":[\"L'utente è assente\"],\"/XQgft\":[\"Scopri\"],\"/cF7Rs\":[\"Volume\"],\"/dqduX\":[\"Pagina successiva\"],\"/fc3q4\":[\"Tutto il contenuto\"],\"/kISDh\":[\"Abilita suoni di notifica\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Riproduci suoni per menzioni e messaggi\"],\"0/0ZGA\":[\"Maschera nome canale\"],\"0D6j7U\":[\"Scopri di più sulle regole personalizzate →\"],\"0XsHcR\":[\"Espelli utente\"],\"0ZpE//\":[\"Ordina per utenti\"],\"0bEPwz\":[\"Imposta assente\"],\"0dGkPt\":[\"Espandi lista canali\"],\"0gS7M5\":[\"Nome visualizzato\"],\"0kS+M8\":[\"EsempioRET\"],\"0rgoY7\":[\"Connettiti solo ai server che scegli\"],\"0wdd7X\":[\"Entra\"],\"0wkVYx\":[\"Messaggi privati\"],\"111uHX\":[\"Anteprima link\"],\"196EG4\":[\"Elimina chat privata\"],\"1DSr1i\":[\"Registra un account\"],\"1O/24y\":[\"Attiva/Disattiva lista canali\"],\"1VPJJ2\":[\"Avviso link esterno\"],\"1ZC/dv\":[\"Nessuna menzione o messaggio non letto\"],\"1pO1zi\":[\"Il nome del server è obbligatorio\"],\"1uwfzQ\":[\"Visualizza topic del canale\"],\"268g7c\":[\"Inserisci nome visualizzato\"],\"2F9+AZ\":[\"Nessun traffico IRC raw ancora catturato. Prova a connetterti o a inviare un messaggio.\"],\"2FOFq1\":[\"Gli operatori del server sulla rete potrebbero leggere i tuoi messaggi\"],\"2FYpfJ\":[\"Altro\"],\"2HF1Y2\":[[\"inviter\"],\" ha invitato \",[\"target\"],\" a unirsi a \",[\"channel\"]],\"2I70QL\":[\"Visualizza informazioni profilo utente\"],\"2QYdmE\":[\"Utenti:\"],\"2QpEjG\":[\"è uscito\"],\"2YE223\":[\"Messaggio in #\",[\"0\"],\" (Invio per nuova riga, Shift+Invio per inviare)\"],\"2bimFY\":[\"Usa password del server\"],\"2iTmdZ\":[\"Archiviazione locale:\"],\"2odkwe\":[\"Rigoroso – Protezione più aggressiva\"],\"2uDhbA\":[\"Inserisci il nome utente da invitare\"],\"2ygf/L\":[\"← Indietro\"],\"2zEgxj\":[\"Cerca GIF...\"],\"3RdPhl\":[\"Rinomina canale\"],\"3THokf\":[\"Utente con diritto di parola\"],\"3TSz9S\":[\"Minimizza\"],\"3jBDvM\":[\"Nome visualizzato del canale\"],\"3ryuFU\":[\"Segnalazioni di crash opzionali per migliorare l'app\"],\"3uBF/8\":[\"Chiudi visualizzatore\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Inserisci nome account...\"],\"4/Rr0R\":[\"Invita un utente nel canale corrente\"],\"4EZrJN\":[\"Regole\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Profilo flood (+F)\"],\"4RZQRK\":[\"Cosa stai facendo?\"],\"4hfTrB\":[\"Nickname\"],\"4n99LO\":[\"Già in \",[\"0\"]],\"4t6vMV\":[\"Passa automaticamente a riga singola per messaggi brevi\"],\"4vsHmf\":[\"Tempo (min)\"],\"5+INAX\":[\"Evidenzia i messaggi che ti menzionano\"],\"5R5Pv/\":[\"Nome oper\"],\"678PKt\":[\"Nome rete\"],\"6Aih4U\":[\"Non in linea\"],\"6CO3WE\":[\"Password richiesta per entrare. Lascia vuoto per rimuovere la chiave.\"],\"6HhMs3\":[\"Messaggio di uscita\"],\"6V3Ea3\":[\"Copiato\"],\"6lGV3K\":[\"Mostra meno\"],\"6yFOEi\":[\"Inserisci password oper...\"],\"7+IHTZ\":[\"Nessun file scelto\"],\"73hrRi\":[\"nick!user@host (es., spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Invia messaggio privato\"],\"7U1W7c\":[\"Molto rilassato\"],\"7Y1YQj\":[\"Nome reale:\"],\"7YHArF\":[\"— apri nel visualizzatore\"],\"7fjnVl\":[\"Cerca utenti...\"],\"7jL88x\":[\"Eliminare questo messaggio? Questa azione non può essere annullata.\"],\"7nGhhM\":[\"A cosa stai pensando?\"],\"7sEpu1\":[\"Membri — \",[\"0\"]],\"7sNhEz\":[\"Nome utente\"],\"8H0Q+x\":[\"Scopri di più sui profili →\"],\"8Phu0A\":[\"Mostra quando gli utenti cambiano soprannome\"],\"8XTG9e\":[\"Inserisci password oper\"],\"8XsV2J\":[\"Riprova invio\"],\"8ZsakT\":[\"Password\"],\"8kR84m\":[\"Stai per aprire un link esterno:\"],\"8lCgih\":[\"Rimuovi regola\"],\"8o3dPc\":[\"Rilascia i file per caricarli\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"si è unito\"],\"other\":[\"si è unito \",[\"joinCount\"],\" volte\"]}]],\"9BMLnJ\":[\"Riconnetti al server\"],\"9OEgyT\":[\"Aggiungi reazione\"],\"9PQ8m2\":[\"G-Line (ban globale)\"],\"9Qs99X\":[\"Email:\"],\"9QupBP\":[\"Rimuovi pattern\"],\"9bG48P\":[\"Invio in corso\"],\"9f5f0u\":[\"Domande sulla privacy? Contattaci:\"],\"9unqs3\":[\"Assente:\"],\"9v3hwv\":[\"Nessun server trovato.\"],\"9zb2WA\":[\"Connessione in corso\"],\"A1taO8\":[\"Cerca\"],\"A2adVi\":[\"Invia notifiche di digitazione\"],\"A9Rhec\":[\"Nome canale\"],\"AWOSPo\":[\"Ingrandisci\"],\"AXSpEQ\":[\"Oper alla connessione\"],\"AeXO77\":[\"Account\"],\"AhNP40\":[\"Cerca posizione\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Nickname cambiato\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Annulla risposta\"],\"ApSx0O\":[\"Trovati \",[\"0\"],\" messaggi corrispondenti a \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Nessun risultato trovato\"],\"AyNqAB\":[\"Mostra tutti gli eventi del server in chat\"],\"B/QqGw\":[\"Lontano dalla tastiera\"],\"B8AaMI\":[\"Questo campo è obbligatorio\"],\"BA2c49\":[\"Il server non supporta il filtro LIST avanzato\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" e altri \",[\"3\"],\" stanno scrivendo...\"],\"BGul2A\":[\"Hai modifiche non salvate. Sei sicuro di voler chiudere senza salvare?\"],\"BIf9fi\":[\"Il tuo messaggio di stato\"],\"BPm98R\":[\"Nessun server selezionato. Scegli prima un server dalla barra laterale; i link di invito sono gestiti per server.\"],\"BZz3md\":[\"Il tuo sito web personale\"],\"Bgm/H7\":[\"Consenti l'inserimento di più righe di testo\"],\"BiQIl1\":[\"Fissa questa conversazione privata\"],\"BlNZZ2\":[\"Clicca per andare al messaggio\"],\"Bowq3c\":[\"Solo gli operatori possono cambiare l'argomento\"],\"Btozzp\":[\"Questa immagine è scaduta\"],\"Bycfjm\":[\"Totale: \",[\"0\"]],\"C6IBQc\":[\"Copia JSON completo\"],\"C9L9wL\":[\"Raccolta dati\"],\"CDq4wC\":[\"Modera utente\"],\"CHVRxG\":[\"Messaggio a @\",[\"0\"],\" (Shift+Invio per nuova riga)\"],\"CN9zdR\":[\"Nome oper e password sono obbligatori\"],\"CW3sYa\":[\"Aggiungi reazione \",[\"emoji\"]],\"CaAkqd\":[\"Mostra disconnessioni\"],\"CbvaYj\":[\"Banna per nickname\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Seleziona un canale\"],\"CsekCi\":[\"Normale\"],\"D+NlUC\":[\"Sistema\"],\"D28t6+\":[\"è entrato e uscito\"],\"DB8zMK\":[\"Applica\"],\"DBcWHr\":[\"File audio di notifica personalizzato\"],\"DTy9Xw\":[\"Anteprime multimediali\"],\"Dj4pSr\":[\"Scegli una password sicura\"],\"Du+zn+\":[\"Ricerca...\"],\"Du2T2f\":[\"Impostazione non trovata\"],\"DwsSVQ\":[\"Applica filtri e aggiorna\"],\"E3W/zd\":[\"Soprannome predefinito\"],\"E6nRW7\":[\"Copia URL\"],\"E703RG\":[\"Modi:\"],\"EAeu1Z\":[\"Invia invito\"],\"EFKJQT\":[\"Impostazione\"],\"EGPQBv\":[\"Regole flood personalizzate (+f)\"],\"ELik0r\":[\"Vedi l'informativa completa sulla privacy\"],\"EPbeC2\":[\"Visualizza o modifica il topic del canale\"],\"EQCDNT\":[\"Inserisci nome utente oper...\"],\"EUvulZ\":[\"Trovato 1 messaggio corrispondente a \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Immagine successiva\"],\"EdQY6l\":[\"Nessuno\"],\"EnqLYU\":[\"Cerca server...\"],\"F0OKMc\":[\"Modifica server\"],\"F6Int2\":[\"Abilita evidenziazioni\"],\"FDoLyE\":[\"Utenti max.\"],\"FUU/hZ\":[\"Controlla quanti media esterni vengono caricati nella chat.\"],\"Fdp03t\":[\"attivo\"],\"FfPWR0\":[\"Finestra\"],\"FjkaiT\":[\"Riduci\"],\"FlqOE9\":[\"Cosa significa:\"],\"FolHNl\":[\"Gestisci il tuo account e l'autenticazione\"],\"Fp2Dif\":[\"Uscito dal server\"],\"G5KmCc\":[\"GZ-Line (Z-Line globale)\"],\"GDs0lz\":[\"<0>Rischio:0> Informazioni sensibili (messaggi, conversazioni private, dati di autenticazione) potrebbero essere esposte ad amministratori di rete o attaccanti posizionati tra i server IRC.\"],\"GR+2I3\":[\"Aggiungi maschera di invito (es. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Chiudi avvisi server in finestra separata\"],\"GdhD7H\":[\"Clicca di nuovo per confermare\"],\"GlHnXw\":[\"Cambio nick fallito: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Anteprima:\"],\"GtmO8/\":[\"da\"],\"GtuHUQ\":[\"Rinomina questo canale sul server. Tutti gli utenti vedranno il nuovo nome.\"],\"GuGfFX\":[\"Attiva/Disattiva ricerca\"],\"GxkJXS\":[\"Caricamento...\"],\"GzbwnK\":[\"È entrato nel canale\"],\"GzsUDB\":[\"Profilo esteso\"],\"H/PnT8\":[\"Inserisci emoji\"],\"H6Izzl\":[\"Il tuo codice colore preferito\"],\"H9jIv+\":[\"Mostra entrate/uscite\"],\"HAKBY9\":[\"Carica file\"],\"HdE1If\":[\"Canale\"],\"Hk4AW9\":[\"Il tuo nome visualizzato preferito\"],\"HmHDk7\":[\"Seleziona membro\"],\"HrQzPU\":[\"Canali su \",[\"networkName\"]],\"I2tXQ5\":[\"Messaggio a @\",[\"0\"],\" (Invio per nuova riga, Shift+Invio per inviare)\"],\"I6bw/h\":[\"Banna utente\"],\"I92Z+b\":[\"Abilita notifiche\"],\"I9D72S\":[\"Sei sicuro di voler eliminare questo messaggio? Questa azione non può essere annullata.\"],\"IA+1wo\":[\"Mostra quando gli utenti vengono espulsi dai canali\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Salva modifiche\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" e \",[\"2\"],\" stanno scrivendo...\"],\"IgrLD/\":[\"Pausa\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Rispondi\"],\"IoHMnl\":[\"Il valore massimo è \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Connessione...\"],\"J5T9NW\":[\"Informazioni utente\"],\"J8Y5+z\":[\"Ops! La rete si è divisa! ⚠️\"],\"JBHkBA\":[\"Ha lasciato il canale\"],\"JCwL0Q\":[\"Inserisci motivo (opzionale)\"],\"JFciKP\":[\"Attiva/Disattiva\"],\"JXGkhG\":[\"Cambia il nome del canale (solo operatori)\"],\"JcD7qf\":[\"Altre azioni\"],\"JdkA+c\":[\"Segreto (+s)\"],\"Jmu12l\":[\"Canali del server\"],\"JvQ++s\":[\"Abilita Markdown\"],\"K2jwh/\":[\"Nessun dato WHOIS disponibile\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Elimina messaggio\"],\"KKBlUU\":[\"Incorpora\"],\"KM0pLb\":[\"Benvenuto nel canale!\"],\"KR6W2h\":[\"Smetti di ignorare utente\"],\"KV+Bi1\":[\"Solo su invito (+i)\"],\"KdCtwE\":[\"Quanti secondi monitorare l'attività flood prima di reimpostare i contatori\"],\"Kkezga\":[\"Password del server\"],\"KsiQ/8\":[\"Gli utenti devono essere invitati per entrare\"],\"L+gB/D\":[\"Informazioni sul canale\"],\"LC1a7n\":[\"Il server IRC ha segnalato che i suoi collegamenti tra server hanno un basso livello di sicurezza. Ciò significa che quando i tuoi messaggi vengono instradati tra i server IRC nella rete, potrebbero non essere correttamente cifrati o i certificati SSL/TLS potrebbero non essere validati correttamente.\"],\"LNfLR5\":[\"Mostra espulsioni\"],\"LQb0W/\":[\"Mostra tutti gli eventi\"],\"LU7/yA\":[\"Nome alternativo per la visualizzazione. Può contenere spazi, emoji e caratteri speciali. Il nome reale (\",[\"channelName\"],\") verrà comunque usato per i comandi IRC.\"],\"LUb9O7\":[\"È richiesta una porta del server valida\"],\"LV4fT6\":[\"Descrizione (opzionale, es. \\\"Beta tester Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Informativa sulla privacy\"],\"LcuSDR\":[\"Gestisci le informazioni del tuo profilo e i metadati\"],\"LqLS9B\":[\"Mostra cambi di soprannome\"],\"LsDQt2\":[\"Impostazioni canale\"],\"LtI9AS\":[\"Proprietario\"],\"LuNhhL\":[\"ha reagito a questo messaggio\"],\"M/AZNG\":[\"URL dell'immagine del tuo avatar\"],\"M/WIer\":[\"Invia messaggio\"],\"M8er/5\":[\"Nome:\"],\"MHk+7g\":[\"Immagine precedente\"],\"MRorGe\":[\"Messaggio privato\"],\"MVbSGP\":[\"Finestra temporale (secondi)\"],\"MkpcsT\":[\"I tuoi messaggi e impostazioni sono archiviati localmente sul tuo dispositivo\"],\"N/hDSy\":[\"Segna come bot, di solito 'on' o vuoto\"],\"N7TQbE\":[\"Invita utente in \",[\"channelName\"]],\"NCca/o\":[\"Inserisci nickname predefinito...\"],\"Nqs6B9\":[\"Mostra tutti i media esterni. Qualsiasi URL potrebbe causare una richiesta a un server sconosciuto.\"],\"Nt+9O7\":[\"Usa WebSocket invece di TCP grezzo\"],\"NxIHzc\":[\"Espelli utente\"],\"O+v/cL\":[\"Sfoglia tutti i canali del server\"],\"ODwSCk\":[\"Invia un GIF\"],\"OGQ5kK\":[\"Configura suoni di notifica ed evidenziazioni\"],\"OIPt1Z\":[\"Mostra o nascondi la barra laterale dei membri\"],\"OKSNq/\":[\"Molto rigido\"],\"ONWvwQ\":[\"Carica\"],\"OVKoQO\":[\"La tua password account per l'autenticazione\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Portare IRC nel futuro\"],\"OhCpra\":[\"Imposta un topic…\"],\"OkltoQ\":[\"Banna \",[\"username\"],\" per nickname (impedisce di rientrare con lo stesso nick)\"],\"P+t/Te\":[\"Nessun dato aggiuntivo\"],\"P42Wcc\":[\"Sicuro\"],\"PD38l0\":[\"Anteprima avatar canale\"],\"PD9mEt\":[\"Scrivi un messaggio...\"],\"PPqfdA\":[\"Apri impostazioni configurazione canale\"],\"PSCjfZ\":[\"L'argomento visualizzato per questo canale. Tutti gli utenti possono vederlo.\"],\"PZCecv\":[\"Anteprima PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 volta\"],\"other\":[[\"c\"],\" volte\"]}]],\"PguS2C\":[\"Aggiungi maschera di eccezione (es. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Visualizzazione di \",[\"displayedChannelsCount\"],\" su \",[\"0\"],\" canali\"],\"PqhVlJ\":[\"Banna utente (per hostmask)\"],\"Q+chwU\":[\"Nome utente:\"],\"Q2QY4/\":[\"Elimina questo invito\"],\"Q6hhn8\":[\"Preferenze\"],\"QF4a34\":[\"Inserisci un nome utente\"],\"QGqSZ2\":[\"Colore e formattazione\"],\"QJQd1J\":[\"Modifica profilo\"],\"QSzGDE\":[\"Inattivo\"],\"QUlny5\":[\"Benvenuto su \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Leggi di più\"],\"QuSkCF\":[\"Filtra canali...\"],\"QwUrDZ\":[\"ha cambiato il topic in: \",[\"topic\"]],\"R0UH07\":[\"Immagine \",[\"0\"],\" di \",[\"1\"]],\"R7SsBE\":[\"Disattiva audio\"],\"R8rf1X\":[\"Clicca per impostare il topic\"],\"RArB3D\":[\"è stato espulso da \",[\"channelName\"],\" da \",[\"username\"]],\"RI3cWd\":[\"Scopri il mondo di IRC con ObsidianIRC\"],\"RIfHS5\":[\"Crea un nuovo link di invito\"],\"RMMaN5\":[\"Moderato (+m)\"],\"RWw9Lg\":[\"Chiudi finestra\"],\"RZ2BuZ\":[\"La registrazione dell'account \",[\"account\"],\" richiede verifica: \",[\"message\"]],\"RySp6q\":[\"Nascondi commenti\"],\"SPKQTd\":[\"Il nickname è obbligatorio\"],\"SPVjfj\":[\"Il valore predefinito sarà 'nessun motivo' se lasciato vuoto\"],\"SQKPvQ\":[\"Invita utente\"],\"SkZcl+\":[\"Scegli un profilo di protezione flood predefinito. Questi profili forniscono impostazioni di protezione bilanciate per diversi casi d'uso.\"],\"Slr+3C\":[\"Utenti min.\"],\"Spnlre\":[\"Hai invitato \",[\"target\"],\" a unirsi a \",[\"channel\"]],\"T/ckN5\":[\"Apri nel visualizzatore\"],\"T91vKp\":[\"Riproduci\"],\"TV2Wdu\":[\"Scopri come gestiamo i tuoi dati e proteggiamo la tua privacy.\"],\"TgFpwD\":[\"Applicazione...\"],\"TkzSFB\":[\"Nessuna modifica\"],\"TtserG\":[\"Inserisci nome reale\"],\"Ttz9J1\":[\"Inserisci password...\"],\"Tz0i8g\":[\"Impostazioni\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reagisci\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Non hai ancora creato alcun link di invito. Usa il modulo qui sopra per crearne uno.\"],\"UGT5vp\":[\"Salva impostazioni\"],\"UV5hLB\":[\"Nessun ban trovato\"],\"Uaj3Nd\":[\"Messaggi di stato\"],\"Ue3uny\":[\"Predefinito (nessun profilo)\"],\"UkARhe\":[\"Normale – Protezione standard\"],\"Umn7Cj\":[\"Ancora nessun commento. Sii il primo!\"],\"UtUIRh\":[[\"0\"],\" messaggi precedenti\"],\"UwzP+U\":[\"Connessione sicura\"],\"V0/A4O\":[\"Proprietario del canale\"],\"V4qgxE\":[\"Creato prima (min fa)\"],\"V8yTm6\":[\"Cancella ricerca\"],\"VJMMyz\":[\"ObsidianIRC - Portare IRC nel futuro\"],\"VJScHU\":[\"Motivo\"],\"VLsmVV\":[\"Silenzia notifiche\"],\"VbyRUy\":[\"Commenti\"],\"Vmx0mQ\":[\"Impostato da:\"],\"VqnIZz\":[\"Visualizza la nostra informativa sulla privacy e le pratiche sui dati\"],\"VrMygG\":[\"La lunghezza minima è \",[\"0\"]],\"VrnTui\":[\"I tuoi pronomi, mostrati nel profilo\"],\"W8E3qn\":[\"Account autenticato\"],\"WAakm9\":[\"Elimina canale\"],\"WFxTHC\":[\"Aggiungi maschera di ban (es. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"L'host del server è obbligatorio\"],\"WRYdXW\":[\"Posizione audio\"],\"WUOH5B\":[\"Ignora utente\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Mostra 1 altro elemento\"],\"other\":[\"Mostra \",[\"1\"],\" altri elementi\"]}]],\"WYxRzo\":[\"Crea e gestisci i tuoi link di invito\"],\"Wd38W1\":[\"Lascia il canale vuoto per un invito generico alla rete. La descrizione è solo per i tuoi appunti — visibile solo a te in questo elenco.\"],\"Weq9zb\":[\"Generale\"],\"Wfj7Sk\":[\"Attiva o disattiva i suoni delle notifiche\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Profilo utente\"],\"X6S3lt\":[\"Cerca impostazioni, canali, server...\"],\"XEHan5\":[\"Continua comunque\"],\"XI1+wb\":[\"Formato non valido\"],\"XIXeuC\":[\"Messaggio a @\",[\"0\"]],\"XMS+k4\":[\"Avvia messaggio privato\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Rimuovi fissaggio chat privata\"],\"Xm/s+u\":[\"Visualizzazione\"],\"Xp2n93\":[\"Mostra media dall'host di file attendibile del tuo server. Nessuna richiesta viene inviata a servizi esterni.\"],\"XvjC4F\":[\"Salvataggio...\"],\"Y/qryO\":[\"Nessun utente trovato corrispondente alla ricerca\"],\"YAqRpI\":[\"Registrazione dell'account \",[\"account\"],\" riuscita: \",[\"message\"]],\"YEfzvP\":[\"Argomento protetto (+t)\"],\"YQOn6a\":[\"Comprimi lista membri\"],\"YRCoE9\":[\"Operatore del canale\"],\"YURQaF\":[\"Vedi profilo\"],\"YdBSvr\":[\"Controlla la visualizzazione dei media e dei contenuti esterni\"],\"Yj6U3V\":[\"Nessun server centrale:\"],\"YjvpGx\":[\"Pronomi\"],\"YqH4l4\":[\"Nessuna chiave\"],\"YyUPpV\":[\"Account:\"],\"ZJSWfw\":[\"Messaggio mostrato alla disconnessione dal server\"],\"ZR1dJ4\":[\"Inviti\"],\"ZdWg0V\":[\"Apri nel browser\"],\"ZhRBbl\":[\"Cerca messaggi…\"],\"Zmcu3y\":[\"Filtri avanzati\"],\"a2/8e5\":[\"Argomento impostato dopo (min fa)\"],\"aHKcKc\":[\"Pagina precedente\"],\"aJTbXX\":[\"Password oper\"],\"aQryQv\":[\"Il pattern esiste già\"],\"aW9pLN\":[\"Numero massimo di utenti nel canale. Lascia vuoto per nessun limite.\"],\"ah4fmZ\":[\"Mostra anche anteprime da YouTube, Vimeo, SoundCloud e servizi noti simili.\"],\"aifXak\":[\"Nessun media in questo canale\"],\"ap2zBz\":[\"Rilassato\"],\"az8lvo\":[\"Disattivato\"],\"azXSNo\":[\"Espandi lista membri\"],\"azdliB\":[\"Accedi a un account\"],\"b26wlF\":[\"lei/la\"],\"bD/+Ei\":[\"Rigido\"],\"bQ6BJn\":[\"Configura regole dettagliate di protezione flood. Ogni regola specifica il tipo di attività da monitorare e l'azione da intraprendere quando le soglie vengono superate.\"],\"beV7+y\":[\"L'utente riceverà un invito per unirsi a \",[\"channelName\"],\".\"],\"bk84cH\":[\"Messaggio di assenza\"],\"bkHdLj\":[\"Aggiungi server IRC\"],\"bmQLn5\":[\"Aggiungi regola\"],\"bwRvnp\":[\"Azione\"],\"c8+EVZ\":[\"Account verificato\"],\"cGYUlD\":[\"Nessuna anteprima media caricata.\"],\"cLF98o\":[\"Mostra commenti (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Nessun utente disponibile\"],\"cSgpoS\":[\"Fissa chat privata\"],\"cde3ce\":[\"Messaggio a <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Copia output formattato\"],\"cl/A5J\":[\"Benvenuto su \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Elimina\"],\"coPLXT\":[\"Non archiviamo le tue comunicazioni IRC sui nostri server\"],\"crYH/6\":[\"Player SoundCloud\"],\"d3sis4\":[\"Aggiungi server\"],\"d9aN5k\":[\"Rimuovi \",[\"username\"],\" dal canale\"],\"dEgA5A\":[\"Annulla\"],\"dGi1We\":[\"Rimuovi il fissaggio di questa conversazione privata\"],\"dJVuyC\":[\"ha lasciato \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"a\"],\"dXqxlh\":[\"<0>⚠️ Rischio di sicurezza!0> Questa connessione potrebbe essere vulnerabile a intercettazioni o attacchi man-in-the-middle.\"],\"da9Q/R\":[\"Modalità canale modificate\"],\"dhJN3N\":[\"Mostra commenti\"],\"dj2xTE\":[\"Ignora notifica\"],\"dpCzmC\":[\"Impostazioni protezione flood\"],\"e9dQpT\":[\"Vuoi aprire questo link in una nuova scheda?\"],\"ePK91l\":[\"Modifica\"],\"eYBDuB\":[\"Carica un'immagine o fornisci un URL con sostituzione opzionale \",[\"size\"]],\"edBbee\":[\"Banna \",[\"username\"],\" per hostmask (impedisce di rientrare dallo stesso IP/host)\"],\"ekfzWq\":[\"Impostazioni utente\"],\"elPDWs\":[\"Personalizza la tua esperienza con il client IRC\"],\"eu2osY\":[\"<0>💡 Raccomandazione:0> Procedi solo se ti fidi di questo server e comprendi i rischi. Evita di condividere informazioni sensibili o password su questa connessione.\"],\"euEhbr\":[\"Clicca per unirti a \",[\"channel\"]],\"ez3vLd\":[\"Abilita input multiriga\"],\"f0J5Ki\":[\"Le comunicazioni tra server potrebbero usare connessioni non cifrate\"],\"f9BHJk\":[\"Avvisa utente\"],\"fDOLLd\":[\"Nessun canale trovato.\"],\"ffzDkB\":[\"Analisi anonime:\"],\"fq1GF9\":[\"Mostra quando gli utenti si disconnettono dal server\"],\"gEF57C\":[\"Questo server supporta solo un tipo di connessione\"],\"gJuLUI\":[\"Lista di ignorati\"],\"gNzMrk\":[\"Avatar attuale\"],\"gjPWyO\":[\"Inserisci nickname...\"],\"gz6UQ3\":[\"Massimizza\"],\"h6razj\":[\"Escludi maschera nome canale\"],\"hG6jnw\":[\"Nessun topic impostato\"],\"hG89Ed\":[\"Immagine\"],\"hYgDIe\":[\"Crea\"],\"hZ6znB\":[\"Porta\"],\"ha+Bz5\":[\"es., 100:1440\"],\"he3ygx\":[\"Copia\"],\"hehnjM\":[\"Quantità\"],\"hzdLuQ\":[\"Solo gli utenti con voice o superiore possono parlare\"],\"i0qMbr\":[\"Home\"],\"iDNBZe\":[\"Notifiche\"],\"iH8pgl\":[\"Indietro\"],\"iL9SZg\":[\"Banna utente (per nickname)\"],\"iNt+3c\":[\"Torna all'immagine\"],\"iQvi+a\":[\"Non avvisarmi sulla bassa sicurezza del link per questo server\"],\"iSLIjg\":[\"Connetti\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Host del server\"],\"idD8Ev\":[\"Salvato\"],\"iivqkW\":[\"Connesso dal\"],\"ij+Elv\":[\"Anteprima immagine\"],\"ilIWp7\":[\"Attiva/Disattiva notifiche\"],\"iuaqvB\":[\"Usa * come wildcard. Esempi: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Banna per maschera host\"],\"jA4uoI\":[\"Argomento:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Motivo (opzionale)\"],\"jUV7CU\":[\"Carica avatar\"],\"jW5Uwh\":[\"Controlla quanti media esterni vengono caricati. Disattivato / Sicuro / Fonti affidabili / Tutto il contenuto.\"],\"jXzms5\":[\"Opzioni allegato\"],\"jZlrte\":[\"Colore\"],\"jfC/xh\":[\"Contatti\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Carica messaggi precedenti\"],\"k3ID0F\":[\"Filtra membri…\"],\"k65gsE\":[\"Analisi approfondita\"],\"k7Zgob\":[\"Annulla connessione\"],\"kAVx5h\":[\"Nessun invito trovato\"],\"kCLEPU\":[\"Connesso a\"],\"kF5LKb\":[\"Pattern ignorati:\"],\"kGeOx/\":[\"Unisciti a \",[\"0\"]],\"kITKr8\":[\"Caricamento modalità canale...\"],\"kPpPsw\":[\"Sei un IRC Operator\"],\"kWJmRL\":[\"Tu\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Copia JSON\"],\"krViRy\":[\"Clicca per copiare come JSON\"],\"ks71ra\":[\"Eccezioni\"],\"kw4lRv\":[\"Semi-operatore del canale\"],\"kxgIRq\":[\"Seleziona o aggiungi un canale per iniziare.\"],\"ky6dWe\":[\"Anteprima avatar\"],\"l+GxCv\":[\"Caricamento canali...\"],\"l+IUVW\":[\"Verifica dell'account \",[\"account\"],\" riuscita: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"si è riconnesso\"],\"other\":[\"si è riconnesso \",[\"reconnectCount\"],\" volte\"]}]],\"l1l8sj\":[[\"0\"],\"g fa\"],\"l5NhnV\":[\"#canale (opzionale)\"],\"l5jmzx\":[[\"0\"],\" e \",[\"1\"],\" stanno scrivendo...\"],\"lCF0wC\":[\"Aggiorna\"],\"lHy8N5\":[\"Caricamento altri canali...\"],\"lasgrr\":[\"usato\"],\"lbpf14\":[\"Entra in \",[\"value\"]],\"lfFsZ4\":[\"Canali\"],\"lkNdiH\":[\"Nome account\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Carica immagine\"],\"loQxaJ\":[\"Sono tornato\"],\"lvfaxv\":[\"HOME\"],\"m16xKo\":[\"Aggiungi\"],\"m8flAk\":[\"Anteprima (non ancora caricata)\"],\"mEPxTp\":[\"<0>⚠️ Attenzione!0> Apri solo link da fonti attendibili. I link malevoli possono compromettere la tua sicurezza o privacy.\"],\"mHGdhG\":[\"Informazioni sul server\"],\"mHS8lb\":[\"Messaggio in #\",[\"0\"]],\"mMYBD9\":[\"Ampio – Portata di protezione estesa\"],\"mTGsPd\":[\"Argomento del canale\"],\"mU8j6O\":[\"Nessun messaggio esterno (+n)\"],\"mZp8FL\":[\"Ritorno automatico alla singola riga\"],\"mdQu8G\":[\"IlTuoNickname\"],\"miSSBQ\":[\"Commenti (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Utente autenticato\"],\"mwtcGl\":[\"Chiudi commenti\"],\"mzI/c+\":[\"Scarica\"],\"n3fGRk\":[\"impostato da \",[\"0\"]],\"nE9jsU\":[\"Rilassato – Protezione meno aggressiva\"],\"nNflMD\":[\"Abbandona canale\"],\"nPXkBi\":[\"Caricamento dati WHOIS...\"],\"nQnxxF\":[\"Messaggio in #\",[\"0\"],\" (Shift+Invio per nuova riga)\"],\"nWMRxa\":[\"Rimuovi fissaggio\"],\"nkC032\":[\"Nessun profilo flood\"],\"o69z4d\":[\"Invia un messaggio di avviso a \",[\"username\"]],\"o9ylQi\":[\"Cerca GIF per iniziare\"],\"oFGkER\":[\"Avvisi del server\"],\"oOi11l\":[\"Vai in fondo\"],\"oPYIL5\":[\"rete\"],\"oQEzQR\":[\"Nuovo messaggio privato\"],\"oXOSPE\":[\"In linea\"],\"oal760\":[\"Sono possibili attacchi man-in-the-middle sui link del server\"],\"oeqmmJ\":[\"Fonti attendibili\"],\"optX0N\":[[\"0\"],\"h fa\"],\"ovBPCi\":[\"Predefinito\"],\"p0Z69r\":[\"Il pattern non può essere vuoto\"],\"p1KgtK\":[\"Caricamento audio non riuscito\"],\"p59pEv\":[\"Dettagli aggiuntivi\"],\"p7sRI6\":[\"Avvisa gli altri quando stai scrivendo\"],\"pBm1od\":[\"Canale segreto\"],\"pNmiXx\":[\"Il tuo soprannome predefinito per tutti i server\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Password account\"],\"peNE68\":[\"Permanente\"],\"plhHQt\":[\"Nessun dato\"],\"pm6+q5\":[\"Avviso di sicurezza\"],\"pn5qSs\":[\"Informazioni aggiuntive\"],\"q0cR4S\":[\"ora è conosciuto come **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Il canale non apparirà nei comandi LIST o NAMES\"],\"qLpTm/\":[\"Rimuovi reazione \",[\"emoji\"]],\"qVkGWK\":[\"Fissa\"],\"qY8wNa\":[\"Homepage\"],\"qb0xJ7\":[\"Wildcard: * corrisponde a qualsiasi sequenza, ? a un singolo carattere. Esempi: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Chiave canale (+k)\"],\"qtoOYG\":[\"Nessun limite\"],\"r1W2AS\":[\"Immagine dal filehost\"],\"rIPR2O\":[\"Argomento impostato prima (min fa)\"],\"rMMSYo\":[\"La lunghezza massima è \",[\"0\"]],\"rWtzQe\":[\"La rete si è divisa e riconnessa. ✅\"],\"rYG2u6\":[\"Attendere...\"],\"rdUucN\":[\"Anteprima\"],\"rjGI/Q\":[\"Privacy\"],\"rk8iDX\":[\"Caricamento GIF...\"],\"rn6SBY\":[\"Attiva audio\"],\"s/UKqq\":[\"È stato espulso dal canale\"],\"s8cATI\":[\"si è unito a \",[\"channelName\"]],\"sCO9ue\":[\"La connessione a <0>\",[\"serverName\"],\"0> presenta le seguenti problematiche di sicurezza:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"ora è conosciuto come **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" ti ha invitato a unirti a \",[\"channel\"]],\"sby+1/\":[\"Clicca per copiare\"],\"sfN25C\":[\"Il tuo nome reale o completo\"],\"sliuzR\":[\"Apri link\"],\"sqrO9R\":[\"Menzioni personalizzate\"],\"sr6RdJ\":[\"Multiriga con Shift+Invio\"],\"swrCpB\":[\"Il canale è stato rinominato da \",[\"oldName\"],\" a \",[\"newName\"],\" da \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Avanzato\"],\"t/YqKh\":[\"Rimuovi\"],\"t47eHD\":[\"Il tuo identificatore unico su questo server\"],\"tAkAh0\":[\"URL con sostituzione opzionale \",[\"size\"],\". Esempio: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Mostra o nascondi la barra laterale dei canali\"],\"tfDRzk\":[\"Salva\"],\"tiBsJk\":[\"ha lasciato \",[\"channelName\"]],\"tt4/UD\":[\"ha abbandonato (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Il nick {nick} è già in uso, nuovo tentativo con {newNick}\"],\"u0a8B4\":[\"Autenticarsi come operatore IRC per l'accesso amministrativo\"],\"u0rWFU\":[\"Creato dopo (min fa)\"],\"u72w3t\":[\"Utenti e modelli da ignorare\"],\"u7jc2L\":[\"ha abbandonato\"],\"uAQUqI\":[\"Stato\"],\"uB85T3\":[\"Salvataggio fallito: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"Server IRC:\"],\"ukyW4o\":[\"I tuoi link di invito\"],\"usSSr/\":[\"Livello zoom\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Shift+Invio per le nuove righe (Invio invia)\"],\"vERlcd\":[\"Profilo\"],\"vK0RL8\":[\"Nessun argomento\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Lingua\"],\"vaHYxN\":[\"Nome reale\"],\"vhjbKr\":[\"Assente\"],\"w4NYox\":[\"client \",[\"title\"]],\"w8xQRx\":[\"Valore non valido\"],\"wFjjxZ\":[\"è stato espulso da \",[\"channelName\"],\" da \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nessuna eccezione di ban trovata\"],\"wPrGnM\":[\"Amministratore del canale\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Mostra quando gli utenti entrano o escono dai canali\"],\"whqZ9r\":[\"Parole o frasi aggiuntive da evidenziare\"],\"wm7RV4\":[\"Suono di notifica\"],\"wz/Yoq\":[\"I tuoi messaggi potrebbero essere intercettati durante l'instradamento tra server\"],\"x3+y8b\":[\"Numero di persone registrate tramite questo link\"],\"xCJdfg\":[\"Cancella\"],\"xOTzt5\":[\"proprio ora\"],\"xUHRTR\":[\"Autentica automaticamente come operatore alla connessione\"],\"xWHwwQ\":[\"Ban\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"Questo server non supporta i link di invito (la capability<0>obby.world/invitation0>non è annunciata). Puoi comunque chattare normalmente; questo pannello è per le reti basate su obbyircd.\"],\"xceQrO\":[\"Sono supportati solo websocket sicuri\"],\"xdtXa+\":[\"nome-canale\"],\"xfXC7q\":[\"Canali testuali\"],\"xlCYOE\":[\"Caricamento messaggi...\"],\"xlhswE\":[\"Il valore minimo è \",[\"0\"]],\"xq97Ci\":[\"Aggiungi una parola o frase...\"],\"xuRqRq\":[\"Limite client (+l)\"],\"xwF+7J\":[[\"0\"],\" sta scrivendo...\"],\"y1eoq1\":[\"Copia link\"],\"yNeucF\":[\"Questo server non supporta i metadati del profilo esteso (estensione IRCv3 METADATA). Campi come avatar, nome visualizzato e stato non sono disponibili.\"],\"yPlrca\":[\"Avatar del canale\"],\"yQE2r9\":[\"Caricamento\"],\"ySU+JY\":[\"tuo@email.com\"],\"yTX1Rt\":[\"Nome utente operatore\"],\"yYOzWD\":[\"log\"],\"yfx9Re\":[\"Password operatore IRC\"],\"ygCKqB\":[\"Stop\"],\"ymDxJx\":[\"Nome utente operatore IRC\"],\"yrpRsQ\":[\"Ordina per nome\"],\"yz7wBu\":[\"Chiudi\"],\"zJw+jA\":[\"imposta modalità: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"m fa\"],\"zebeLu\":[\"Inserisci nome utente oper\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Formato pattern non valido. Usa il formato nick!user@host (wildcards * consentiti)\"],\"+6NQQA\":[\"Canale di supporto generale\"],\"+6NyRG\":[\"Client\"],\"+K0AvT\":[\"Disconnetti\"],\"+cyFdH\":[\"Messaggio predefinito quando ci si segna come assenti\"],\"+mVPqU\":[\"Mostra la formattazione Markdown nei messaggi\"],\"+vqCJH\":[\"Il tuo nome utente account per l'autenticazione\"],\"+yPBXI\":[\"Scegli file\"],\"+zy2Nq\":[\"Tipo\"],\"/09cao\":[\"Sicurezza link bassa (Livello \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Gli utenti esterni non possono inviare messaggi\"],\"/4C8U0\":[\"Copia tutto\"],\"/6BzZF\":[\"Attiva/Disattiva lista membri\"],\"/AkXyp\":[\"Confermare?\"],\"/TNOPk\":[\"L'utente è assente\"],\"/XQgft\":[\"Scopri\"],\"/cF7Rs\":[\"Volume\"],\"/dqduX\":[\"Pagina successiva\"],\"/fc3q4\":[\"Tutto il contenuto\"],\"/kISDh\":[\"Abilita suoni di notifica\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Riproduci suoni per menzioni e messaggi\"],\"0/0ZGA\":[\"Maschera nome canale\"],\"0D6j7U\":[\"Scopri di più sulle regole personalizzate →\"],\"0XsHcR\":[\"Espelli utente\"],\"0ZpE//\":[\"Ordina per utenti\"],\"0bEPwz\":[\"Imposta assente\"],\"0dGkPt\":[\"Espandi lista canali\"],\"0gS7M5\":[\"Nome visualizzato\"],\"0kS+M8\":[\"EsempioRET\"],\"0rgoY7\":[\"Connettiti solo ai server che scegli\"],\"0wdd7X\":[\"Entra\"],\"0wkVYx\":[\"Messaggi privati\"],\"111uHX\":[\"Anteprima link\"],\"196EG4\":[\"Elimina chat privata\"],\"1DSr1i\":[\"Registra un account\"],\"1O/24y\":[\"Attiva/Disattiva lista canali\"],\"1TNIig\":[\"Apri\"],\"1VPJJ2\":[\"Avviso link esterno\"],\"1ZC/dv\":[\"Nessuna menzione o messaggio non letto\"],\"1pO1zi\":[\"Il nome del server è obbligatorio\"],\"1uwfzQ\":[\"Visualizza topic del canale\"],\"268g7c\":[\"Inserisci nome visualizzato\"],\"2CEOW6\":[\"Rete collegata tramite bouncer soju\"],\"2F9+AZ\":[\"Nessun traffico IRC raw ancora catturato. Prova a connetterti o a inviare un messaggio.\"],\"2FOFq1\":[\"Gli operatori del server sulla rete potrebbero leggere i tuoi messaggi\"],\"2FYpfJ\":[\"Altro\"],\"2HF1Y2\":[[\"inviter\"],\" ha invitato \",[\"target\"],\" a unirsi a \",[\"channel\"]],\"2I70QL\":[\"Visualizza informazioni profilo utente\"],\"2QYdmE\":[\"Utenti:\"],\"2QpEjG\":[\"è uscito\"],\"2YE223\":[\"Messaggio in #\",[\"0\"],\" (Invio per nuova riga, Shift+Invio per inviare)\"],\"2bimFY\":[\"Usa password del server\"],\"2iTmdZ\":[\"Archiviazione locale:\"],\"2odkwe\":[\"Rigoroso – Protezione più aggressiva\"],\"2uDhbA\":[\"Inserisci il nome utente da invitare\"],\"2ygf/L\":[\"← Indietro\"],\"2zEgxj\":[\"Cerca GIF...\"],\"3RdPhl\":[\"Rinomina canale\"],\"3THokf\":[\"Utente con diritto di parola\"],\"3TSz9S\":[\"Minimizza\"],\"3jBDvM\":[\"Nome visualizzato del canale\"],\"3ryuFU\":[\"Segnalazioni di crash opzionali per migliorare l'app\"],\"3uBF/8\":[\"Chiudi visualizzatore\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Inserisci nome account...\"],\"4/Rr0R\":[\"Invita un utente nel canale corrente\"],\"4EZrJN\":[\"Regole\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Profilo flood (+F)\"],\"4RZQRK\":[\"Cosa stai facendo?\"],\"4hfTrB\":[\"Nickname\"],\"4n99LO\":[\"Già in \",[\"0\"]],\"4t6vMV\":[\"Passa automaticamente a riga singola per messaggi brevi\"],\"4vsHmf\":[\"Tempo (min)\"],\"4x/Axu\":[\"Il tuo bouncer non ha ancora nessuna rete. Aggiungine una per iniziare.\"],\"5+INAX\":[\"Evidenzia i messaggi che ti menzionano\"],\"5R5Pv/\":[\"Nome oper\"],\"678PKt\":[\"Nome rete\"],\"6Aih4U\":[\"Non in linea\"],\"6CO3WE\":[\"Password richiesta per entrare. Lascia vuoto per rimuovere la chiave.\"],\"6HhMs3\":[\"Messaggio di uscita\"],\"6V3Ea3\":[\"Copiato\"],\"6lGV3K\":[\"Mostra meno\"],\"6yFOEi\":[\"Inserisci password oper...\"],\"7+IHTZ\":[\"Nessun file scelto\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (es., spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Invia messaggio privato\"],\"7U1W7c\":[\"Molto rilassato\"],\"7Y1YQj\":[\"Nome reale:\"],\"7YHArF\":[\"— apri nel visualizzatore\"],\"7fjnVl\":[\"Cerca utenti...\"],\"7jL88x\":[\"Eliminare questo messaggio? Questa azione non può essere annullata.\"],\"7nGhhM\":[\"A cosa stai pensando?\"],\"7sEpu1\":[\"Membri — \",[\"0\"]],\"7sNhEz\":[\"Nome utente\"],\"8H0Q+x\":[\"Scopri di più sui profili →\"],\"8Phu0A\":[\"Mostra quando gli utenti cambiano soprannome\"],\"8XTG9e\":[\"Inserisci password oper\"],\"8XsV2J\":[\"Riprova invio\"],\"8ZsakT\":[\"Password\"],\"8kR84m\":[\"Stai per aprire un link esterno:\"],\"8lCgih\":[\"Rimuovi regola\"],\"8o3dPc\":[\"Rilascia i file per caricarli\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"si è unito\"],\"other\":[\"si è unito \",[\"joinCount\"],\" volte\"]}]],\"9BMLnJ\":[\"Riconnetti al server\"],\"9OEgyT\":[\"Aggiungi reazione\"],\"9PQ8m2\":[\"G-Line (ban globale)\"],\"9Qs99X\":[\"Email:\"],\"9QupBP\":[\"Rimuovi pattern\"],\"9W7tl5\":[\"(invariato)\"],\"9bG48P\":[\"Invio in corso\"],\"9f5f0u\":[\"Domande sulla privacy? Contattaci:\"],\"9iweoP\":[\"Reti su \",[\"0\"]],\"9unqs3\":[\"Assente:\"],\"9v3hwv\":[\"Nessun server trovato.\"],\"9zb2WA\":[\"Connessione in corso\"],\"A1taO8\":[\"Cerca\"],\"A2adVi\":[\"Invia notifiche di digitazione\"],\"A9Rhec\":[\"Nome canale\"],\"AWOSPo\":[\"Ingrandisci\"],\"AXSpEQ\":[\"Oper alla connessione\"],\"AdKRCX\":[\"Sei connesso a <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Account\"],\"AhNP40\":[\"Cerca posizione\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Nickname cambiato\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Annulla risposta\"],\"ApSx0O\":[\"Trovati \",[\"0\"],\" messaggi corrispondenti a \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Nessun risultato trovato\"],\"AyNqAB\":[\"Mostra tutti gli eventi del server in chat\"],\"B/QqGw\":[\"Lontano dalla tastiera\"],\"B0sB2k\":[\"Testo in chiaro\"],\"B8AaMI\":[\"Questo campo è obbligatorio\"],\"BA2c49\":[\"Il server non supporta il filtro LIST avanzato\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" e altri \",[\"3\"],\" stanno scrivendo...\"],\"BGul2A\":[\"Hai modifiche non salvate. Sei sicuro di voler chiudere senza salvare?\"],\"BIf9fi\":[\"Il tuo messaggio di stato\"],\"BOJWfb\":[\"Disconnettersi dal bouncer soju?\"],\"BPm98R\":[\"Nessun server selezionato. Scegli prima un server dalla barra laterale; i link di invito sono gestiti per server.\"],\"BZz3md\":[\"Il tuo sito web personale\"],\"Bgm/H7\":[\"Consenti l'inserimento di più righe di testo\"],\"BiQIl1\":[\"Fissa questa conversazione privata\"],\"BlNZZ2\":[\"Clicca per andare al messaggio\"],\"Bowq3c\":[\"Solo gli operatori possono cambiare l'argomento\"],\"Btozzp\":[\"Questa immagine è scaduta\"],\"Bycfjm\":[\"Totale: \",[\"0\"]],\"C6IBQc\":[\"Copia JSON completo\"],\"C9L9wL\":[\"Raccolta dati\"],\"CDq4wC\":[\"Modera utente\"],\"CHVRxG\":[\"Messaggio a @\",[\"0\"],\" (Shift+Invio per nuova riga)\"],\"CN9zdR\":[\"Nome oper e password sono obbligatori\"],\"CW3sYa\":[\"Aggiungi reazione \",[\"emoji\"]],\"CaAkqd\":[\"Mostra disconnessioni\"],\"CbvaYj\":[\"Banna per nickname\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Seleziona un canale\"],\"CsekCi\":[\"Normale\"],\"D+NlUC\":[\"Sistema\"],\"D28t6+\":[\"è entrato e uscito\"],\"DB8zMK\":[\"Applica\"],\"DBcWHr\":[\"File audio di notifica personalizzato\"],\"DTy9Xw\":[\"Anteprime multimediali\"],\"Dj4pSr\":[\"Scegli una password sicura\"],\"Du+zn+\":[\"Ricerca...\"],\"Du2T2f\":[\"Impostazione non trovata\"],\"DwsSVQ\":[\"Applica filtri e aggiorna\"],\"E3W/zd\":[\"Soprannome predefinito\"],\"E6nRW7\":[\"Copia URL\"],\"E703RG\":[\"Modi:\"],\"EAeu1Z\":[\"Invia invito\"],\"EFKJQT\":[\"Impostazione\"],\"EGPQBv\":[\"Regole flood personalizzate (+f)\"],\"ELik0r\":[\"Vedi l'informativa completa sulla privacy\"],\"EPbeC2\":[\"Visualizza o modifica il topic del canale\"],\"EQCDNT\":[\"Inserisci nome utente oper...\"],\"EUvulZ\":[\"Trovato 1 messaggio corrispondente a \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Immagine successiva\"],\"EdQY6l\":[\"Nessuno\"],\"EnqLYU\":[\"Cerca server...\"],\"F0OKMc\":[\"Modifica server\"],\"F6Int2\":[\"Abilita evidenziazioni\"],\"FDoLyE\":[\"Utenti max.\"],\"FUU/hZ\":[\"Controlla quanti media esterni vengono caricati nella chat.\"],\"Fdp03t\":[\"attivo\"],\"FfPWR0\":[\"Finestra\"],\"FjkaiT\":[\"Riduci\"],\"FlqOE9\":[\"Cosa significa:\"],\"FolHNl\":[\"Gestisci il tuo account e l'autenticazione\"],\"Fp2Dif\":[\"Uscito dal server\"],\"G5KmCc\":[\"GZ-Line (Z-Line globale)\"],\"GDs0lz\":[\"<0>Rischio:0> Informazioni sensibili (messaggi, conversazioni private, dati di autenticazione) potrebbero essere esposte ad amministratori di rete o attaccanti posizionati tra i server IRC.\"],\"GR+2I3\":[\"Aggiungi maschera di invito (es. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Chiudi avvisi server in finestra separata\"],\"GdhD7H\":[\"Clicca di nuovo per confermare\"],\"GjRZex\":[\"Disconnettere la rete?\"],\"GlHnXw\":[\"Cambio nick fallito: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Anteprima:\"],\"GtmO8/\":[\"da\"],\"GtuHUQ\":[\"Rinomina questo canale sul server. Tutti gli utenti vedranno il nuovo nome.\"],\"GuGfFX\":[\"Attiva/Disattiva ricerca\"],\"GxkJXS\":[\"Caricamento...\"],\"GzbwnK\":[\"È entrato nel canale\"],\"GzsUDB\":[\"Profilo esteso\"],\"H/PnT8\":[\"Inserisci emoji\"],\"H6Izzl\":[\"Il tuo codice colore preferito\"],\"H9jIv+\":[\"Mostra entrate/uscite\"],\"HAKBY9\":[\"Carica file\"],\"HdE1If\":[\"Canale\"],\"Hk4AW9\":[\"Il tuo nome visualizzato preferito\"],\"HmHDk7\":[\"Seleziona membro\"],\"HrQzPU\":[\"Canali su \",[\"networkName\"]],\"I2tXQ5\":[\"Messaggio a @\",[\"0\"],\" (Invio per nuova riga, Shift+Invio per inviare)\"],\"I6bw/h\":[\"Banna utente\"],\"I92Z+b\":[\"Abilita notifiche\"],\"I9D72S\":[\"Sei sicuro di voler eliminare questo messaggio? Questa azione non può essere annullata.\"],\"IA+1wo\":[\"Mostra quando gli utenti vengono espulsi dai canali\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Salva modifiche\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" e \",[\"2\"],\" stanno scrivendo...\"],\"IgrLD/\":[\"Pausa\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Rispondi\"],\"IoHMnl\":[\"Il valore massimo è \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Connessione...\"],\"J5T9NW\":[\"Informazioni utente\"],\"J8Y5+z\":[\"Ops! La rete si è divisa! ⚠️\"],\"JBHkBA\":[\"Ha lasciato il canale\"],\"JCwL0Q\":[\"Inserisci motivo (opzionale)\"],\"JFciKP\":[\"Attiva/Disattiva\"],\"JXGkhG\":[\"Cambia il nome del canale (solo operatori)\"],\"JcD7qf\":[\"Altre azioni\"],\"JdkA+c\":[\"Segreto (+s)\"],\"Jmu12l\":[\"Canali del server\"],\"JoQY+E\":[\"Questo rimuove la rete dal tuo bouncer soju. Per usarla di nuovo, dovrai aggiungerla di nuovo.\"],\"JvQ++s\":[\"Abilita Markdown\"],\"K2jwh/\":[\"Nessun dato WHOIS disponibile\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Elimina messaggio\"],\"KKBlUU\":[\"Incorpora\"],\"KM0pLb\":[\"Benvenuto nel canale!\"],\"KR6W2h\":[\"Smetti di ignorare utente\"],\"KV+Bi1\":[\"Solo su invito (+i)\"],\"KdCtwE\":[\"Quanti secondi monitorare l'attività flood prima di reimpostare i contatori\"],\"Kkezga\":[\"Password del server\"],\"KsiQ/8\":[\"Gli utenti devono essere invitati per entrare\"],\"L+gB/D\":[\"Informazioni sul canale\"],\"LC1a7n\":[\"Il server IRC ha segnalato che i suoi collegamenti tra server hanno un basso livello di sicurezza. Ciò significa che quando i tuoi messaggi vengono instradati tra i server IRC nella rete, potrebbero non essere correttamente cifrati o i certificati SSL/TLS potrebbero non essere validati correttamente.\"],\"LEwpeL\":[\"bouncer soju (controllo)\"],\"LNfLR5\":[\"Mostra espulsioni\"],\"LP+1Z7\":[\"Aggiungi rete\"],\"LQb0W/\":[\"Mostra tutti gli eventi\"],\"LU7/yA\":[\"Nome alternativo per la visualizzazione. Può contenere spazi, emoji e caratteri speciali. Il nome reale (\",[\"channelName\"],\") verrà comunque usato per i comandi IRC.\"],\"LUb9O7\":[\"È richiesta una porta del server valida\"],\"LV4fT6\":[\"Descrizione (opzionale, es. \\\"Beta tester Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Informativa sulla privacy\"],\"LcuSDR\":[\"Gestisci le informazioni del tuo profilo e i metadati\"],\"LqLS9B\":[\"Mostra cambi di soprannome\"],\"LsDQt2\":[\"Impostazioni canale\"],\"LtI9AS\":[\"Proprietario\"],\"LuNhhL\":[\"ha reagito a questo messaggio\"],\"M/AZNG\":[\"URL dell'immagine del tuo avatar\"],\"M/WIer\":[\"Invia messaggio\"],\"M8er/5\":[\"Nome:\"],\"MHk+7g\":[\"Immagine precedente\"],\"MRorGe\":[\"Messaggio privato\"],\"MVbSGP\":[\"Finestra temporale (secondi)\"],\"MkpcsT\":[\"I tuoi messaggi e impostazioni sono archiviati localmente sul tuo dispositivo\"],\"MzPdC2\":[\"Password del server (PASS)\"],\"N/hDSy\":[\"Segna come bot, di solito 'on' o vuoto\"],\"N6j2JH\":[\"Modifica \",[\"0\"]],\"N7TQbE\":[\"Invita utente in \",[\"channelName\"]],\"NCca/o\":[\"Inserisci nickname predefinito...\"],\"Nqs6B9\":[\"Mostra tutti i media esterni. Qualsiasi URL potrebbe causare una richiesta a un server sconosciuto.\"],\"Nt+9O7\":[\"Usa WebSocket invece di TCP grezzo\"],\"NxIHzc\":[\"Espelli utente\"],\"O+v/cL\":[\"Sfoglia tutti i canali del server\"],\"OCGpR4\":[\"(eredita)\"],\"ODwSCk\":[\"Invia un GIF\"],\"OGQ5kK\":[\"Configura suoni di notifica ed evidenziazioni\"],\"OIPt1Z\":[\"Mostra o nascondi la barra laterale dei membri\"],\"OKSNq/\":[\"Molto rigido\"],\"ONWvwQ\":[\"Carica\"],\"OVKoQO\":[\"La tua password account per l'autenticazione\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Portare IRC nel futuro\"],\"OhCpra\":[\"Imposta un topic…\"],\"OkltoQ\":[\"Banna \",[\"username\"],\" per nickname (impedisce di rientrare con lo stesso nick)\"],\"P+t/Te\":[\"Nessun dato aggiuntivo\"],\"P42Wcc\":[\"Sicuro\"],\"PD38l0\":[\"Anteprima avatar canale\"],\"PD9mEt\":[\"Scrivi un messaggio...\"],\"PPqfdA\":[\"Apri impostazioni configurazione canale\"],\"PSCjfZ\":[\"L'argomento visualizzato per questo canale. Tutti gli utenti possono vederlo.\"],\"PZCecv\":[\"Anteprima PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 volta\"],\"other\":[[\"c\"],\" volte\"]}]],\"PguS2C\":[\"Aggiungi maschera di eccezione (es. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Visualizzazione di \",[\"displayedChannelsCount\"],\" su \",[\"0\"],\" canali\"],\"PqhVlJ\":[\"Banna utente (per hostmask)\"],\"Q+chwU\":[\"Nome utente:\"],\"Q2QY4/\":[\"Elimina questo invito\"],\"Q3v9Wc\":[\"Sì, elimina\"],\"Q6hhn8\":[\"Preferenze\"],\"QF4a34\":[\"Inserisci un nome utente\"],\"QGqSZ2\":[\"Colore e formattazione\"],\"QJQd1J\":[\"Modifica profilo\"],\"QSzGDE\":[\"Inattivo\"],\"QUlny5\":[\"Benvenuto su \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Leggi di più\"],\"QuSkCF\":[\"Filtra canali...\"],\"QwUrDZ\":[\"ha cambiato il topic in: \",[\"topic\"]],\"R0UH07\":[\"Immagine \",[\"0\"],\" di \",[\"1\"]],\"R7SsBE\":[\"Disattiva audio\"],\"R8rf1X\":[\"Clicca per impostare il topic\"],\"RArB3D\":[\"è stato espulso da \",[\"channelName\"],\" da \",[\"username\"]],\"RI3cWd\":[\"Scopri il mondo di IRC con ObsidianIRC\"],\"RIfHS5\":[\"Crea un nuovo link di invito\"],\"RMMaN5\":[\"Moderato (+m)\"],\"RWw9Lg\":[\"Chiudi finestra\"],\"RZ2BuZ\":[\"La registrazione dell'account \",[\"account\"],\" richiede verifica: \",[\"message\"]],\"RySp6q\":[\"Nascondi commenti\"],\"S5Togi\":[\"Caricamento delle reti dal tuo bouncer…\"],\"SPKQTd\":[\"Il nickname è obbligatorio\"],\"SPVjfj\":[\"Il valore predefinito sarà 'nessun motivo' se lasciato vuoto\"],\"SQKPvQ\":[\"Invita utente\"],\"STmlpb\":[\"Torna all'elenco delle reti\"],\"SkZcl+\":[\"Scegli un profilo di protezione flood predefinito. Questi profili forniscono impostazioni di protezione bilanciate per diversi casi d'uso.\"],\"Slr+3C\":[\"Utenti min.\"],\"Spnlre\":[\"Hai invitato \",[\"target\"],\" a unirsi a \",[\"channel\"]],\"T/ckN5\":[\"Apri nel visualizzatore\"],\"T91vKp\":[\"Riproduci\"],\"TV2Wdu\":[\"Scopri come gestiamo i tuoi dati e proteggiamo la tua privacy.\"],\"TgFpwD\":[\"Applicazione...\"],\"TkzSFB\":[\"Nessuna modifica\"],\"TtserG\":[\"Inserisci nome reale\"],\"Ttz9J1\":[\"Inserisci password...\"],\"Tz0i8g\":[\"Impostazioni\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reagisci\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Non hai ancora creato alcun link di invito. Usa il modulo qui sopra per crearne uno.\"],\"UGT5vp\":[\"Salva impostazioni\"],\"UV5hLB\":[\"Nessun ban trovato\"],\"Uaj3Nd\":[\"Messaggi di stato\"],\"Ue3uny\":[\"Predefinito (nessun profilo)\"],\"UkARhe\":[\"Normale – Protezione standard\"],\"Umn7Cj\":[\"Ancora nessun commento. Sii il primo!\"],\"UtUIRh\":[[\"0\"],\" messaggi precedenti\"],\"UwzP+U\":[\"Connessione sicura\"],\"V0/A4O\":[\"Proprietario del canale\"],\"V0zZWc\":[\"Verrà chiusa anche la rete collegata qui sotto.\"],\"V4qgxE\":[\"Creato prima (min fa)\"],\"V8yTm6\":[\"Cancella ricerca\"],\"VJMMyz\":[\"ObsidianIRC - Portare IRC nel futuro\"],\"VJScHU\":[\"Motivo\"],\"VLsmVV\":[\"Silenzia notifiche\"],\"VbyRUy\":[\"Commenti\"],\"Vmx0mQ\":[\"Impostato da:\"],\"VqnIZz\":[\"Visualizza la nostra informativa sulla privacy e le pratiche sui dati\"],\"VrMygG\":[\"La lunghezza minima è \",[\"0\"]],\"VrnTui\":[\"I tuoi pronomi, mostrati nel profilo\"],\"W8E3qn\":[\"Account autenticato\"],\"WAakm9\":[\"Elimina canale\"],\"WFxTHC\":[\"Aggiungi maschera di ban (es. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"L'host del server è obbligatorio\"],\"WRYdXW\":[\"Posizione audio\"],\"WUOH5B\":[\"Ignora utente\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Mostra 1 altro elemento\"],\"other\":[\"Mostra \",[\"1\"],\" altri elementi\"]}]],\"WYxRzo\":[\"Crea e gestisci i tuoi link di invito\"],\"Wd38W1\":[\"Lascia il canale vuoto per un invito generico alla rete. La descrizione è solo per i tuoi appunti â visibile solo a te in questo elenco.\"],\"Weq9zb\":[\"Generale\"],\"Wfj7Sk\":[\"Attiva o disattiva i suoni delle notifiche\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Profilo utente\"],\"X6S3lt\":[\"Cerca impostazioni, canali, server...\"],\"XEHan5\":[\"Continua comunque\"],\"XI1+wb\":[\"Formato non valido\"],\"XIXeuC\":[\"Messaggio a @\",[\"0\"]],\"XMS+k4\":[\"Avvia messaggio privato\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Rimuovi fissaggio chat privata\"],\"Xm/s+u\":[\"Visualizzazione\"],\"Xp2n93\":[\"Mostra media dall'host di file attendibile del tuo server. Nessuna richiesta viene inviata a servizi esterni.\"],\"XvjC4F\":[\"Salvataggio...\"],\"Y/qryO\":[\"Nessun utente trovato corrispondente alla ricerca\"],\"YAqRpI\":[\"Registrazione dell'account \",[\"account\"],\" riuscita: \",[\"message\"]],\"YEfzvP\":[\"Argomento protetto (+t)\"],\"YQOn6a\":[\"Comprimi lista membri\"],\"YRCoE9\":[\"Operatore del canale\"],\"YURQaF\":[\"Vedi profilo\"],\"YdBSvr\":[\"Controlla la visualizzazione dei media e dei contenuti esterni\"],\"Yj6U3V\":[\"Nessun server centrale:\"],\"YjvpGx\":[\"Pronomi\"],\"YqH4l4\":[\"Nessuna chiave\"],\"YyUPpV\":[\"Account:\"],\"ZJSWfw\":[\"Messaggio mostrato alla disconnessione dal server\"],\"ZR1dJ4\":[\"Inviti\"],\"ZdWg0V\":[\"Apri nel browser\"],\"ZhRBbl\":[\"Cerca messaggi…\"],\"Zmcu3y\":[\"Filtri avanzati\"],\"a0bHay\":[\"Disconnettere <0>\",[\"0\"],\"0>?\"],\"a2/8e5\":[\"Argomento impostato dopo (min fa)\"],\"aHKcKc\":[\"Pagina precedente\"],\"aJTbXX\":[\"Password oper\"],\"aQryQv\":[\"Il pattern esiste già\"],\"aW9pLN\":[\"Numero massimo di utenti nel canale. Lascia vuoto per nessun limite.\"],\"ah4fmZ\":[\"Mostra anche anteprime da YouTube, Vimeo, SoundCloud e servizi noti simili.\"],\"aifXak\":[\"Nessun media in questo canale\"],\"ap2zBz\":[\"Rilassato\"],\"az8lvo\":[\"Disattivato\"],\"azXSNo\":[\"Espandi lista membri\"],\"azdliB\":[\"Accedi a un account\"],\"b26wlF\":[\"lei/la\"],\"bD/+Ei\":[\"Rigido\"],\"bQ6BJn\":[\"Configura regole dettagliate di protezione flood. Ogni regola specifica il tipo di attività da monitorare e l'azione da intraprendere quando le soglie vengono superate.\"],\"beV7+y\":[\"L'utente riceverà un invito per unirsi a \",[\"channelName\"],\".\"],\"bk84cH\":[\"Messaggio di assenza\"],\"bkHdLj\":[\"Aggiungi server IRC\"],\"bmQLn5\":[\"Aggiungi regola\"],\"bv4cFj\":[\"Trasporto\"],\"bwRvnp\":[\"Azione\"],\"c8+EVZ\":[\"Account verificato\"],\"cGYUlD\":[\"Nessuna anteprima media caricata.\"],\"cLF98o\":[\"Mostra commenti (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Nessun utente disponibile\"],\"cSgpoS\":[\"Fissa chat privata\"],\"cXeEKu\":[\"Verranno chiuse anche le \",[\"0\"],\" reti collegate qui sotto.\"],\"cde3ce\":[\"Messaggio a <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Copia output formattato\"],\"cl/A5J\":[\"Benvenuto su \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Elimina\"],\"coPLXT\":[\"Non archiviamo le tue comunicazioni IRC sui nostri server\"],\"crYH/6\":[\"Player SoundCloud\"],\"cv5DQb\":[\"nessun host impostato\"],\"d3sis4\":[\"Aggiungi server\"],\"d9aN5k\":[\"Rimuovi \",[\"username\"],\" dal canale\"],\"dEgA5A\":[\"Annulla\"],\"dGi1We\":[\"Rimuovi il fissaggio di questa conversazione privata\"],\"dJVuyC\":[\"ha lasciato \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"a\"],\"dXqxlh\":[\"<0>⚠️ Rischio di sicurezza!0> Questa connessione potrebbe essere vulnerabile a intercettazioni o attacchi man-in-the-middle.\"],\"da9Q/R\":[\"Modalità canale modificate\"],\"dhJN3N\":[\"Mostra commenti\"],\"dj2xTE\":[\"Ignora notifica\"],\"dpCzmC\":[\"Impostazioni protezione flood\"],\"e9dQpT\":[\"Vuoi aprire questo link in una nuova scheda?\"],\"ePK91l\":[\"Modifica\"],\"eYBDuB\":[\"Carica un'immagine o fornisci un URL con sostituzione opzionale \",[\"size\"]],\"edBbee\":[\"Banna \",[\"username\"],\" per hostmask (impedisce di rientrare dallo stesso IP/host)\"],\"ekfzWq\":[\"Impostazioni utente\"],\"elPDWs\":[\"Personalizza la tua esperienza con il client IRC\"],\"eu2osY\":[\"<0>💡 Raccomandazione:0> Procedi solo se ti fidi di questo server e comprendi i rischi. Evita di condividere informazioni sensibili o password su questa connessione.\"],\"euEhbr\":[\"Clicca per unirti a \",[\"channel\"]],\"ez3vLd\":[\"Abilita input multiriga\"],\"f0J5Ki\":[\"Le comunicazioni tra server potrebbero usare connessioni non cifrate\"],\"f9BHJk\":[\"Avvisa utente\"],\"fDOLLd\":[\"Nessun canale trovato.\"],\"ffzDkB\":[\"Analisi anonime:\"],\"fq1GF9\":[\"Mostra quando gli utenti si disconnettono dal server\"],\"gCldcN\":[\"Cambia colore d'accento\"],\"gEF57C\":[\"Questo server supporta solo un tipo di connessione\"],\"gJuLUI\":[\"Lista di ignorati\"],\"gNzMrk\":[\"Avatar attuale\"],\"gjPWyO\":[\"Inserisci nickname...\"],\"gz6UQ3\":[\"Massimizza\"],\"h6/IMX\":[\"Aggiungi la tua prima rete\"],\"h6razj\":[\"Escludi maschera nome canale\"],\"hG6jnw\":[\"Nessun topic impostato\"],\"hG89Ed\":[\"Immagine\"],\"hYgDIe\":[\"Crea\"],\"hZ6znB\":[\"Porta\"],\"ha+Bz5\":[\"es., 100:1440\"],\"he3ygx\":[\"Copia\"],\"hehnjM\":[\"Quantità\"],\"hzdLuQ\":[\"Solo gli utenti con voice o superiore possono parlare\"],\"i0qMbr\":[\"Home\"],\"iDNBZe\":[\"Notifiche\"],\"iH8pgl\":[\"Indietro\"],\"iL9SZg\":[\"Banna utente (per nickname)\"],\"iNt+3c\":[\"Torna all'immagine\"],\"iQvi+a\":[\"Non avvisarmi sulla bassa sicurezza del link per questo server\"],\"iSLIjg\":[\"Connetti\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Host del server\"],\"idD8Ev\":[\"Salvato\"],\"iivqkW\":[\"Connesso dal\"],\"ij+Elv\":[\"Anteprima immagine\"],\"ilIWp7\":[\"Attiva/Disattiva notifiche\"],\"iuaqvB\":[\"Usa * come wildcard. Esempi: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Banna per maschera host\"],\"jA4uoI\":[\"Argomento:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Motivo (opzionale)\"],\"jUV7CU\":[\"Carica avatar\"],\"jW5Uwh\":[\"Controlla quanti media esterni vengono caricati. Disattivato / Sicuro / Fonti affidabili / Tutto il contenuto.\"],\"jXzms5\":[\"Opzioni allegato\"],\"jZlrte\":[\"Colore\"],\"jfC/xh\":[\"Contatti\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Carica messaggi precedenti\"],\"k3ID0F\":[\"Filtra membri…\"],\"k65gsE\":[\"Analisi approfondita\"],\"k7Zgob\":[\"Annulla connessione\"],\"kAVx5h\":[\"Nessun invito trovato\"],\"kCLEPU\":[\"Connesso a\"],\"kF5LKb\":[\"Pattern ignorati:\"],\"kGeOx/\":[\"Unisciti a \",[\"0\"]],\"kITKr8\":[\"Caricamento modalità canale...\"],\"kPpPsw\":[\"Sei un IRC Operator\"],\"kWJmRL\":[\"Tu\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Copia JSON\"],\"krViRy\":[\"Clicca per copiare come JSON\"],\"ks71ra\":[\"Eccezioni\"],\"kw4lRv\":[\"Semi-operatore del canale\"],\"kxgIRq\":[\"Seleziona o aggiungi un canale per iniziare.\"],\"ky6dWe\":[\"Anteprima avatar\"],\"l+GxCv\":[\"Caricamento canali...\"],\"l+IUVW\":[\"Verifica dell'account \",[\"account\"],\" riuscita: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"si è riconnesso\"],\"other\":[\"si è riconnesso \",[\"reconnectCount\"],\" volte\"]}]],\"l1l8sj\":[[\"0\"],\"g fa\"],\"l5NhnV\":[\"#canale (opzionale)\"],\"l5jmzx\":[[\"0\"],\" e \",[\"1\"],\" stanno scrivendo...\"],\"lCF0wC\":[\"Aggiorna\"],\"lHy8N5\":[\"Caricamento altri canali...\"],\"lasgrr\":[\"usato\"],\"lbpf14\":[\"Entra in \",[\"value\"]],\"lfFsZ4\":[\"Canali\"],\"lkNdiH\":[\"Nome account\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Carica immagine\"],\"loQxaJ\":[\"Sono tornato\"],\"lvfaxv\":[\"HOME\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Aggiungi\"],\"m8flAk\":[\"Anteprima (non ancora caricata)\"],\"mEPxTp\":[\"<0>⚠️ Attenzione!0> Apri solo link da fonti attendibili. I link malevoli possono compromettere la tua sicurezza o privacy.\"],\"mHGdhG\":[\"Informazioni sul server\"],\"mHS8lb\":[\"Messaggio in #\",[\"0\"]],\"mMYBD9\":[\"Ampio – Portata di protezione estesa\"],\"mTGsPd\":[\"Argomento del canale\"],\"mU8j6O\":[\"Nessun messaggio esterno (+n)\"],\"mZp8FL\":[\"Ritorno automatico alla singola riga\"],\"mdQu8G\":[\"IlTuoNickname\"],\"miSSBQ\":[\"Commenti (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Utente autenticato\"],\"mwtcGl\":[\"Chiudi commenti\"],\"myL0MR\":[\"Eliminare questa rete?\"],\"mzI/c+\":[\"Scarica\"],\"n3fGRk\":[\"impostato da \",[\"0\"]],\"n5+j9l\":[\"es. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Rilassato – Protezione meno aggressiva\"],\"nNflMD\":[\"Abbandona canale\"],\"nPXkBi\":[\"Caricamento dati WHOIS...\"],\"nQnxxF\":[\"Messaggio in #\",[\"0\"],\" (Shift+Invio per nuova riga)\"],\"nWMRxa\":[\"Rimuovi fissaggio\"],\"nkC032\":[\"Nessun profilo flood\"],\"o69z4d\":[\"Invia un messaggio di avviso a \",[\"username\"]],\"o9ylQi\":[\"Cerca GIF per iniziare\"],\"oFGkER\":[\"Avvisi del server\"],\"oOi11l\":[\"Vai in fondo\"],\"oPYIL5\":[\"rete\"],\"oQEzQR\":[\"Nuovo messaggio privato\"],\"oXOSPE\":[\"In linea\"],\"oal760\":[\"Sono possibili attacchi man-in-the-middle sui link del server\"],\"oeqmmJ\":[\"Fonti attendibili\"],\"optX0N\":[[\"0\"],\"h fa\"],\"ovBPCi\":[\"Predefinito\"],\"p0Z69r\":[\"Il pattern non può essere vuoto\"],\"p1KgtK\":[\"Caricamento audio non riuscito\"],\"p59pEv\":[\"Dettagli aggiuntivi\"],\"p7sRI6\":[\"Avvisa gli altri quando stai scrivendo\"],\"pBm1od\":[\"Canale segreto\"],\"pNmiXx\":[\"Il tuo soprannome predefinito per tutti i server\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Password account\"],\"peNE68\":[\"Permanente\"],\"plhHQt\":[\"Nessun dato\"],\"pm6+q5\":[\"Avviso di sicurezza\"],\"pn5qSs\":[\"Informazioni aggiuntive\"],\"q0cR4S\":[\"ora è conosciuto come **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Il canale non apparirà nei comandi LIST o NAMES\"],\"qLpTm/\":[\"Rimuovi reazione \",[\"emoji\"]],\"qVkGWK\":[\"Fissa\"],\"qY8wNa\":[\"Homepage\"],\"qb0xJ7\":[\"Wildcard: * corrisponde a qualsiasi sequenza, ? a un singolo carattere. Esempi: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Chiave canale (+k)\"],\"qtoOYG\":[\"Nessun limite\"],\"r1W2AS\":[\"Immagine dal filehost\"],\"rIPR2O\":[\"Argomento impostato prima (min fa)\"],\"rMMSYo\":[\"La lunghezza massima è \",[\"0\"]],\"rWtzQe\":[\"La rete si è divisa e riconnessa. ✅\"],\"rYG2u6\":[\"Attendere...\"],\"rdUucN\":[\"Anteprima\"],\"rjGI/Q\":[\"Privacy\"],\"rk8iDX\":[\"Caricamento GIF...\"],\"rn6SBY\":[\"Attiva audio\"],\"s/UKqq\":[\"È stato espulso dal canale\"],\"s7oqXR\":[\"Seleziona una rete\"],\"s8cATI\":[\"si è unito a \",[\"channelName\"]],\"sCO9ue\":[\"La connessione a <0>\",[\"serverName\"],\"0> presenta le seguenti problematiche di sicurezza:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"ora è conosciuto come **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" ti ha invitato a unirti a \",[\"channel\"]],\"sUBSbK\":[\"Nessuna rete upstream ancora.\"],\"sby+1/\":[\"Clicca per copiare\"],\"sfN25C\":[\"Il tuo nome reale o completo\"],\"sliuzR\":[\"Apri link\"],\"sqrO9R\":[\"Menzioni personalizzate\"],\"sr6RdJ\":[\"Multiriga con Shift+Invio\"],\"swrCpB\":[\"Il canale è stato rinominato da \",[\"oldName\"],\" a \",[\"newName\"],\" da \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Avanzato\"],\"t/YqKh\":[\"Rimuovi\"],\"t47eHD\":[\"Il tuo identificatore unico su questo server\"],\"tAkAh0\":[\"URL con sostituzione opzionale \",[\"size\"],\". Esempio: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Mostra o nascondi la barra laterale dei canali\"],\"tfDRzk\":[\"Salva\"],\"tiBsJk\":[\"ha lasciato \",[\"channelName\"]],\"tt4/UD\":[\"ha abbandonato (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Il nick {nick} è già in uso, nuovo tentativo con {newNick}\"],\"u0a8B4\":[\"Autenticarsi come operatore IRC per l'accesso amministrativo\"],\"u0rWFU\":[\"Creato dopo (min fa)\"],\"u72w3t\":[\"Utenti e modelli da ignorare\"],\"u7jc2L\":[\"ha abbandonato\"],\"uAQUqI\":[\"Stato\"],\"uB85T3\":[\"Salvataggio fallito: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"Server IRC:\"],\"ukyW4o\":[\"I tuoi link di invito\"],\"usSSr/\":[\"Livello zoom\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Shift+Invio per le nuove righe (Invio invia)\"],\"vERlcd\":[\"Profilo\"],\"vK0RL8\":[\"Nessun argomento\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Lingua\"],\"vaHYxN\":[\"Nome reale\"],\"vhjbKr\":[\"Assente\"],\"w/nogd\":[[\"0\"],\" rete\",[\"1\"],\" — scegline una a cui unirti\"],\"w4NYox\":[\"client \",[\"title\"]],\"w8xQRx\":[\"Valore non valido\"],\"wFjjxZ\":[\"è stato espulso da \",[\"channelName\"],\" da \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nessuna eccezione di ban trovata\"],\"wPrGnM\":[\"Amministratore del canale\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Mostra quando gli utenti entrano o escono dai canali\"],\"whqZ9r\":[\"Parole o frasi aggiuntive da evidenziare\"],\"wm7RV4\":[\"Suono di notifica\"],\"wz/Yoq\":[\"I tuoi messaggi potrebbero essere intercettati durante l'instradamento tra server\"],\"x3+y8b\":[\"Numero di persone registrate tramite questo link\"],\"xCJdfg\":[\"Cancella\"],\"xOTzt5\":[\"proprio ora\"],\"xUHRTR\":[\"Autentica automaticamente come operatore alla connessione\"],\"xWHwwQ\":[\"Ban\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"Questo server non supporta i link di invito (la capability<0>obby.world/invitation0>non è annunciata). Puoi comunque chattare normalmente; questo pannello è per le reti basate su obbyircd.\"],\"xceQrO\":[\"Sono supportati solo websocket sicuri\"],\"xdtXa+\":[\"nome-canale\"],\"xfXC7q\":[\"Canali testuali\"],\"xlCYOE\":[\"Caricamento messaggi...\"],\"xlhswE\":[\"Il valore minimo è \",[\"0\"]],\"xq97Ci\":[\"Aggiungi una parola o frase...\"],\"xuRqRq\":[\"Limite client (+l)\"],\"xwF+7J\":[[\"0\"],\" sta scrivendo...\"],\"y1eoq1\":[\"Copia link\"],\"yJztBY\":[\"Elimina rete\"],\"yNeucF\":[\"Questo server non supporta i metadati del profilo esteso (estensione IRCv3 METADATA). Campi come avatar, nome visualizzato e stato non sono disponibili.\"],\"yPlrca\":[\"Avatar del canale\"],\"yQE2r9\":[\"Caricamento\"],\"ySU+JY\":[\"tuo@email.com\"],\"yTX1Rt\":[\"Nome utente operatore\"],\"yYOzWD\":[\"log\"],\"yfx9Re\":[\"Password operatore IRC\"],\"ygCKqB\":[\"Stop\"],\"ymDxJx\":[\"Nome utente operatore IRC\"],\"yrpRsQ\":[\"Ordina per nome\"],\"yz7wBu\":[\"Chiudi\"],\"zJw+jA\":[\"imposta modalità: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"m fa\"],\"zebeLu\":[\"Inserisci nome utente oper\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/it/messages.po b/src/locales/it/messages.po
index 129ddcc7..e02b9b00 100644
--- a/src/locales/it/messages.po
+++ b/src/locales/it/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - Portare IRC nel futuro"
msgid "— open in viewer"
msgstr "— apri nel visualizzatore"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(eredita)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(invariato)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} e {1} stanno scrivendo..."
msgid "{0} is typing..."
msgstr "{0} sta scrivendo..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} rete{1} — scegline una a cui unirti"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -205,6 +221,12 @@ msgstr "Aggiungi maschera di invito (es. nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "Aggiungi server IRC"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Aggiungi rete"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Aggiungi regola"
msgid "Add Server"
msgstr "Aggiungi server"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Aggiungi la tua prima rete"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Dettagli aggiuntivi"
@@ -377,6 +403,10 @@ msgstr "Indietro"
msgid "Back to image"
msgstr "Torna all'immagine"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Torna all'elenco delle reti"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "Banna {username} per hostmask (impedisce di rientrare dallo stesso IP/host)"
@@ -424,6 +454,10 @@ msgstr "Sfoglia tutti i canali del server"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Annulla connessione"
msgid "Cancel reply"
msgstr "Annulla risposta"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Cambia colore d'accento"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Cambia il nome del canale (solo operatori)"
@@ -606,6 +645,8 @@ msgstr "Limite client (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -669,6 +710,7 @@ msgid "Confirm?"
msgstr "Confermare?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Connetti"
@@ -814,6 +856,10 @@ msgstr "Elimina canale"
msgid "Delete message"
msgstr "Elimina messaggio"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Elimina rete"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Elimina chat privata"
@@ -826,15 +872,36 @@ msgstr "Elimina questo invito"
msgid "Delete this message? This cannot be undone."
msgstr "Eliminare questo messaggio? Questa azione non può essere annullata."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Eliminare questa rete?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
msgstr "Descrizione (opzionale, es. \"Beta tester Q3\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Disconnetti"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "Disconnettere <0>{0}0>?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Disconnettersi dal bouncer soju?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Disconnettere la rete?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Scopri"
@@ -893,18 +960,29 @@ msgstr "Scarica"
msgid "Drop files to upload"
msgstr "Rilascia i file per caricarli"
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "es. <0>wss://host:port/socket0>"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "es., 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Modifica"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "Modifica {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Modifica profilo"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "HOME"
msgid "Homepage"
msgstr "Homepage"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Host"
@@ -1323,7 +1402,7 @@ msgstr "Abbandona canale"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Lascia il canale vuoto per un invito generico alla rete. La descrizione è solo per i tuoi appunti — visibile solo a te in questo elenco."
+msgstr "Lascia il canale vuoto per un invito generico alla rete. La descrizione è solo per i tuoi appunti â visibile solo a te in questo elenco."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "Ha lasciato il canale"
msgid "Let others know when you are typing"
msgstr "Avvisa gli altri quando stai scrivendo"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Anteprima link"
@@ -1375,6 +1458,10 @@ msgstr "Caricamento GIF..."
msgid "Loading more channels..."
msgstr "Caricamento altri canali..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Caricamento delle reti dal tuo bouncer…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "Caricamento dati WHOIS..."
@@ -1565,10 +1652,21 @@ msgstr "Nome:"
msgid "network"
msgstr "rete"
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Rete collegata tramite bouncer soju"
+
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Nome rete"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Reti su {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Nuovo messaggio privato"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (es., spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Nessun file scelto"
msgid "No flood profile"
msgstr "Nessun profilo flood"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "nessun host impostato"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Nessun invito trovato"
@@ -1698,6 +1801,10 @@ msgstr "Nessun topic impostato"
msgid "No unread mentions or messages"
msgstr "Nessuna menzione o messaggio non letto"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Nessuna rete upstream ancora."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Nessun utente disponibile"
@@ -1784,6 +1891,10 @@ msgstr "Ops! La rete si è divisa! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Apri"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Apri impostazioni configurazione canale"
@@ -1887,6 +1998,10 @@ msgstr "Fissa chat privata"
msgid "Pin this private message conversation"
msgstr "Fissa questa conversazione privata"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Testo in chiaro"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "Messaggio privato"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Porta"
@@ -2006,6 +2122,7 @@ msgstr "ha reagito a questo messaggio"
msgid "Read more"
msgstr "Leggi di più"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Motivo"
msgid "Reason (optional)"
msgstr "Motivo (opzionale)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Riconnetti al server"
@@ -2095,6 +2213,7 @@ msgstr "Regole"
msgid "Safe"
msgstr "Sicuro"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Cerca posizione"
msgid "Select a channel"
msgstr "Seleziona un canale"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Seleziona una rete"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Seleziona membro"
@@ -2276,6 +2399,10 @@ msgstr "Gli operatori del server sulla rete potrebbero leggere i tuoi messaggi"
msgid "Server Password"
msgstr "Password del server"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Password del server (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Le comunicazioni tra server potrebbero usare connessioni non cifrate"
@@ -2380,6 +2507,12 @@ msgstr "Connesso dal"
msgid "Software:"
msgstr "Software:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "bouncer soju (controllo)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Ordina per nome"
@@ -2459,18 +2592,31 @@ msgstr "Questa immagine è scaduta"
msgid "This many people registered through this link"
msgstr "Numero di persone registrate tramite questo link"
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Questo rimuove la rete dal tuo bouncer soju. Per usarla di nuovo, dovrai aggiungerla di nuovo."
+
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
msgstr "Questo server non supporta i metadati del profilo esteso (estensione IRCv3 METADATA). Campi come avatar, nome visualizzato e stato non sono disponibili."
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Questo server non supporta i link di invito (la capability<0>obby.world/invitation0>non è annunciata). Puoi comunque chattare normalmente; questo pannello è per le reti basate su obbyircd."
+msgstr "Questo server non supporta i link di invito (la capability<0>obby.world/invitation0>non è annunciata). Puoi comunque chattare normalmente; questo pannello è per le reti basate su obbyircd."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Questo server supporta solo un tipo di connessione"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Verranno chiuse anche le {0} reti collegate qui sotto."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Verrà chiusa anche la rete collegata qui sotto."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Tempo (min)"
@@ -2479,6 +2625,10 @@ msgstr "Tempo (min)"
msgid "Time Window (seconds)"
msgstr "Finestra temporale (secondi)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Argomento:"
msgid "Total: {0}"
msgstr "Totale: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Trasporto"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Fonti attendibili"
@@ -2641,6 +2795,7 @@ msgstr "Profilo utente"
msgid "User Settings"
msgstr "Impostazioni utente"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Ampio – Portata di protezione estesa"
msgid "Will default to 'no reason' if left empty"
msgstr "Il valore predefinito sarà 'nessun motivo' se lasciato vuoto"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Sì, elimina"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2814,6 +2973,11 @@ msgstr "Non hai ancora creato alcun link di invito. Usa il modulo qui sopra per
msgid "You invited {target} to join {channel}"
msgstr "Hai invitato {target} a unirsi a {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Sei connesso a <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "La tua password account per l'autenticazione"
@@ -2822,6 +2986,10 @@ msgstr "La tua password account per l'autenticazione"
msgid "Your account username for authentication"
msgstr "Il tuo nome utente account per l'autenticazione"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Il tuo bouncer non ha ancora nessuna rete. Aggiungine una per iniziare."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Il tuo soprannome predefinito per tutti i server"
diff --git a/src/locales/ja/messages.mjs b/src/locales/ja/messages.mjs
index 14058ddb..87a0767c 100644
--- a/src/locales/ja/messages.mjs
+++ b/src/locales/ja/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"無効なパターン形式です。nick!user@host 形式を使用してください(ワイルドカード * 使用可)\"],\"+6NQQA\":[\"一般サポートチャンネル\"],\"+6NyRG\":[\"クライアント\"],\"+K0AvT\":[\"切断\"],\"+cyFdH\":[\"離席時に表示するデフォルトメッセージ\"],\"+mVPqU\":[\"メッセージ内のMarkdown書式を表示する\"],\"+vqCJH\":[\"認証用のアカウントユーザー名\"],\"+yPBXI\":[\"ファイルを選択\"],\"+zy2Nq\":[\"種類\"],\"/09cao\":[\"リンクセキュリティが低い(レベル \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"チャンネル外のユーザーはメッセージを送信できません\"],\"/4C8U0\":[\"すべてコピー\"],\"/6BzZF\":[\"メンバーリストを切り替え\"],\"/AkXyp\":[\"確認しますか?\"],\"/TNOPk\":[\"ユーザーは離席中です\"],\"/XQgft\":[\"探す\"],\"/cF7Rs\":[\"音量\"],\"/dqduX\":[\"次のページ\"],\"/fc3q4\":[\"すべてのコンテンツ\"],\"/kISDh\":[\"通知音を有効にする\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"音声\"],\"/rfkZe\":[\"メンションやメッセージに対してサウンドを再生する\"],\"0/0ZGA\":[\"チャンネル名マスク\"],\"0D6j7U\":[\"カスタムルールについて詳しく →\"],\"0XsHcR\":[\"ユーザーをキック\"],\"0ZpE//\":[\"ユーザー数順で並び替え\"],\"0bEPwz\":[\"離席中に設定\"],\"0dGkPt\":[\"チャンネルリストを展開\"],\"0gS7M5\":[\"表示名\"],\"0kS+M8\":[\"サンプルNET\"],\"0rgoY7\":[\"自分で選んだサーバーにのみ接続します\"],\"0wdd7X\":[\"参加\"],\"0wkVYx\":[\"プライベートメッセージ\"],\"111uHX\":[\"リンクプレビュー\"],\"196EG4\":[\"プライベートチャットを削除\"],\"1DSr1i\":[\"アカウントを登録する\"],\"1O/24y\":[\"チャンネルリストを切り替え\"],\"1VPJJ2\":[\"外部リンクの警告\"],\"1ZC/dv\":[\"未読のメンションやメッセージはありません\"],\"1pO1zi\":[\"サーバー名は必須です\"],\"1uwfzQ\":[\"チャンネルトピックを表示\"],\"268g7c\":[\"表示名を入力\"],\"2F9+AZ\":[\"まだ生のIRCトラフィックを取得していません。接続するかメッセージを送信してください。\"],\"2FOFq1\":[\"ネットワーク上のサーバーオペレーターがメッセージを閲覧できる可能性があります\"],\"2FYpfJ\":[\"詳細\"],\"2HF1Y2\":[[\"inviter\"],\" が \",[\"target\"],\" を \",[\"channel\"],\" に招待しました\"],\"2I70QL\":[\"ユーザープロフィール情報を表示する\"],\"2QYdmE\":[\"ユーザー:\"],\"2QpEjG\":[\"退出しました\"],\"2YE223\":[\"#\",[\"0\"],\" へメッセージ(Enterで改行、Shift+Enterで送信)\"],\"2bimFY\":[\"サーバーパスワードを使用する\"],\"2iTmdZ\":[\"ローカルストレージ:\"],\"2odkwe\":[\"厳格 — より積極的な保護\"],\"2uDhbA\":[\"招待するユーザー名を入力\"],\"2ygf/L\":[\"← 戻る\"],\"2zEgxj\":[\"GIFを検索...\"],\"3RdPhl\":[\"チャンネル名を変更\"],\"3THokf\":[\"Voiceユーザー\"],\"3TSz9S\":[\"最小化\"],\"3jBDvM\":[\"チャンネル表示名\"],\"3ryuFU\":[\"アプリ改善のための任意のクラッシュレポート\"],\"3uBF/8\":[\"ビューアを閉じる\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"アカウント名を入力...\"],\"4/Rr0R\":[\"現在のチャンネルにユーザーを招待する\"],\"4EZrJN\":[\"ルール\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"フラッドプロファイル (+F)\"],\"4RZQRK\":[\"今何してるの?\"],\"4hfTrB\":[\"ニックネーム\"],\"4n99LO\":[\"すでに \",[\"0\"],\" にいます\"],\"4t6vMV\":[\"短いメッセージの場合は自動的に1行入力に切り替える\"],\"4vsHmf\":[\"時間(分)\"],\"5+INAX\":[\"自分へのメンションを含むメッセージをハイライトする\"],\"5R5Pv/\":[\"Oper名\"],\"678PKt\":[\"ネットワーク名\"],\"6Aih4U\":[\"オフライン\"],\"6CO3WE\":[\"チャンネルに参加するために必要なパスワード。キーを削除するには空欄にしてください。\"],\"6HhMs3\":[\"退出メッセージ\"],\"6V3Ea3\":[\"コピーしました\"],\"6lGV3K\":[\"折りたたむ\"],\"6yFOEi\":[\"oper パスワードを入力...\"],\"7+IHTZ\":[\"ファイルが選択されていません\"],\"73hrRi\":[\"nick!user@host(例:spam*!*@*、*!*@badhost.com)\"],\"7QkKyN\":[\"プライベートメッセージを送信\"],\"7U1W7c\":[\"とても緩め\"],\"7Y1YQj\":[\"本名:\"],\"7YHArF\":[\"— ビューアで開く\"],\"7fjnVl\":[\"ユーザーを検索...\"],\"7jL88x\":[\"このメッセージを削除しますか?この操作は元に戻せません。\"],\"7nGhhM\":[\"今どんな気分ですか?\"],\"7sEpu1\":[\"メンバー — \",[\"0\"]],\"7sNhEz\":[\"ユーザー名\"],\"8H0Q+x\":[\"プロファイルについて詳しく →\"],\"8Phu0A\":[\"ユーザーがニックネームを変更したときに表示する\"],\"8XTG9e\":[\"Operパスワードを入力\"],\"8XsV2J\":[\"再送信\"],\"8ZsakT\":[\"パスワード\"],\"8kR84m\":[\"外部リンクを開こうとしています:\"],\"8lCgih\":[\"ルールを削除\"],\"8o3dPc\":[\"ファイルをドロップしてアップロード\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"other\":[[\"joinCount\"],\"回 参加した\"]}]],\"9BMLnJ\":[\"サーバーに再接続\"],\"9OEgyT\":[\"リアクションを追加\"],\"9PQ8m2\":[\"G-Line(グローバルBAN)\"],\"9Qs99X\":[\"メール:\"],\"9QupBP\":[\"パターンを削除\"],\"9bG48P\":[\"送信中\"],\"9f5f0u\":[\"プライバシーに関するご質問はこちら:\"],\"9unqs3\":[\"退席中:\"],\"9v3hwv\":[\"サーバーが見つかりません。\"],\"9zb2WA\":[\"接続中\"],\"A1taO8\":[\"検索\"],\"A2adVi\":[\"入力中通知を送信する\"],\"A9Rhec\":[\"チャンネル名\"],\"AWOSPo\":[\"ズームイン\"],\"AXSpEQ\":[\"接続時にOperになる\"],\"AeXO77\":[\"アカウント\"],\"AhNP40\":[\"シーク\"],\"Ai2U7L\":[\"ホスト\"],\"AjBQnf\":[\"ニックネームを変更しました\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"返信をキャンセル\"],\"ApSx0O\":[\"\\\"\",[\"searchQuery\"],\"\\\"に一致するメッセージが\",[\"0\"],\"件見つかりました\"],\"AxPAXW\":[\"結果が見つかりません\"],\"AyNqAB\":[\"すべてのサーバーイベントをチャットに表示する\"],\"B/QqGw\":[\"席を外しています\"],\"B8AaMI\":[\"この項目は必須です\"],\"BA2c49\":[\"このサーバーは高度なLISTフィルタリングをサポートしていません\"],\"BDKt3I\":[[\"0\"],\"、\",[\"1\"],\"、\",[\"2\"],\" と他 \",[\"3\"],\" 人が入力中...\"],\"BGul2A\":[\"保存されていない変更があります。保存せずに閉じてもよいですか?\"],\"BIf9fi\":[\"ステータスメッセージ\"],\"BPm98R\":[\"サーバーが選択されていません。まずサイドバーからサーバーを選択してください。招待リンクはサーバーごとに管理されます。\"],\"BZz3md\":[\"個人ウェブサイト\"],\"Bgm/H7\":[\"複数行のテキスト入力を許可する\"],\"BiQIl1\":[\"このプライベートメッセージの会話をピン留めする\"],\"BlNZZ2\":[\"クリックしてメッセージに移動\"],\"Bowq3c\":[\"オペレーターのみがチャンネルトピックを変更できます\"],\"Btozzp\":[\"この画像の有効期限が切れています\"],\"Bycfjm\":[\"合計:\",[\"0\"]],\"C6IBQc\":[\"JSON全体をコピー\"],\"C9L9wL\":[\"データ収集\"],\"CDq4wC\":[\"ユーザーをモデレート\"],\"CHVRxG\":[\"@\",[\"0\"],\" へメッセージ(Shift+Enterで改行)\"],\"CN9zdR\":[\"Oper名とパスワードは必須です\"],\"CW3sYa\":[\"リアクション \",[\"emoji\"],\" を追加\"],\"CaAkqd\":[\"退出を表示\"],\"CbvaYj\":[\"ニックネームでBAN\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"チャンネルを選択\"],\"CsekCi\":[\"通常\"],\"D+NlUC\":[\"システム\"],\"D28t6+\":[\"参加して退出しました\"],\"DB8zMK\":[\"適用\"],\"DBcWHr\":[\"カスタム通知音ファイル\"],\"DTy9Xw\":[\"メディアプレビュー\"],\"Dj4pSr\":[\"安全なパスワードを選択してください\"],\"Du+zn+\":[\"検索中...\"],\"Du2T2f\":[\"設定が見つかりません\"],\"DwsSVQ\":[\"フィルターを適用して更新\"],\"E3W/zd\":[\"デフォルトニックネーム\"],\"E6nRW7\":[\"URLをコピー\"],\"E703RG\":[\"モード:\"],\"EAeu1Z\":[\"招待を送信\"],\"EFKJQT\":[\"設定\"],\"EGPQBv\":[\"カスタムフラッドルール (+f)\"],\"ELik0r\":[\"プライバシーポリシー全文を見る\"],\"EPbeC2\":[\"チャンネルトピックを表示または編集する\"],\"EQCDNT\":[\"oper ユーザー名を入力...\"],\"EUvulZ\":[\"\\\"\",[\"searchQuery\"],\"\\\"に一致するメッセージが1件見つかりました\"],\"EatZYJ\":[\"次の画像\"],\"EdQY6l\":[\"なし\"],\"EnqLYU\":[\"サーバーを検索...\"],\"F0OKMc\":[\"サーバーを編集\"],\"F6Int2\":[\"ハイライトを有効にする\"],\"FDoLyE\":[\"最大ユーザー数\"],\"FUU/hZ\":[\"チャットで読み込む外部メディアの量を制御します。\"],\"Fdp03t\":[\"オン\"],\"FfPWR0\":[\"モーダル\"],\"FjkaiT\":[\"ズームアウト\"],\"FlqOE9\":[\"これが意味すること:\"],\"FolHNl\":[\"アカウントと認証を管理する\"],\"Fp2Dif\":[\"サーバーを退出しました\"],\"G5KmCc\":[\"GZ-Line(グローバルZ-Line)\"],\"GDs0lz\":[\"<0>リスク:0> 機密情報(メッセージ、プライベート会話、認証情報)が、IRCサーバー間に位置するネットワーク管理者や攻撃者に露出する可能性があります。\"],\"GR+2I3\":[\"招待マスクを追加(例:nick!*@*、*!*@host.com)\"],\"GRLyMU\":[\"ポップアウトしたサーバー通知を閉じる\"],\"GdhD7H\":[\"もう一度クリックして確認\"],\"GlHnXw\":[\"ニックネームの変更に失敗しました: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"プレビュー:\"],\"GtmO8/\":[\"から\"],\"GtuHUQ\":[\"サーバー上でこのチャンネルの名前を変更します。すべてのユーザーに新しい名前が表示されます。\"],\"GuGfFX\":[\"検索を切り替え\"],\"GxkJXS\":[\"アップロード中...\"],\"GzbwnK\":[\"チャンネルに参加しました\"],\"GzsUDB\":[\"拡張プロフィール\"],\"H/PnT8\":[\"絵文字を挿入\"],\"H6Izzl\":[\"お好みのカラーコード\"],\"H9jIv+\":[\"参加/退出を表示\"],\"HAKBY9\":[\"ファイルをアップロード\"],\"HdE1If\":[\"チャンネル\"],\"Hk4AW9\":[\"お好みの表示名\"],\"HmHDk7\":[\"メンバーを選択\"],\"HrQzPU\":[[\"networkName\"],\" のチャンネル\"],\"I2tXQ5\":[\"@\",[\"0\"],\" へメッセージ(Enterで改行、Shift+Enterで送信)\"],\"I6bw/h\":[\"ユーザーをBAN\"],\"I92Z+b\":[\"通知を有効にする\"],\"I9D72S\":[\"このメッセージを削除してもよいですか?この操作は元に戻せません。\"],\"IA+1wo\":[\"ユーザーがチャンネルからキックされたときに表示する\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"情報:\"],\"IUwGEM\":[\"変更を保存\"],\"IVeGK6\":[[\"0\"],\"、\",[\"1\"],\"、\",[\"2\"],\" が入力中...\"],\"IgrLD/\":[\"一時停止\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"返信\"],\"IoHMnl\":[\"最大値は \",[\"0\"],\" です\"],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"接続中...\"],\"J5T9NW\":[\"ユーザー情報\"],\"J8Y5+z\":[\"おっと!ネットワーク分割が発生しました!⚠️\"],\"JBHkBA\":[\"チャンネルを退出しました\"],\"JCwL0Q\":[\"理由を入力(任意)\"],\"JFciKP\":[\"切り替え\"],\"JXGkhG\":[\"チャンネル名を変更する(オペレーターのみ)\"],\"JcD7qf\":[\"その他のアクション\"],\"JdkA+c\":[\"シークレット (+s)\"],\"Jmu12l\":[\"サーバーチャンネル\"],\"JvQ++s\":[\"Markdownを有効にする\"],\"K2jwh/\":[\"WHOISデータがありません\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"メッセージを削除\"],\"KKBlUU\":[\"埋め込み\"],\"KM0pLb\":[\"チャンネルへようこそ!\"],\"KR6W2h\":[\"無視を解除\"],\"KV+Bi1\":[\"招待制 (+i)\"],\"KdCtwE\":[\"カウンターをリセットするまでフラッドアクティビティを監視する秒数\"],\"Kkezga\":[\"サーバーパスワード\"],\"KsiQ/8\":[\"ユーザーはチャンネルに参加するために招待が必要です\"],\"L+gB/D\":[\"チャンネル情報\"],\"LC1a7n\":[\"IRCサーバーは、サーバー間リンクのセキュリティレベルが低いと報告しています。これは、ネットワーク内のIRCサーバー間でメッセージが中継される際に、適切に暗号化されていないか、SSL/TLS証明書が正しく検証されない可能性があることを意味します。\"],\"LNfLR5\":[\"キックを表示\"],\"LQb0W/\":[\"すべてのイベントを表示\"],\"LU7/yA\":[\"UI上で表示するための別名です。スペース、絵文字、特殊文字を含めることができます。実際のチャンネル名(\",[\"channelName\"],\")は引き続きIRCコマンドで使用されます。\"],\"LUb9O7\":[\"有効なサーバーポートが必要です\"],\"LV4fT6\":[\"説明(任意、例:「Q3ベータテスター」)\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"プライバシーポリシー\"],\"LcuSDR\":[\"プロフィール情報とメタデータを管理する\"],\"LqLS9B\":[\"ニックネーム変更を表示\"],\"LsDQt2\":[\"チャンネル設定\"],\"LtI9AS\":[\"オーナー\"],\"LuNhhL\":[\"このメッセージにリアクションしました\"],\"M/AZNG\":[\"アバター画像のURL\"],\"M/WIer\":[\"メッセージを送信\"],\"M8er/5\":[\"名前:\"],\"MHk+7g\":[\"前の画像\"],\"MRorGe\":[\"DMを送る\"],\"MVbSGP\":[\"時間ウィンドウ(秒)\"],\"MkpcsT\":[\"メッセージと設定はデバイス上にローカルで保存されます\"],\"N/hDSy\":[\"ボットとしてマーク — 通常は「on」または空欄\"],\"N7TQbE\":[[\"channelName\"],\" にユーザーを招待\"],\"NCca/o\":[\"デフォルトニックネームを入力...\"],\"Nqs6B9\":[\"すべての外部メディアを表示します。URLが不明なサーバーへのリクエストを引き起こす場合があります。\"],\"Nt+9O7\":[\"生のTCPの代わりにWebSocketを使用する\"],\"NxIHzc\":[\"ユーザーを切断\"],\"O+v/cL\":[\"サーバー上のすべてのチャンネルを一覧表示\"],\"ODwSCk\":[\"GIFを送信\"],\"OGQ5kK\":[\"通知音とハイライトを設定する\"],\"OIPt1Z\":[\"メンバーリストのサイドバーを表示/非表示にする\"],\"OKSNq/\":[\"とても厳格\"],\"ONWvwQ\":[\"アップロード\"],\"OVKoQO\":[\"認証用のアカウントパスワード\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - IRCを未来へ\"],\"OhCpra\":[\"トピックを設定…\"],\"OkltoQ\":[[\"username\"],\" をニックネームでBANする(同じニックネームでの再参加を防止)\"],\"P+t/Te\":[\"追加データなし\"],\"P42Wcc\":[\"安全\"],\"PD38l0\":[\"チャンネルアバタープレビュー\"],\"PD9mEt\":[\"メッセージを入力...\"],\"PPqfdA\":[\"チャンネル設定を開く\"],\"PSCjfZ\":[\"このチャンネルに表示されるトピックです。すべてのユーザーがトピックを閲覧できます。\"],\"PZCecv\":[\"PDFプレビュー\"],\"PeLgsC\":[[\"c\",\"plural\",{\"other\":[[\"c\"],\"回\"]}]],\"PguS2C\":[\"例外マスクを追加(例:nick!*@*、*!*@host.com)\"],\"Pil5Ty\":[[\"0\"],\" 件中 \",[\"displayedChannelsCount\"],\" 件を表示中\"],\"PqhVlJ\":[\"ユーザーをBAN(ホストマスク)\"],\"Q+chwU\":[\"ユーザー名:\"],\"Q2QY4/\":[\"この招待を削除\"],\"Q6hhn8\":[\"設定\"],\"QF4a34\":[\"ユーザー名を入力してください\"],\"QGqSZ2\":[\"カラーと書式設定\"],\"QJQd1J\":[\"プロフィールを編集\"],\"QSzGDE\":[\"アイドル\"],\"QUlny5\":[[\"0\"],\" へようこそ!\"],\"Qoq+GP\":[\"もっと読む\"],\"QuSkCF\":[\"チャンネルをフィルター...\"],\"QwUrDZ\":[\"トピックを変更しました: \",[\"topic\"]],\"R0UH07\":[[\"1\"],\"枚中\",[\"0\"],\"枚目の画像\"],\"R7SsBE\":[\"ミュート\"],\"R8rf1X\":[\"クリックしてトピックを設定\"],\"RArB3D\":[[\"username\"],\" によって \",[\"channelName\"],\" からキックされました\"],\"RI3cWd\":[\"ObsidianIRCでIRCの世界を探索しよう\"],\"RIfHS5\":[\"新しい招待リンクを作成\"],\"RMMaN5\":[\"モデレート制 (+m)\"],\"RWw9Lg\":[\"モーダルを閉じる\"],\"RZ2BuZ\":[[\"account\"],\" のアカウント登録には確認が必要です: \",[\"message\"]],\"RySp6q\":[\"コメントを非表示\"],\"SPKQTd\":[\"ニックネームは必須です\"],\"SPVjfj\":[\"空欄の場合は「理由なし」がデフォルトになります\"],\"SQKPvQ\":[\"ユーザーを招待\"],\"SkZcl+\":[\"定義済みのフラッド保護プロファイルを選択してください。これらのプロファイルは、さまざまなユースケースに対してバランスの取れた保護設定を提供します。\"],\"Slr+3C\":[\"最小ユーザー数\"],\"Spnlre\":[[\"target\"],\" を \",[\"channel\"],\" に招待しました\"],\"T/ckN5\":[\"ビューアで開く\"],\"T91vKp\":[\"再生\"],\"TV2Wdu\":[\"データの取り扱いとプライバシー保護について詳しく見る。\"],\"TgFpwD\":[\"適用中...\"],\"TkzSFB\":[\"変更なし\"],\"TtserG\":[\"本名を入力\"],\"Ttz9J1\":[\"パスワードを入力...\"],\"Tz0i8g\":[\"設定\"],\"U3pytU\":[\"管理者\"],\"UDb2YD\":[\"リアクション\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"まだ招待リンクを作成していません。上のフォームから最初の招待リンクを作成してください。\"],\"UGT5vp\":[\"設定を保存\"],\"UV5hLB\":[\"BANが見つかりません\"],\"Uaj3Nd\":[\"ステータスメッセージ\"],\"Ue3uny\":[\"デフォルト(プロファイルなし)\"],\"UkARhe\":[\"通常 — 標準的な保護\"],\"Umn7Cj\":[\"まだコメントはありません。最初のコメントを投稿しましょう!\"],\"UtUIRh\":[[\"0\"],\" 件の古いメッセージ\"],\"UwzP+U\":[\"セキュア接続\"],\"V0/A4O\":[\"チャンネルオーナー\"],\"V4qgxE\":[\"作成日時(以前、分前)\"],\"V8yTm6\":[\"検索をクリア\"],\"VJMMyz\":[\"ObsidianIRC — IRCを未来へ\"],\"VJScHU\":[\"理由\"],\"VLsmVV\":[\"通知をミュート\"],\"VbyRUy\":[\"コメント\"],\"Vmx0mQ\":[\"設定者:\"],\"VqnIZz\":[\"プライバシーポリシーとデータ取り扱い方針を見る\"],\"VrMygG\":[\"最小文字数は \",[\"0\"],\" 文字です\"],\"VrnTui\":[\"プロフィールに表示される代名詞\"],\"W8E3qn\":[\"認証済みアカウント\"],\"WAakm9\":[\"チャンネルを削除\"],\"WFxTHC\":[\"BANマスクを追加(例:nick!*@*、*!*@host.com)\"],\"WN1g9F\":[\"サーバーホストは必須です\"],\"WRYdXW\":[\"音声の再生位置\"],\"WUOH5B\":[\"ユーザーを無視\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"other\":[\"さらに \",[\"1\"],\" 件表示\"]}]],\"WYxRzo\":[\"招待リンクを作成・管理する\"],\"Wd38W1\":[\"チャンネルを空欄にすると、汎用のネットワーク招待になります。説明はあなた専用の記録で、このリストで自分にのみ表示されます。\"],\"Weq9zb\":[\"一般\"],\"Wfj7Sk\":[\"通知音をミュートまたはミュート解除する\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"ユーザープロフィール\"],\"X6S3lt\":[\"設定、チャンネル、サーバーを検索...\"],\"XEHan5\":[\"このまま続行\"],\"XI1+wb\":[\"無効な形式\"],\"XIXeuC\":[\"@\",[\"0\"],\" へメッセージ\"],\"XMS+k4\":[\"プライベートメッセージを開始\"],\"XWgxXq\":[\"アルバム\"],\"Xd7+IT\":[\"プライベートチャットのピン留めを解除\"],\"Xm/s+u\":[\"表示\"],\"Xp2n93\":[\"サーバーの信頼済みファイルホストからのメディアを表示します。外部サービスへのリクエストは発生しません。\"],\"XvjC4F\":[\"保存中...\"],\"Y/qryO\":[\"検索に一致するユーザーが見つかりません\"],\"YAqRpI\":[[\"account\"],\" のアカウント登録に成功しました: \",[\"message\"]],\"YEfzvP\":[\"トピック保護 (+t)\"],\"YQOn6a\":[\"メンバーリストを折りたたむ\"],\"YRCoE9\":[\"チャンネルオペレーター\"],\"YURQaF\":[\"プロフィールを表示\"],\"YdBSvr\":[\"メディア表示と外部コンテンツを制御する\"],\"Yj6U3V\":[\"中央サーバーなし:\"],\"YjvpGx\":[\"代名詞\"],\"YqH4l4\":[\"キーなし\"],\"YyUPpV\":[\"アカウント:\"],\"ZJSWfw\":[\"サーバーから切断したときに表示されるメッセージ\"],\"ZR1dJ4\":[\"招待\"],\"ZdWg0V\":[\"ブラウザで開く\"],\"ZhRBbl\":[\"メッセージを検索…\"],\"Zmcu3y\":[\"詳細フィルター\"],\"a2/8e5\":[\"トピック設定日時(以降、分前)\"],\"aHKcKc\":[\"前のページ\"],\"aJTbXX\":[\"Operパスワード\"],\"aQryQv\":[\"パターンはすでに存在します\"],\"aW9pLN\":[\"チャンネルに参加できる最大ユーザー数。制限なしの場合は空欄にしてください。\"],\"ah4fmZ\":[\"YouTube、Vimeo、SoundCloudなどの既知のサービスのプレビューも表示します。\"],\"aifXak\":[\"このチャンネルにはメディアがありません\"],\"ap2zBz\":[\"緩め\"],\"az8lvo\":[\"オフ\"],\"azXSNo\":[\"メンバーリストを展開\"],\"azdliB\":[\"アカウントにログイン\"],\"b26wlF\":[\"彼女/彼女の\"],\"bD/+Ei\":[\"厳格\"],\"bQ6BJn\":[\"詳細なフラッド保護ルールを設定します。各ルールは、監視するアクティビティの種類と、しきい値を超えた場合に実行するアクションを指定します。\"],\"beV7+y\":[\"ユーザーは \",[\"channelName\"],\" への参加招待を受け取ります。\"],\"bk84cH\":[\"離席メッセージ\"],\"bkHdLj\":[\"IRCサーバーを追加\"],\"bmQLn5\":[\"ルールを追加\"],\"bwRvnp\":[\"アクション\"],\"c8+EVZ\":[\"認証済みアカウント\"],\"cGYUlD\":[\"メディアプレビューは読み込まれません。\"],\"cLF98o\":[\"コメントを表示 (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"利用可能なユーザーがいません\"],\"cSgpoS\":[\"プライベートチャットをピン留め\"],\"cde3ce\":[\"<0>\",[\"0\"],\"0> へメッセージ\"],\"chQsxg\":[\"フォーマット済み出力をコピー\"],\"cl/A5J\":[[\"__DEFAULT_IRC_SERVER_NAME__\"],\" へようこそ!\"],\"cnGeoo\":[\"削除\"],\"coPLXT\":[\"IRC通信はサーバーに保存されません\"],\"crYH/6\":[\"SoundCloudプレーヤー\"],\"d3sis4\":[\"サーバーを追加\"],\"d9aN5k\":[[\"username\"],\" をチャンネルから削除する\"],\"dEgA5A\":[\"キャンセル\"],\"dGi1We\":[\"このプライベートメッセージの会話のピン留めを解除する\"],\"dJVuyC\":[[\"channelName\"],\" を退出しました (\",[\"reason\"],\")\"],\"dMtLDE\":[\"宛て\"],\"dXqxlh\":[\"<0>⚠️ セキュリティリスク!0> この接続は傍受や中間者攻撃に対して脆弱な可能性があります。\"],\"da9Q/R\":[\"チャンネルモードを変更しました\"],\"dhJN3N\":[\"コメントを表示\"],\"dj2xTE\":[\"通知を閉じる\"],\"dpCzmC\":[\"フラッド保護設定\"],\"e9dQpT\":[\"このリンクを新しいタブで開きますか?\"],\"ePK91l\":[\"編集\"],\"eYBDuB\":[\"画像をアップロードするか、動的サイズ変換のための \",[\"size\"],\" 置換を含むURLを入力してください\"],\"edBbee\":[[\"username\"],\" をホストマスクでBANする(同じIP/ホストからの再参加を防止)\"],\"ekfzWq\":[\"ユーザー設定\"],\"elPDWs\":[\"IRCクライアントの使い心地をカスタマイズする\"],\"eu2osY\":[\"<0>💡 推奨事項:0> このサーバーを信頼し、リスクを理解している場合のみ続行してください。この接続で機密情報やパスワードを共有しないようにしてください。\"],\"euEhbr\":[\"クリックして \",[\"channel\"],\" に参加\"],\"ez3vLd\":[\"複数行入力を有効にする\"],\"f0J5Ki\":[\"サーバー間通信に暗号化されていない接続が使用される可能性があります\"],\"f9BHJk\":[\"ユーザーに警告\"],\"fDOLLd\":[\"チャンネルが見つかりません。\"],\"ffzDkB\":[\"匿名分析:\"],\"fq1GF9\":[\"ユーザーがサーバーから切断したときに表示\"],\"gEF57C\":[\"このサーバーは1種類の接続タイプのみサポートしています\"],\"gJuLUI\":[\"無視リスト\"],\"gNzMrk\":[\"現在のアバター\"],\"gjPWyO\":[\"ニックネームを入力...\"],\"gz6UQ3\":[\"最大化\"],\"h6razj\":[\"チャンネル名マスクを除外\"],\"hG6jnw\":[\"トピックが設定されていません\"],\"hG89Ed\":[\"画像\"],\"hYgDIe\":[\"作成\"],\"hZ6znB\":[\"ポート\"],\"ha+Bz5\":[\"例:100:1440\"],\"he3ygx\":[\"コピー\"],\"hehnjM\":[\"数量\"],\"hzdLuQ\":[\"Voice以上の権限を持つユーザーのみ発言できます\"],\"i0qMbr\":[\"ホーム\"],\"iDNBZe\":[\"通知\"],\"iH8pgl\":[\"戻る\"],\"iL9SZg\":[\"ユーザーをBAN(ニックネーム)\"],\"iNt+3c\":[\"画像に戻る\"],\"iQvi+a\":[\"このサーバーのリンクセキュリティが低い場合に警告しない\"],\"iSLIjg\":[\"接続\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"サーバーホスト\"],\"idD8Ev\":[\"保存済み\"],\"iivqkW\":[\"サインイン日時\"],\"ij+Elv\":[\"画像プレビュー\"],\"ilIWp7\":[\"通知を切り替え\"],\"iuaqvB\":[\"ワイルドカードには * を使用してください。例:baduser!*@*、*!*@spammer.com、troll*!*@*\"],\"ixkTse\":[\"ボット\"],\"j2DGR0\":[\"ホストマスクでBAN\"],\"jA4uoI\":[\"トピック:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"理由(任意)\"],\"jUV7CU\":[\"アバターをアップロード\"],\"jW5Uwh\":[\"外部メディアの読み込み範囲を制御します。オフ/安全/信頼できるソース/すべてのコンテンツ。\"],\"jXzms5\":[\"添付オプション\"],\"jZlrte\":[\"カラー\"],\"jfC/xh\":[\"連絡先\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"古いメッセージを読み込む\"],\"k3ID0F\":[\"メンバーをフィルター…\"],\"k65gsE\":[\"詳細を見る\"],\"k7Zgob\":[\"接続をキャンセル\"],\"kAVx5h\":[\"招待が見つかりません\"],\"kCLEPU\":[\"接続先\"],\"kF5LKb\":[\"無視パターン:\"],\"kGeOx/\":[[\"0\"],\" に参加\"],\"kITKr8\":[\"チャンネルモードを読み込み中...\"],\"kPpPsw\":[\"あなたはIRC Operatorです\"],\"kWJmRL\":[\"あなた\"],\"kfcRb0\":[\"アバター\"],\"kjMqSj\":[\"JSONをコピー\"],\"krViRy\":[\"JSONとしてコピーするにはクリック\"],\"ks71ra\":[\"例外\"],\"kw4lRv\":[\"チャンネルハーフオペレーター\"],\"kxgIRq\":[\"チャンネルを選択または追加して始めましょう。\"],\"ky6dWe\":[\"アバタープレビュー\"],\"l+GxCv\":[\"チャンネルを読み込み中...\"],\"l+IUVW\":[[\"account\"],\" のアカウント確認に成功しました: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"other\":[[\"reconnectCount\"],\"回 再接続した\"]}]],\"l1l8sj\":[[\"0\"],\"日前\"],\"l5NhnV\":[\"#チャンネル(任意)\"],\"l5jmzx\":[[\"0\"],\" と \",[\"1\"],\" が入力中...\"],\"lCF0wC\":[\"更新\"],\"lHy8N5\":[\"さらにチャンネルを読み込み中...\"],\"lasgrr\":[\"使用済み\"],\"lbpf14\":[[\"value\"],\"に参加\"],\"lfFsZ4\":[\"チャンネル\"],\"lkNdiH\":[\"アカウント名\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"画像をアップロード\"],\"loQxaJ\":[\"戻りました\"],\"lvfaxv\":[\"ホーム\"],\"m16xKo\":[\"追加\"],\"m8flAk\":[\"プレビュー(未アップロード)\"],\"mEPxTp\":[\"<0>⚠️ 注意!0> 信頼できる送信元のリンクのみ開いてください。悪意のあるリンクはセキュリティやプライバシーを侵害する恐れがあります。\"],\"mHGdhG\":[\"サーバー情報\"],\"mHS8lb\":[\"#\",[\"0\"],\" へメッセージ\"],\"mMYBD9\":[\"広め — より広範な保護範囲\"],\"mTGsPd\":[\"チャンネルトピック\"],\"mU8j6O\":[\"外部メッセージ禁止 (+n)\"],\"mZp8FL\":[\"自動的に1行入力に切り替え\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"コメント (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"ユーザーは認証済みです\"],\"mwtcGl\":[\"コメントを閉じる\"],\"mzI/c+\":[\"ダウンロード\"],\"n3fGRk\":[[\"0\"],\" が設定\"],\"nE9jsU\":[\"緩め — 控えめな保護\"],\"nNflMD\":[\"チャンネルを退出\"],\"nPXkBi\":[\"WHOISデータを読み込み中...\"],\"nQnxxF\":[\"#\",[\"0\"],\" へメッセージ(Shift+Enterで改行)\"],\"nWMRxa\":[\"ピン留めを解除\"],\"nkC032\":[\"フラッドプロファイルなし\"],\"o69z4d\":[[\"username\"],\" に警告メッセージを送る\"],\"o9ylQi\":[\"GIFを検索して始めましょう\"],\"oFGkER\":[\"サーバー通知\"],\"oOi11l\":[\"最下部にスクロール\"],\"oPYIL5\":[\"ネットワーク\"],\"oQEzQR\":[\"新しいDM\"],\"oXOSPE\":[\"オンライン\"],\"oal760\":[\"サーバーリンクへの中間者攻撃が可能な状態です\"],\"oeqmmJ\":[\"信頼できるソース\"],\"optX0N\":[[\"0\"],\"時間前\"],\"ovBPCi\":[\"デフォルト\"],\"p0Z69r\":[\"パターンを空にすることはできません\"],\"p1KgtK\":[\"音声の読み込みに失敗しました\"],\"p59pEv\":[\"詳細情報\"],\"p7sRI6\":[\"入力中であることを他のユーザーに知らせる\"],\"pBm1od\":[\"シークレットチャンネル\"],\"pNmiXx\":[\"すべてのサーバーで使用するデフォルトのニックネーム\"],\"pUUo9G\":[\"ホスト名:\"],\"pVGPmz\":[\"アカウントパスワード\"],\"peNE68\":[\"永続的\"],\"plhHQt\":[\"データなし\"],\"pm6+q5\":[\"セキュリティ警告\"],\"pn5qSs\":[\"追加情報\"],\"q0cR4S\":[\"は現在 **\",[\"newNick\"],\"** として知られています\"],\"qFcunY\":[\"LIST または NAMES コマンドにチャンネルが表示されません\"],\"qLpTm/\":[\"リアクション \",[\"emoji\"],\" を削除\"],\"qVkGWK\":[\"ピン留め\"],\"qY8wNa\":[\"ホームページ\"],\"qb0xJ7\":[\"ワイルドカードを使用してください:* は任意の文字列、? は任意の1文字に一致します。例:nick!*@*、*!*@host.com、*!*user@*\"],\"qhzpRq\":[\"チャンネルキー (+k)\"],\"qtoOYG\":[\"制限なし\"],\"r1W2AS\":[\"ファイルホスト画像\"],\"rIPR2O\":[\"トピック設定日時(以前、分前)\"],\"rMMSYo\":[\"最大文字数は \",[\"0\"],\" 文字です\"],\"rWtzQe\":[\"ネットワークが分割され、再接続されました。✅\"],\"rYG2u6\":[\"しばらくお待ちください...\"],\"rdUucN\":[\"プレビュー\"],\"rjGI/Q\":[\"プライバシー\"],\"rk8iDX\":[\"GIFを読み込み中...\"],\"rn6SBY\":[\"ミュート解除\"],\"s/UKqq\":[\"チャンネルからキックされました\"],\"s8cATI\":[[\"channelName\"],\" に参加しました\"],\"sCO9ue\":[\"<0>\",[\"serverName\"],\"0> への接続には次のセキュリティ上の懸念事項があります:\"],\"sGH11W\":[\"サーバー\"],\"sHI1H+\":[\"は現在 **\",[\"newNick\"],\"** として知られています\"],\"sJyV04\":[[\"inviter\"],\" があなたを \",[\"channel\"],\" に招待しました\"],\"sby+1/\":[\"クリックしてコピー\"],\"sfN25C\":[\"本名またはフルネーム\"],\"sliuzR\":[\"リンクを開く\"],\"sqrO9R\":[\"カスタムメンション\"],\"sr6RdJ\":[\"Shift+Enterで複数行入力\"],\"swrCpB\":[[\"user\"],\" がチャンネルを \",[\"oldName\"],\" から \",[\"newName\"],\" に変更しました\",[\"0\"]],\"sxkWRg\":[\"詳細設定\"],\"t/YqKh\":[\"削除\"],\"t47eHD\":[\"このサーバーでの固有識別子\"],\"tAkAh0\":[\"動的サイズ変換のための \",[\"size\"],\" 置換を含むURL。例:https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"チャンネルリストのサイドバーを表示/非表示にする\"],\"tfDRzk\":[\"保存\"],\"tiBsJk\":[[\"channelName\"],\" を退出しました\"],\"tt4/UD\":[\"退出しました (\",[\"reason\"],\")\"],\"u0TcnO\":[\"ニックネーム {nick} は既に使用中です。{newNick} で再試行します\"],\"u0a8B4\":[\"管理者アクセスのためにIRC Operatorとして認証する\"],\"u0rWFU\":[\"作成日時(以降、分前)\"],\"u72w3t\":[\"無視するユーザーとパターン\"],\"u7jc2L\":[\"退出しました\"],\"uAQUqI\":[\"ステータス\"],\"uB85T3\":[\"保存失敗:\",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRCサーバー:\"],\"ukyW4o\":[\"あなたの招待リンク\"],\"usSSr/\":[\"ズームレベル\"],\"v7uvcf\":[\"ソフトウェア:\"],\"vE8kb+\":[\"Shift+Enterで改行(Enterで送信)\"],\"vERlcd\":[\"プロフィール\"],\"vK0RL8\":[\"トピックなし\"],\"vSJd18\":[\"動画\"],\"vXIe7J\":[\"言語\"],\"vaHYxN\":[\"本名\"],\"vhjbKr\":[\"離席中\"],\"w4NYox\":[[\"title\"],\" クライアント\"],\"w8xQRx\":[\"無効な値\"],\"wFjjxZ\":[[\"username\"],\" によって \",[\"channelName\"],\" からキックされました (\",[\"reason\"],\")\"],\"wGjaGl\":[\"BAN例外が見つかりません\"],\"wPrGnM\":[\"チャンネル管理者\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"ユーザーがチャンネルに参加・退出したときに表示する\"],\"whqZ9r\":[\"ハイライトする追加の単語またはフレーズ\"],\"wm7RV4\":[\"通知音\"],\"wz/Yoq\":[\"サーバー間で中継される際にメッセージが傍受される可能性があります\"],\"x3+y8b\":[\"このリンクから登録した人数\"],\"xCJdfg\":[\"クリア\"],\"xOTzt5\":[\"たった今\"],\"xUHRTR\":[\"接続時に自動的にOperatorとして認証する\"],\"xWHwwQ\":[\"BAN一覧\"],\"xYilR2\":[\"メディア\"],\"xbi8D6\":[\"このサーバーは招待リンクに対応していません(<0>obby.world/invitation0>ケイパビリティが告知されていません)。通常通りチャットは利用できます。このパネルはobbyircdベースのネットワーク用です。\"],\"xceQrO\":[\"安全なWebSocketのみサポートされています\"],\"xdtXa+\":[\"チャンネル名\"],\"xfXC7q\":[\"テキストチャンネル\"],\"xlCYOE\":[\"メッセージを取得中...\"],\"xlhswE\":[\"最小値は \",[\"0\"],\" です\"],\"xq97Ci\":[\"単語またはフレーズを追加...\"],\"xuRqRq\":[\"クライアント制限 (+l)\"],\"xwF+7J\":[[\"0\"],\" が入力中...\"],\"y1eoq1\":[\"リンクをコピー\"],\"yNeucF\":[\"このサーバーは拡張プロフィールメタデータ(IRCv3 METADATA拡張)をサポートしていません。アバター、表示名、ステータスなどの追加フィールドは利用できません。\"],\"yPlrca\":[\"チャンネルアバター\"],\"yQE2r9\":[\"読み込み中\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Operユーザー名\"],\"yYOzWD\":[\"ログ\"],\"yfx9Re\":[\"IRC Operatorパスワード\"],\"ygCKqB\":[\"停止\"],\"ymDxJx\":[\"IRC Operatorユーザー名\"],\"yrpRsQ\":[\"名前順で並び替え\"],\"yz7wBu\":[\"閉じる\"],\"zJw+jA\":[\"モードを設定: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"分前\"],\"zebeLu\":[\"Operユーザー名を入力\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"無効なパターン形式です。nick!user@host 形式を使用してください(ワイルドカード * 使用可)\"],\"+6NQQA\":[\"一般サポートチャンネル\"],\"+6NyRG\":[\"クライアント\"],\"+K0AvT\":[\"切断\"],\"+cyFdH\":[\"離席時に表示するデフォルトメッセージ\"],\"+mVPqU\":[\"メッセージ内のMarkdown書式を表示する\"],\"+vqCJH\":[\"認証用のアカウントユーザー名\"],\"+yPBXI\":[\"ファイルを選択\"],\"+zy2Nq\":[\"種類\"],\"/09cao\":[\"リンクセキュリティが低い(レベル \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"チャンネル外のユーザーはメッセージを送信できません\"],\"/4C8U0\":[\"ãã¹ã¦ã³ãã¼\"],\"/6BzZF\":[\"メンバーリストを切り替え\"],\"/AkXyp\":[\"確èªãã¾ããï¼\"],\"/TNOPk\":[\"ユーザーは離席中です\"],\"/XQgft\":[\"探す\"],\"/cF7Rs\":[\"音量\"],\"/dqduX\":[\"次のページ\"],\"/fc3q4\":[\"すべてのコンテンツ\"],\"/kISDh\":[\"通知音を有効にする\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"音声\"],\"/rfkZe\":[\"メンションやメッセージに対してサウンドを再生する\"],\"0/0ZGA\":[\"チャンネル名マスク\"],\"0D6j7U\":[\"カスタムルールについて詳しく →\"],\"0XsHcR\":[\"ユーザーをキック\"],\"0ZpE//\":[\"ユーザー数順で並び替え\"],\"0bEPwz\":[\"離席中に設定\"],\"0dGkPt\":[\"チャンネルリストを展開\"],\"0gS7M5\":[\"表示名\"],\"0kS+M8\":[\"サンプルNET\"],\"0rgoY7\":[\"自分で選んだサーバーにのみ接続します\"],\"0wdd7X\":[\"参加\"],\"0wkVYx\":[\"プライベートメッセージ\"],\"111uHX\":[\"リンクプレビュー\"],\"196EG4\":[\"プライベートチャットを削除\"],\"1DSr1i\":[\"アカウントを登録する\"],\"1O/24y\":[\"チャンネルリストを切り替え\"],\"1TNIig\":[\"開く\"],\"1VPJJ2\":[\"外部リンクの警告\"],\"1ZC/dv\":[\"未読のメンションやメッセージはありません\"],\"1pO1zi\":[\"サーバー名は必須です\"],\"1uwfzQ\":[\"チャンネルトピックを表示\"],\"268g7c\":[\"表示名を入力\"],\"2CEOW6\":[\"soju バウンサー経由でバインドされたネットワーク\"],\"2F9+AZ\":[\"ã¾ã\xA0çã®IRCãã©ãã£ãã¯ãåå¾ãã¦ãã¾ãããæ¥ç¶ãããã¡ãã»ã¼ã¸ãéä¿¡ãã¦ãã\xA0ããã\"],\"2FOFq1\":[\"ネットワーク上のサーバーオペレーターがメッセージを閲覧できる可能性があります\"],\"2FYpfJ\":[\"詳細\"],\"2HF1Y2\":[[\"inviter\"],\" が \",[\"target\"],\" を \",[\"channel\"],\" に招待しました\"],\"2I70QL\":[\"ユーザープロフィール情報を表示する\"],\"2QYdmE\":[\"ユーザー:\"],\"2QpEjG\":[\"退出しました\"],\"2YE223\":[\"#\",[\"0\"],\" へメッセージ(Enterで改行、Shift+Enterで送信)\"],\"2bimFY\":[\"サーバーパスワードを使用する\"],\"2iTmdZ\":[\"ローカルストレージ:\"],\"2odkwe\":[\"厳格 — より積極的な保護\"],\"2uDhbA\":[\"招待するユーザー名を入力\"],\"2ygf/L\":[\"← 戻る\"],\"2zEgxj\":[\"GIFを検索...\"],\"3RdPhl\":[\"チャンネル名を変更\"],\"3THokf\":[\"Voiceユーザー\"],\"3TSz9S\":[\"最小化\"],\"3jBDvM\":[\"チャンネル表示名\"],\"3ryuFU\":[\"アプリ改善のための任意のクラッシュレポート\"],\"3uBF/8\":[\"ビューアを閉じる\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"アカウント名を入力...\"],\"4/Rr0R\":[\"現在のチャンネルにユーザーを招待する\"],\"4EZrJN\":[\"ルール\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"フラッドプロファイル (+F)\"],\"4RZQRK\":[\"今何してるの?\"],\"4hfTrB\":[\"ニックネーム\"],\"4n99LO\":[\"すでに \",[\"0\"],\" にいます\"],\"4t6vMV\":[\"短いメッセージの場合は自動的に1行入力に切り替える\"],\"4vsHmf\":[\"時間(分)\"],\"4x/Axu\":[\"バウンサーにはまだネットワークがありません。最初のネットワークを追加して始めましょう。\"],\"5+INAX\":[\"自分へのメンションを含むメッセージをハイライトする\"],\"5R5Pv/\":[\"Oper名\"],\"678PKt\":[\"ネットワーク名\"],\"6Aih4U\":[\"オフライン\"],\"6CO3WE\":[\"チャンネルに参加するために必要なパスワード。キーを削除するには空欄にしてください。\"],\"6HhMs3\":[\"退出メッセージ\"],\"6V3Ea3\":[\"コピーしました\"],\"6lGV3K\":[\"折りたたむ\"],\"6yFOEi\":[\"oper パスワードを入力...\"],\"7+IHTZ\":[\"ファイルが選択されていません\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host(例:spam*!*@*、*!*@badhost.com)\"],\"7QkKyN\":[\"プライベートメッセージを送信\"],\"7U1W7c\":[\"とても緩め\"],\"7Y1YQj\":[\"本名:\"],\"7YHArF\":[\"— ビューアで開く\"],\"7fjnVl\":[\"ユーザーを検索...\"],\"7jL88x\":[\"このメッセージを削除しますか?この操作は元に戻せません。\"],\"7nGhhM\":[\"今どんな気分ですか?\"],\"7sEpu1\":[\"メンバー — \",[\"0\"]],\"7sNhEz\":[\"ユーザー名\"],\"8H0Q+x\":[\"プロファイルについて詳しく →\"],\"8Phu0A\":[\"ユーザーがニックネームを変更したときに表示する\"],\"8XTG9e\":[\"Operパスワードを入力\"],\"8XsV2J\":[\"再送信\"],\"8ZsakT\":[\"パスワード\"],\"8kR84m\":[\"外部リンクを開こうとしています:\"],\"8lCgih\":[\"ルールを削除\"],\"8o3dPc\":[\"ãã¡ã¤ã«ããããããã¦ã¢ãããã¼ã\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"other\":[[\"joinCount\"],\"回 参加した\"]}]],\"9BMLnJ\":[\"サーバーに再接続\"],\"9OEgyT\":[\"リアクションを追加\"],\"9PQ8m2\":[\"G-Line(グローバルBAN)\"],\"9Qs99X\":[\"メール:\"],\"9QupBP\":[\"パターンを削除\"],\"9W7tl5\":[\"(変更なし)\"],\"9bG48P\":[\"送信中\"],\"9f5f0u\":[\"プライバシーに関するご質問はこちら:\"],\"9iweoP\":[[\"0\"],\" のネットワーク\"],\"9unqs3\":[\"退席中:\"],\"9v3hwv\":[\"サーバーが見つかりません。\"],\"9zb2WA\":[\"接続中\"],\"A1taO8\":[\"検索\"],\"A2adVi\":[\"入力中通知を送信する\"],\"A9Rhec\":[\"チャンネル名\"],\"AWOSPo\":[\"ズームイン\"],\"AXSpEQ\":[\"接続時にOperになる\"],\"AdKRCX\":[\"<0>\",[\"0\"],\"0> に接続しています。\"],\"AeXO77\":[\"アカウント\"],\"AhNP40\":[\"シーク\"],\"Ai2U7L\":[\"ホスト\"],\"AjBQnf\":[\"ニックネームを変更しました\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"返信をキャンセル\"],\"ApSx0O\":[\"\\\"\",[\"searchQuery\"],\"\\\"に一致するメッセージが\",[\"0\"],\"件見つかりました\"],\"AxPAXW\":[\"結果が見つかりません\"],\"AyNqAB\":[\"すべてのサーバーイベントをチャットに表示する\"],\"B/QqGw\":[\"席を外しています\"],\"B0sB2k\":[\"平文\"],\"B8AaMI\":[\"この項目は必須です\"],\"BA2c49\":[\"このサーバーは高度なLISTフィルタリングをサポートしていません\"],\"BDKt3I\":[[\"0\"],\"、\",[\"1\"],\"、\",[\"2\"],\" と他 \",[\"3\"],\" 人が入力中...\"],\"BGul2A\":[\"保存されていない変更があります。保存せずに閉じてもよいですか?\"],\"BIf9fi\":[\"ステータスメッセージ\"],\"BOJWfb\":[\"soju バウンサーから切断しますか?\"],\"BPm98R\":[\"ãµã¼ãã¼ã鏿ããã¦ãã¾ãããã¾ããµã¤ããã¼ãããµã¼ãã¼ã鏿ãã¦ãã\xA0ãããæå¾
ãªã³ã¯ã¯ãµã¼ãã¼ãã¨ã«ç®¡çããã¾ãã\"],\"BZz3md\":[\"個人ウェブサイト\"],\"Bgm/H7\":[\"複数行のテキスト入力を許可する\"],\"BiQIl1\":[\"このプライベートメッセージの会話をピン留めする\"],\"BlNZZ2\":[\"クリックしてメッセージに移動\"],\"Bowq3c\":[\"オペレーターのみがチャンネルトピックを変更できます\"],\"Btozzp\":[\"この画像の有効期限が切れています\"],\"Bycfjm\":[\"合計:\",[\"0\"]],\"C6IBQc\":[\"JSON全体をコピー\"],\"C9L9wL\":[\"データ収集\"],\"CDq4wC\":[\"ユーザーをモデレート\"],\"CHVRxG\":[\"@\",[\"0\"],\" へメッセージ(Shift+Enterで改行)\"],\"CN9zdR\":[\"Oper名とパスワードは必須です\"],\"CW3sYa\":[\"リアクション \",[\"emoji\"],\" を追加\"],\"CaAkqd\":[\"退出を表示\"],\"CbvaYj\":[\"ニックネームでBAN\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"チャンネルを選択\"],\"CsekCi\":[\"通常\"],\"D+NlUC\":[\"システム\"],\"D28t6+\":[\"参加して退出しました\"],\"DB8zMK\":[\"適用\"],\"DBcWHr\":[\"カスタム通知音ファイル\"],\"DTy9Xw\":[\"メディアプレビュー\"],\"Dj4pSr\":[\"安全なパスワードを選択してください\"],\"Du+zn+\":[\"検索中...\"],\"Du2T2f\":[\"設定が見つかりません\"],\"DwsSVQ\":[\"フィルターを適用して更新\"],\"E3W/zd\":[\"デフォルトニックネーム\"],\"E6nRW7\":[\"URLをコピー\"],\"E703RG\":[\"モード:\"],\"EAeu1Z\":[\"招待を送信\"],\"EFKJQT\":[\"設定\"],\"EGPQBv\":[\"カスタムフラッドルール (+f)\"],\"ELik0r\":[\"プライバシーポリシー全文を見る\"],\"EPbeC2\":[\"チャンネルトピックを表示または編集する\"],\"EQCDNT\":[\"oper ユーザー名を入力...\"],\"EUvulZ\":[\"\\\"\",[\"searchQuery\"],\"\\\"に一致するメッセージが1件見つかりました\"],\"EatZYJ\":[\"次の画像\"],\"EdQY6l\":[\"なし\"],\"EnqLYU\":[\"サーバーを検索...\"],\"F0OKMc\":[\"サーバーを編集\"],\"F6Int2\":[\"ハイライトを有効にする\"],\"FDoLyE\":[\"最大ユーザー数\"],\"FUU/hZ\":[\"チャットで読み込む外部メディアの量を制御します。\"],\"Fdp03t\":[\"オン\"],\"FfPWR0\":[\"モーダル\"],\"FjkaiT\":[\"ズームアウト\"],\"FlqOE9\":[\"これが意味すること:\"],\"FolHNl\":[\"アカウントと認証を管理する\"],\"Fp2Dif\":[\"サーバーを退出しました\"],\"G5KmCc\":[\"GZ-Line(グローバルZ-Line)\"],\"GDs0lz\":[\"<0>リスク:0> 機密情報(メッセージ、プライベート会話、認証情報)が、IRCサーバー間に位置するネットワーク管理者や攻撃者に露出する可能性があります。\"],\"GR+2I3\":[\"招待マスクを追加(例:nick!*@*、*!*@host.com)\"],\"GRLyMU\":[\"ポップアウトしたサーバー通知を閉じる\"],\"GdhD7H\":[\"ããä¸åº¦ã¯ãªãã¯ãã¦ç¢ºèª\"],\"GjRZex\":[\"ネットワークを切断しますか?\"],\"GlHnXw\":[\"ニックネームの変更に失敗しました: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"プレビュー:\"],\"GtmO8/\":[\"から\"],\"GtuHUQ\":[\"サーバー上でこのチャンネルの名前を変更します。すべてのユーザーに新しい名前が表示されます。\"],\"GuGfFX\":[\"検索を切り替え\"],\"GxkJXS\":[\"アップロード中...\"],\"GzbwnK\":[\"チャンネルに参加しました\"],\"GzsUDB\":[\"拡張プロフィール\"],\"H/PnT8\":[\"絵文字を挿入\"],\"H6Izzl\":[\"お好みのカラーコード\"],\"H9jIv+\":[\"参加/退出を表示\"],\"HAKBY9\":[\"ファイルをアップロード\"],\"HdE1If\":[\"チャンネル\"],\"Hk4AW9\":[\"お好みの表示名\"],\"HmHDk7\":[\"メンバーを選択\"],\"HrQzPU\":[[\"networkName\"],\" のチャンネル\"],\"I2tXQ5\":[\"@\",[\"0\"],\" へメッセージ(Enterで改行、Shift+Enterで送信)\"],\"I6bw/h\":[\"ユーザーをBAN\"],\"I92Z+b\":[\"通知を有効にする\"],\"I9D72S\":[\"このメッセージを削除してもよいですか?この操作は元に戻せません。\"],\"IA+1wo\":[\"ユーザーがチャンネルからキックされたときに表示する\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"情報:\"],\"IUwGEM\":[\"変更を保存\"],\"IVeGK6\":[[\"0\"],\"、\",[\"1\"],\"、\",[\"2\"],\" が入力中...\"],\"IgrLD/\":[\"一時停止\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"返信\"],\"IoHMnl\":[\"最大値は \",[\"0\"],\" です\"],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"接続中...\"],\"J5T9NW\":[\"ユーザー情報\"],\"J8Y5+z\":[\"おっと!ネットワーク分割が発生しました!⚠️\"],\"JBHkBA\":[\"チャンネルを退出しました\"],\"JCwL0Q\":[\"理由を入力(任意)\"],\"JFciKP\":[\"切り替え\"],\"JXGkhG\":[\"チャンネル名を変更する(オペレーターのみ)\"],\"JcD7qf\":[\"その他のアクション\"],\"JdkA+c\":[\"シークレット (+s)\"],\"Jmu12l\":[\"サーバーチャンネル\"],\"JoQY+E\":[\"これにより、ネットワークが soju バウンサーから削除されます。再度使用するには、再追加する必要があります。\"],\"JvQ++s\":[\"Markdownを有効にする\"],\"K2jwh/\":[\"WHOISデータがありません\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"メッセージを削除\"],\"KKBlUU\":[\"埋め込み\"],\"KM0pLb\":[\"チャンネルへようこそ!\"],\"KR6W2h\":[\"無視を解除\"],\"KV+Bi1\":[\"招待制 (+i)\"],\"KdCtwE\":[\"カウンターをリセットするまでフラッドアクティビティを監視する秒数\"],\"Kkezga\":[\"サーバーパスワード\"],\"KsiQ/8\":[\"ユーザーはチャンネルに参加するために招待が必要です\"],\"L+gB/D\":[\"チャンネル情報\"],\"LC1a7n\":[\"IRCサーバーは、サーバー間リンクのセキュリティレベルが低いと報告しています。これは、ネットワーク内のIRCサーバー間でメッセージが中継される際に、適切に暗号化されていないか、SSL/TLS証明書が正しく検証されない可能性があることを意味します。\"],\"LEwpeL\":[\"soju バウンサー(制御)\"],\"LNfLR5\":[\"キックを表示\"],\"LP+1Z7\":[\"ネットワークを追加\"],\"LQb0W/\":[\"すべてのイベントを表示\"],\"LU7/yA\":[\"UI上で表示するための別名です。スペース、絵文字、特殊文字を含めることができます。実際のチャンネル名(\",[\"channelName\"],\")は引き続きIRCコマンドで使用されます。\"],\"LUb9O7\":[\"有効なサーバーポートが必要です\"],\"LV4fT6\":[\"説æï¼ä»»æãä¾ï¼ãQ3ãã¼ã¿ãã¹ã¿ã¼ãï¼\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"プライバシーポリシー\"],\"LcuSDR\":[\"プロフィール情報とメタデータを管理する\"],\"LqLS9B\":[\"ニックネーム変更を表示\"],\"LsDQt2\":[\"チャンネル設定\"],\"LtI9AS\":[\"オーナー\"],\"LuNhhL\":[\"このメッセージにリアクションしました\"],\"M/AZNG\":[\"アバター画像のURL\"],\"M/WIer\":[\"メッセージを送信\"],\"M8er/5\":[\"名前:\"],\"MHk+7g\":[\"前の画像\"],\"MRorGe\":[\"DMを送る\"],\"MVbSGP\":[\"時間ウィンドウ(秒)\"],\"MkpcsT\":[\"メッセージと設定はデバイス上にローカルで保存されます\"],\"MzPdC2\":[\"サーバーパスワード (PASS)\"],\"N/hDSy\":[\"ボットとしてマーク — 通常は「on」または空欄\"],\"N6j2JH\":[[\"0\"],\" を編集\"],\"N7TQbE\":[[\"channelName\"],\" にユーザーを招待\"],\"NCca/o\":[\"デフォルトニックネームを入力...\"],\"Nqs6B9\":[\"すべての外部メディアを表示します。URLが不明なサーバーへのリクエストを引き起こす場合があります。\"],\"Nt+9O7\":[\"生のTCPの代わりにWebSocketを使用する\"],\"NxIHzc\":[\"ユーザーを切断\"],\"O+v/cL\":[\"サーバー上のすべてのチャンネルを一覧表示\"],\"OCGpR4\":[\"(継承)\"],\"ODwSCk\":[\"GIFを送信\"],\"OGQ5kK\":[\"通知音とハイライトを設定する\"],\"OIPt1Z\":[\"メンバーリストのサイドバーを表示/非表示にする\"],\"OKSNq/\":[\"とても厳格\"],\"ONWvwQ\":[\"アップロード\"],\"OVKoQO\":[\"認証用のアカウントパスワード\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - IRCを未来へ\"],\"OhCpra\":[\"トピックを設定…\"],\"OkltoQ\":[[\"username\"],\" をニックネームでBANする(同じニックネームでの再参加を防止)\"],\"P+t/Te\":[\"追加データなし\"],\"P42Wcc\":[\"安全\"],\"PD38l0\":[\"チャンネルアバタープレビュー\"],\"PD9mEt\":[\"メッセージを入力...\"],\"PPqfdA\":[\"チャンネル設定を開く\"],\"PSCjfZ\":[\"このチャンネルに表示されるトピックです。すべてのユーザーがトピックを閲覧できます。\"],\"PZCecv\":[\"PDFプレビュー\"],\"PeLgsC\":[[\"c\",\"plural\",{\"other\":[[\"c\"],\"回\"]}]],\"PguS2C\":[\"例外マスクを追加(例:nick!*@*、*!*@host.com)\"],\"Pil5Ty\":[[\"0\"],\" 件中 \",[\"displayedChannelsCount\"],\" 件を表示中\"],\"PqhVlJ\":[\"ユーザーをBAN(ホストマスク)\"],\"Q+chwU\":[\"ユーザー名:\"],\"Q2QY4/\":[\"ãã®æå¾
ãåé¤\"],\"Q3v9Wc\":[\"はい、削除します\"],\"Q6hhn8\":[\"設定\"],\"QF4a34\":[\"ユーザー名を入力してください\"],\"QGqSZ2\":[\"カラーと書式設定\"],\"QJQd1J\":[\"プロフィールを編集\"],\"QSzGDE\":[\"アイドル\"],\"QUlny5\":[[\"0\"],\" へようこそ!\"],\"Qoq+GP\":[\"もっと読む\"],\"QuSkCF\":[\"チャンネルをフィルター...\"],\"QwUrDZ\":[\"トピックを変更しました: \",[\"topic\"]],\"R0UH07\":[[\"1\"],\"枚中\",[\"0\"],\"枚目の画像\"],\"R7SsBE\":[\"ミュート\"],\"R8rf1X\":[\"クリックしてトピックを設定\"],\"RArB3D\":[[\"username\"],\" によって \",[\"channelName\"],\" からキックされました\"],\"RI3cWd\":[\"ObsidianIRCでIRCの世界を探索しよう\"],\"RIfHS5\":[\"æ°ããæå¾
ãªã³ã¯ã使\"],\"RMMaN5\":[\"モデレート制 (+m)\"],\"RWw9Lg\":[\"モーダルを閉じる\"],\"RZ2BuZ\":[[\"account\"],\" のアカウント登録には確認が必要です: \",[\"message\"]],\"RySp6q\":[\"コメントを非表示\"],\"S5Togi\":[\"バウンサーからネットワークを読み込み中…\"],\"SPKQTd\":[\"ニックネームは必須です\"],\"SPVjfj\":[\"空欄の場合は「理由なし」がデフォルトになります\"],\"SQKPvQ\":[\"ユーザーを招待\"],\"STmlpb\":[\"ネットワーク一覧に戻る\"],\"SkZcl+\":[\"定義済みのフラッド保護プロファイルを選択してください。これらのプロファイルは、さまざまなユースケースに対してバランスの取れた保護設定を提供します。\"],\"Slr+3C\":[\"最小ユーザー数\"],\"Spnlre\":[[\"target\"],\" を \",[\"channel\"],\" に招待しました\"],\"T/ckN5\":[\"ビューアで開く\"],\"T91vKp\":[\"再生\"],\"TV2Wdu\":[\"データの取り扱いとプライバシー保護について詳しく見る。\"],\"TgFpwD\":[\"適用中...\"],\"TkzSFB\":[\"変更なし\"],\"TtserG\":[\"本名を入力\"],\"Ttz9J1\":[\"パスワードを入力...\"],\"Tz0i8g\":[\"設定\"],\"U3pytU\":[\"管理者\"],\"UDb2YD\":[\"リアクション\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"ã¾ã\xA0æå¾
ãªã³ã¯ã使ãã¦ãã¾ãããä¸ã®ãã©ã¼ã\xA0ããæåã®æå¾
ãªã³ã¯ã使ãã¦ãã\xA0ããã\"],\"UGT5vp\":[\"設定を保存\"],\"UV5hLB\":[\"BANが見つかりません\"],\"Uaj3Nd\":[\"ステータスメッセージ\"],\"Ue3uny\":[\"デフォルト(プロファイルなし)\"],\"UkARhe\":[\"通常 — 標準的な保護\"],\"Umn7Cj\":[\"まだコメントはありません。最初のコメントを投稿しましょう!\"],\"UtUIRh\":[[\"0\"],\" 件の古いメッセージ\"],\"UwzP+U\":[\"セキュア接続\"],\"V0/A4O\":[\"チャンネルオーナー\"],\"V0zZWc\":[\"以下のバインドされたネットワークも閉じられます。\"],\"V4qgxE\":[\"作成日時(以前、分前)\"],\"V8yTm6\":[\"検索をクリア\"],\"VJMMyz\":[\"ObsidianIRC — IRCを未来へ\"],\"VJScHU\":[\"理由\"],\"VLsmVV\":[\"通知をミュート\"],\"VbyRUy\":[\"コメント\"],\"Vmx0mQ\":[\"設定者:\"],\"VqnIZz\":[\"プライバシーポリシーとデータ取り扱い方針を見る\"],\"VrMygG\":[\"最小文字数は \",[\"0\"],\" 文字です\"],\"VrnTui\":[\"プロフィールに表示される代名詞\"],\"W8E3qn\":[\"認証済みアカウント\"],\"WAakm9\":[\"チャンネルを削除\"],\"WFxTHC\":[\"BANマスクを追加(例:nick!*@*、*!*@host.com)\"],\"WN1g9F\":[\"サーバーホストは必須です\"],\"WRYdXW\":[\"音声の再生位置\"],\"WUOH5B\":[\"ユーザーを無視\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"other\":[\"さらに \",[\"1\"],\" 件表示\"]}]],\"WYxRzo\":[\"æå¾
ãªã³ã¯ã使ã»ç®¡çãã\"],\"Wd38W1\":[\"ãã£ã³ãã«ã空æ¬ã«ããã¨ãæ±ç¨ã®ãããã¯ã¼ã¯æå¾
ã«ãªãã¾ãã説æã¯ããªãå°ç¨ã®è¨é²ã§ããã®ãªã¹ãã§èªåã«ã®ã¿è¡¨ç¤ºããã¾ãã\"],\"Weq9zb\":[\"一般\"],\"Wfj7Sk\":[\"通知音をミュートまたはミュート解除する\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"ユーザープロフィール\"],\"X6S3lt\":[\"設定、チャンネル、サーバーを検索...\"],\"XEHan5\":[\"このまま続行\"],\"XI1+wb\":[\"無効な形式\"],\"XIXeuC\":[\"@\",[\"0\"],\" へメッセージ\"],\"XMS+k4\":[\"プライベートメッセージを開始\"],\"XWgxXq\":[\"アルバム\"],\"Xd7+IT\":[\"プライベートチャットのピン留めを解除\"],\"Xm/s+u\":[\"表示\"],\"Xp2n93\":[\"サーバーの信頼済みファイルホストからのメディアを表示します。外部サービスへのリクエストは発生しません。\"],\"XvjC4F\":[\"保存中...\"],\"Y/qryO\":[\"検索に一致するユーザーが見つかりません\"],\"YAqRpI\":[[\"account\"],\" のアカウント登録に成功しました: \",[\"message\"]],\"YEfzvP\":[\"トピック保護 (+t)\"],\"YQOn6a\":[\"メンバーリストを折りたたむ\"],\"YRCoE9\":[\"チャンネルオペレーター\"],\"YURQaF\":[\"プロフィールを表示\"],\"YdBSvr\":[\"メディア表示と外部コンテンツを制御する\"],\"Yj6U3V\":[\"中央サーバーなし:\"],\"YjvpGx\":[\"代名詞\"],\"YqH4l4\":[\"キーなし\"],\"YyUPpV\":[\"アカウント:\"],\"ZJSWfw\":[\"サーバーから切断したときに表示されるメッセージ\"],\"ZR1dJ4\":[\"招待\"],\"ZdWg0V\":[\"ブラウザで開く\"],\"ZhRBbl\":[\"メッセージを検索…\"],\"Zmcu3y\":[\"詳細フィルター\"],\"a0bHay\":[\"<0>\",[\"0\"],\"0> を切断しますか?\"],\"a2/8e5\":[\"トピック設定日時(以降、分前)\"],\"aHKcKc\":[\"前のページ\"],\"aJTbXX\":[\"Operパスワード\"],\"aQryQv\":[\"パターンはすでに存在します\"],\"aW9pLN\":[\"チャンネルに参加できる最大ユーザー数。制限なしの場合は空欄にしてください。\"],\"ah4fmZ\":[\"YouTube、Vimeo、SoundCloudなどの既知のサービスのプレビューも表示します。\"],\"aifXak\":[\"このチャンネルにはメディアがありません\"],\"ap2zBz\":[\"緩め\"],\"az8lvo\":[\"オフ\"],\"azXSNo\":[\"メンバーリストを展開\"],\"azdliB\":[\"アカウントにログイン\"],\"b26wlF\":[\"彼女/彼女の\"],\"bD/+Ei\":[\"厳格\"],\"bQ6BJn\":[\"詳細なフラッド保護ルールを設定します。各ルールは、監視するアクティビティの種類と、しきい値を超えた場合に実行するアクションを指定します。\"],\"beV7+y\":[\"ユーザーは \",[\"channelName\"],\" への参加招待を受け取ります。\"],\"bk84cH\":[\"離席メッセージ\"],\"bkHdLj\":[\"IRCサーバーを追加\"],\"bmQLn5\":[\"ルールを追加\"],\"bv4cFj\":[\"トランスポート\"],\"bwRvnp\":[\"アクション\"],\"c8+EVZ\":[\"認証済みアカウント\"],\"cGYUlD\":[\"メディアプレビューは読み込まれません。\"],\"cLF98o\":[\"コメントを表示 (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"利用可能なユーザーがいません\"],\"cSgpoS\":[\"プライベートチャットをピン留め\"],\"cXeEKu\":[\"以下の \",[\"0\"],\" 個のバインドされたネットワークも閉じられます。\"],\"cde3ce\":[\"<0>\",[\"0\"],\"0> へメッセージ\"],\"chQsxg\":[\"フォーマット済み出力をコピー\"],\"cl/A5J\":[[\"__DEFAULT_IRC_SERVER_NAME__\"],\" へようこそ!\"],\"cnGeoo\":[\"削除\"],\"coPLXT\":[\"IRC通信はサーバーに保存されません\"],\"crYH/6\":[\"SoundCloudプレーヤー\"],\"cv5DQb\":[\"ホスト未設定\"],\"d3sis4\":[\"サーバーを追加\"],\"d9aN5k\":[[\"username\"],\" をチャンネルから削除する\"],\"dEgA5A\":[\"キャンセル\"],\"dGi1We\":[\"このプライベートメッセージの会話のピン留めを解除する\"],\"dJVuyC\":[[\"channelName\"],\" を退出しました (\",[\"reason\"],\")\"],\"dMtLDE\":[\"宛て\"],\"dXqxlh\":[\"<0>⚠️ セキュリティリスク!0> この接続は傍受や中間者攻撃に対して脆弱な可能性があります。\"],\"da9Q/R\":[\"チャンネルモードを変更しました\"],\"dhJN3N\":[\"コメントを表示\"],\"dj2xTE\":[\"通知を閉じる\"],\"dpCzmC\":[\"フラッド保護設定\"],\"e9dQpT\":[\"このリンクを新しいタブで開きますか?\"],\"ePK91l\":[\"編集\"],\"eYBDuB\":[\"画像をアップロードするか、動的サイズ変換のための \",[\"size\"],\" 置換を含むURLを入力してください\"],\"edBbee\":[[\"username\"],\" をホストマスクでBANする(同じIP/ホストからの再参加を防止)\"],\"ekfzWq\":[\"ユーザー設定\"],\"elPDWs\":[\"IRCクライアントの使い心地をカスタマイズする\"],\"eu2osY\":[\"<0>💡 推奨事項:0> このサーバーを信頼し、リスクを理解している場合のみ続行してください。この接続で機密情報やパスワードを共有しないようにしてください。\"],\"euEhbr\":[\"クリックして \",[\"channel\"],\" に参加\"],\"ez3vLd\":[\"複数行入力を有効にする\"],\"f0J5Ki\":[\"サーバー間通信に暗号化されていない接続が使用される可能性があります\"],\"f9BHJk\":[\"ユーザーに警告\"],\"fDOLLd\":[\"チャンネルが見つかりません。\"],\"ffzDkB\":[\"匿名分析:\"],\"fq1GF9\":[\"ユーザーがサーバーから切断したときに表示\"],\"gCldcN\":[\"アクセントカラーを変更\"],\"gEF57C\":[\"このサーバーは1種類の接続タイプのみサポートしています\"],\"gJuLUI\":[\"無視リスト\"],\"gNzMrk\":[\"現在のアバター\"],\"gjPWyO\":[\"ニックネームを入力...\"],\"gz6UQ3\":[\"最大化\"],\"h6/IMX\":[\"最初のネットワークを追加\"],\"h6razj\":[\"チャンネル名マスクを除外\"],\"hG6jnw\":[\"トピックが設定されていません\"],\"hG89Ed\":[\"画像\"],\"hYgDIe\":[\"使\"],\"hZ6znB\":[\"ポート\"],\"ha+Bz5\":[\"例:100:1440\"],\"he3ygx\":[\"ã³ãã¼\"],\"hehnjM\":[\"数量\"],\"hzdLuQ\":[\"Voice以上の権限を持つユーザーのみ発言できます\"],\"i0qMbr\":[\"ホーム\"],\"iDNBZe\":[\"通知\"],\"iH8pgl\":[\"戻る\"],\"iL9SZg\":[\"ユーザーをBAN(ニックネーム)\"],\"iNt+3c\":[\"画像に戻る\"],\"iQvi+a\":[\"このサーバーのリンクセキュリティが低い場合に警告しない\"],\"iSLIjg\":[\"接続\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"サーバーホスト\"],\"idD8Ev\":[\"保存済み\"],\"iivqkW\":[\"サインイン日時\"],\"ij+Elv\":[\"画像プレビュー\"],\"ilIWp7\":[\"通知を切り替え\"],\"iuaqvB\":[\"ワイルドカードには * を使用してください。例:baduser!*@*、*!*@spammer.com、troll*!*@*\"],\"ixkTse\":[\"ボット\"],\"j2DGR0\":[\"ホストマスクでBAN\"],\"jA4uoI\":[\"トピック:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"理由(任意)\"],\"jUV7CU\":[\"アバターをアップロード\"],\"jW5Uwh\":[\"外部メディアの読み込み範囲を制御します。オフ/安全/信頼できるソース/すべてのコンテンツ。\"],\"jXzms5\":[\"添付オプション\"],\"jZlrte\":[\"カラー\"],\"jfC/xh\":[\"連絡先\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"古いメッセージを読み込む\"],\"k3ID0F\":[\"メンバーをフィルター…\"],\"k65gsE\":[\"詳細を見る\"],\"k7Zgob\":[\"接続をキャンセル\"],\"kAVx5h\":[\"招待が見つかりません\"],\"kCLEPU\":[\"接続先\"],\"kF5LKb\":[\"無視パターン:\"],\"kGeOx/\":[[\"0\"],\" に参加\"],\"kITKr8\":[\"チャンネルモードを読み込み中...\"],\"kPpPsw\":[\"あなたはIRC Operatorです\"],\"kWJmRL\":[\"あなた\"],\"kfcRb0\":[\"アバター\"],\"kjMqSj\":[\"JSONをコピー\"],\"krViRy\":[\"JSONとしてコピーするにはクリック\"],\"ks71ra\":[\"例外\"],\"kw4lRv\":[\"チャンネルハーフオペレーター\"],\"kxgIRq\":[\"チャンネルを選択または追加して始めましょう。\"],\"ky6dWe\":[\"アバタープレビュー\"],\"l+GxCv\":[\"チャンネルを読み込み中...\"],\"l+IUVW\":[[\"account\"],\" のアカウント確認に成功しました: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"other\":[[\"reconnectCount\"],\"回 再接続した\"]}]],\"l1l8sj\":[[\"0\"],\"æ¥å\"],\"l5NhnV\":[\"#ãã£ã³ãã«ï¼ä»»æï¼\"],\"l5jmzx\":[[\"0\"],\" と \",[\"1\"],\" が入力中...\"],\"lCF0wC\":[\"æ´æ°\"],\"lHy8N5\":[\"さらにチャンネルを読み込み中...\"],\"lasgrr\":[\"ä½¿ç¨æ¸ã¿\"],\"lbpf14\":[[\"value\"],\"に参加\"],\"lfFsZ4\":[\"チャンネル\"],\"lkNdiH\":[\"アカウント名\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"画像をアップロード\"],\"loQxaJ\":[\"戻りました\"],\"lvfaxv\":[\"ホーム\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"追加\"],\"m8flAk\":[\"プレビュー(未アップロード)\"],\"mEPxTp\":[\"<0>⚠️ 注意!0> 信頼できる送信元のリンクのみ開いてください。悪意のあるリンクはセキュリティやプライバシーを侵害する恐れがあります。\"],\"mHGdhG\":[\"サーバー情報\"],\"mHS8lb\":[\"#\",[\"0\"],\" へメッセージ\"],\"mMYBD9\":[\"広め — より広範な保護範囲\"],\"mTGsPd\":[\"チャンネルトピック\"],\"mU8j6O\":[\"外部メッセージ禁止 (+n)\"],\"mZp8FL\":[\"自動的に1行入力に切り替え\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"コメント (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"ユーザーは認証済みです\"],\"mwtcGl\":[\"コメントを閉じる\"],\"myL0MR\":[\"このネットワークを削除しますか?\"],\"mzI/c+\":[\"ダウンロード\"],\"n3fGRk\":[[\"0\"],\" が設定\"],\"n5+j9l\":[\"例: <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"緩め — 控えめな保護\"],\"nNflMD\":[\"チャンネルを退出\"],\"nPXkBi\":[\"WHOISデータを読み込み中...\"],\"nQnxxF\":[\"#\",[\"0\"],\" へメッセージ(Shift+Enterで改行)\"],\"nWMRxa\":[\"ピン留めを解除\"],\"nkC032\":[\"フラッドプロファイルなし\"],\"o69z4d\":[[\"username\"],\" に警告メッセージを送る\"],\"o9ylQi\":[\"GIFを検索して始めましょう\"],\"oFGkER\":[\"サーバー通知\"],\"oOi11l\":[\"最下部にスクロール\"],\"oPYIL5\":[\"ãããã¯ã¼ã¯\"],\"oQEzQR\":[\"新しいDM\"],\"oXOSPE\":[\"オンライン\"],\"oal760\":[\"サーバーリンクへの中間者攻撃が可能な状態です\"],\"oeqmmJ\":[\"信頼できるソース\"],\"optX0N\":[[\"0\"],\"æéå\"],\"ovBPCi\":[\"デフォルト\"],\"p0Z69r\":[\"パターンを空にすることはできません\"],\"p1KgtK\":[\"音声の読み込みに失敗しました\"],\"p59pEv\":[\"詳細情報\"],\"p7sRI6\":[\"入力中であることを他のユーザーに知らせる\"],\"pBm1od\":[\"シークレットチャンネル\"],\"pNmiXx\":[\"すべてのサーバーで使用するデフォルトのニックネーム\"],\"pUUo9G\":[\"ホスト名:\"],\"pVGPmz\":[\"アカウントパスワード\"],\"peNE68\":[\"永続的\"],\"plhHQt\":[\"データなし\"],\"pm6+q5\":[\"セキュリティ警告\"],\"pn5qSs\":[\"追加情報\"],\"q0cR4S\":[\"は現在 **\",[\"newNick\"],\"** として知られています\"],\"qFcunY\":[\"LIST または NAMES コマンドにチャンネルが表示されません\"],\"qLpTm/\":[\"リアクション \",[\"emoji\"],\" を削除\"],\"qVkGWK\":[\"ピン留め\"],\"qY8wNa\":[\"ホームページ\"],\"qb0xJ7\":[\"ワイルドカードを使用してください:* は任意の文字列、? は任意の1文字に一致します。例:nick!*@*、*!*@host.com、*!*user@*\"],\"qhzpRq\":[\"チャンネルキー (+k)\"],\"qtoOYG\":[\"制限なし\"],\"r1W2AS\":[\"ファイルホスト画像\"],\"rIPR2O\":[\"トピック設定日時(以前、分前)\"],\"rMMSYo\":[\"最大文字数は \",[\"0\"],\" 文字です\"],\"rWtzQe\":[\"ネットワークが分割され、再接続されました。✅\"],\"rYG2u6\":[\"しばらくお待ちください...\"],\"rdUucN\":[\"プレビュー\"],\"rjGI/Q\":[\"プライバシー\"],\"rk8iDX\":[\"GIFを読み込み中...\"],\"rn6SBY\":[\"ミュート解除\"],\"s/UKqq\":[\"チャンネルからキックされました\"],\"s7oqXR\":[\"ネットワークを選択\"],\"s8cATI\":[[\"channelName\"],\" に参加しました\"],\"sCO9ue\":[\"<0>\",[\"serverName\"],\"0> への接続には次のセキュリティ上の懸念事項があります:\"],\"sGH11W\":[\"サーバー\"],\"sHI1H+\":[\"は現在 **\",[\"newNick\"],\"** として知られています\"],\"sJyV04\":[[\"inviter\"],\" があなたを \",[\"channel\"],\" に招待しました\"],\"sUBSbK\":[\"アップストリームネットワークがまだありません。\"],\"sby+1/\":[\"クリックしてコピー\"],\"sfN25C\":[\"本名またはフルネーム\"],\"sliuzR\":[\"リンクを開く\"],\"sqrO9R\":[\"カスタムメンション\"],\"sr6RdJ\":[\"Shift+Enterで複数行入力\"],\"swrCpB\":[[\"user\"],\" がチャンネルを \",[\"oldName\"],\" から \",[\"newName\"],\" に変更しました\",[\"0\"]],\"sxkWRg\":[\"詳細設定\"],\"t/YqKh\":[\"削除\"],\"t47eHD\":[\"このサーバーでの固有識別子\"],\"tAkAh0\":[\"動的サイズ変換のための \",[\"size\"],\" 置換を含むURL。例:https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"チャンネルリストのサイドバーを表示/非表示にする\"],\"tfDRzk\":[\"保存\"],\"tiBsJk\":[[\"channelName\"],\" を退出しました\"],\"tt4/UD\":[\"退出しました (\",[\"reason\"],\")\"],\"u0TcnO\":[\"ニックネーム {nick} は既に使用中です。{newNick} で再試行します\"],\"u0a8B4\":[\"管理者アクセスのためにIRC Operatorとして認証する\"],\"u0rWFU\":[\"作成日時(以降、分前)\"],\"u72w3t\":[\"無視するユーザーとパターン\"],\"u7jc2L\":[\"退出しました\"],\"uAQUqI\":[\"ステータス\"],\"uB85T3\":[\"保存失敗:\",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRCサーバー:\"],\"ukyW4o\":[\"ããªãã®æå¾
ãªã³ã¯\"],\"usSSr/\":[\"ズームレベル\"],\"v7uvcf\":[\"ソフトウェア:\"],\"vE8kb+\":[\"Shift+Enterで改行(Enterで送信)\"],\"vERlcd\":[\"プロフィール\"],\"vK0RL8\":[\"トピックなし\"],\"vSJd18\":[\"動画\"],\"vXIe7J\":[\"言語\"],\"vaHYxN\":[\"本名\"],\"vhjbKr\":[\"離席中\"],\"w/nogd\":[[\"0\"],\" ネットワーク\",[\"1\"],\" — 参加するものを選択\"],\"w4NYox\":[[\"title\"],\" クライアント\"],\"w8xQRx\":[\"無効な値\"],\"wFjjxZ\":[[\"username\"],\" によって \",[\"channelName\"],\" からキックされました (\",[\"reason\"],\")\"],\"wGjaGl\":[\"BAN例外が見つかりません\"],\"wPrGnM\":[\"チャンネル管理者\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"ユーザーがチャンネルに参加・退出したときに表示する\"],\"whqZ9r\":[\"ハイライトする追加の単語またはフレーズ\"],\"wm7RV4\":[\"通知音\"],\"wz/Yoq\":[\"サーバー間で中継される際にメッセージが傍受される可能性があります\"],\"x3+y8b\":[\"ãã®ãªã³ã¯ããç»é²ãã人æ°\"],\"xCJdfg\":[\"クリア\"],\"xOTzt5\":[\"ãã£ãä»\"],\"xUHRTR\":[\"接続時に自動的にOperatorとして認証する\"],\"xWHwwQ\":[\"BAN一覧\"],\"xYilR2\":[\"メディア\"],\"xbi8D6\":[\"ãã®ãµã¼ãã¼ã¯æå¾
ãªã³ã¯ã«å¯¾å¿ãã¦ãã¾ããï¼<0>obby.world/invitation0>ã±ã¤ãããªãã£ãåç¥ããã¦ãã¾ããï¼ãé常éããã£ããã¯å©ç¨ã§ãã¾ãããã®ããã«ã¯obbyircdãã¼ã¹ã®ãããã¯ã¼ã¯ç¨ã§ãã\"],\"xceQrO\":[\"安全なWebSocketのみサポートされています\"],\"xdtXa+\":[\"チャンネル名\"],\"xfXC7q\":[\"テキストチャンネル\"],\"xlCYOE\":[\"メッセージを取得中...\"],\"xlhswE\":[\"最小値は \",[\"0\"],\" です\"],\"xq97Ci\":[\"単語またはフレーズを追加...\"],\"xuRqRq\":[\"クライアント制限 (+l)\"],\"xwF+7J\":[[\"0\"],\" が入力中...\"],\"y1eoq1\":[\"ãªã³ã¯ãã³ãã¼\"],\"yJztBY\":[\"ネットワークを削除\"],\"yNeucF\":[\"このサーバーは拡張プロフィールメタデータ(IRCv3 METADATA拡張)をサポートしていません。アバター、表示名、ステータスなどの追加フィールドは利用できません。\"],\"yPlrca\":[\"チャンネルアバター\"],\"yQE2r9\":[\"読み込み中\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Operユーザー名\"],\"yYOzWD\":[\"ログ\"],\"yfx9Re\":[\"IRC Operatorパスワード\"],\"ygCKqB\":[\"停止\"],\"ymDxJx\":[\"IRC Operatorユーザー名\"],\"yrpRsQ\":[\"名前順で並び替え\"],\"yz7wBu\":[\"閉じる\"],\"zJw+jA\":[\"モードを設定: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"åå\"],\"zebeLu\":[\"Operユーザー名を入力\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/ja/messages.po b/src/locales/ja/messages.po
index 20ddb2bd..5be429c5 100644
--- a/src/locales/ja/messages.po
+++ b/src/locales/ja/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - IRCを未来へ"
msgid "— open in viewer"
msgstr "— ビューアで開く"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(継承)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(変更なし)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} と {1} が入力中..."
msgid "{0} is typing..."
msgstr "{0} が入力中..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} ネットワーク{1} — 参加するものを選択"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -69,17 +85,17 @@ msgstr "{0}、{1}、{2} と他 {3} 人が入力中..."
#. placeholder {0}: Math.floor(secs / 86400)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}d ago"
-msgstr "{0}日前"
+msgstr "{0}æ¥å"
#. placeholder {0}: Math.floor(secs / 3600)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}h ago"
-msgstr "{0}時間前"
+msgstr "{0}æéå"
#. placeholder {0}: Math.floor(secs / 60)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}m ago"
-msgstr "{0}分前"
+msgstr "{0}åå"
#: src/lib/eventGrouping.ts
msgid "{c, plural, one {1 time} other {{c} times}}"
@@ -115,7 +131,7 @@ msgstr "*spam*"
#: src/components/ui/InvitationsPanel.tsx
msgid "#channel (optional)"
-msgstr "#チャンネル(任意)"
+msgstr "#ãã£ã³ãã«ï¼ä»»æï¼"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "#new-channel-name"
@@ -205,6 +221,12 @@ msgstr "招待マスクを追加(例:nick!*@*、*!*@host.com)"
msgid "Add IRC Server"
msgstr "IRCサーバーを追加"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "ネットワークを追加"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "ルールを追加"
msgid "Add Server"
msgstr "サーバーを追加"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "最初のネットワークを追加"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "詳細情報"
@@ -377,6 +403,10 @@ msgstr "戻る"
msgid "Back to image"
msgstr "画像に戻る"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "ネットワーク一覧に戻る"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "{username} をホストマスクでBANする(同じIP/ホストからの再参加を防止)"
@@ -424,6 +454,10 @@ msgstr "サーバー上のすべてのチャンネルを一覧表示"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "接続をキャンセル"
msgid "Cancel reply"
msgstr "返信をキャンセル"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "アクセントカラーを変更"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "チャンネル名を変更する(オペレーターのみ)"
@@ -564,7 +603,7 @@ msgstr "検索をクリア"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "もう一度クリックして確認"
+msgstr "ããä¸åº¦ã¯ãªãã¯ãã¦ç¢ºèª"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "クライアント制限 (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -666,9 +707,10 @@ msgstr "通知音とハイライトを設定する"
#: src/components/ui/InvitationsPanel.tsx
msgid "Confirm?"
-msgstr "確認しますか?"
+msgstr "確èªãã¾ããï¼"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "接続"
@@ -711,11 +753,11 @@ msgstr "コピーしました"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy"
-msgstr "コピー"
+msgstr "ã³ãã¼"
#: src/components/ui/RawLogViewer.tsx
msgid "Copy all"
-msgstr "すべてコピー"
+msgstr "ãã¹ã¦ã³ãã¼"
#: src/components/message/JsonLogMessage.tsx
msgid "Copy entire JSON"
@@ -731,7 +773,7 @@ msgstr "JSONをコピー"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy link"
-msgstr "リンクをコピー"
+msgstr "ãªã³ã¯ãã³ãã¼"
#: src/components/ui/ExternalLinkWarningModal.tsx
msgid "Copy URL"
@@ -739,15 +781,15 @@ msgstr "URLをコピー"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create"
-msgstr "作成"
+msgstr "使"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "新しい招待リンクを作成"
+msgstr "æ°ããæå¾
ãªã³ã¯ã使"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "招待リンクを作成・管理する"
+msgstr "æå¾
ãªã³ã¯ã使ã»ç®¡çãã"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,27 +856,52 @@ msgstr "チャンネルを削除"
msgid "Delete message"
msgstr "メッセージを削除"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "ネットワークを削除"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "プライベートチャットを削除"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "この招待を削除"
+msgstr "ãã®æå¾
ãåé¤"
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "このメッセージを削除しますか?この操作は元に戻せません。"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "このネットワークを削除しますか?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "説明(任意、例:「Q3ベータテスター」)"
+msgstr "説æï¼ä»»æãä¾ï¼ãQ3ãã¼ã¿ãã¹ã¿ã¼ãï¼"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "切断"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "<0>{0}0> を切断しますか?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "soju バウンサーから切断しますか?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "ネットワークを切断しますか?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "探す"
@@ -891,20 +958,31 @@ msgstr "ダウンロード"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "ファイルをドロップしてアップロード"
+msgstr "ãã¡ã¤ã«ããããããã¦ã¢ãããã¼ã"
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "例: <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "例:100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "編集"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "{0} を編集"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "プロフィールを編集"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "ホーム"
msgid "Homepage"
msgstr "ホームページ"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "ホスト"
@@ -1285,7 +1364,7 @@ msgstr "チャンネルに参加しました"
#: src/components/ui/InvitationsPanel.tsx
msgid "just now"
-msgstr "たった今"
+msgstr "ãã£ãä»"
#: src/components/ui/ModerationModal.tsx
#: src/components/ui/UserContextMenu.tsx
@@ -1323,7 +1402,7 @@ msgstr "チャンネルを退出"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "チャンネルを空欄にすると、汎用のネットワーク招待になります。説明はあなた専用の記録で、このリストで自分にのみ表示されます。"
+msgstr "ãã£ã³ãã«ã空æ¬ã«ããã¨ãæ±ç¨ã®ãããã¯ã¼ã¯æå¾
ã«ãªãã¾ãã説æã¯ããªãå°ç¨ã®è¨é²ã§ããã®ãªã¹ãã§èªåã«ã®ã¿è¡¨ç¤ºããã¾ãã"
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "チャンネルを退出しました"
msgid "Let others know when you are typing"
msgstr "入力中であることを他のユーザーに知らせる"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "リンクプレビュー"
@@ -1375,6 +1458,10 @@ msgstr "GIFを読み込み中..."
msgid "Loading more channels..."
msgstr "さらにチャンネルを読み込み中..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "バウンサーからネットワークを読み込み中…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "WHOISデータを読み込み中..."
@@ -1563,12 +1650,23 @@ msgstr "名前:"
#: src/components/ui/InvitationsPanel.tsx
msgid "network"
-msgstr "ネットワーク"
+msgstr "ãããã¯ã¼ã¯"
+
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "soju バウンサー経由でバインドされたネットワーク"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "ネットワーク名"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "{0} のネットワーク"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "新しいDM"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host(例:spam*!*@*、*!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "ファイルが選択されていません"
msgid "No flood profile"
msgstr "フラッドプロファイルなし"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "ホスト未設定"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "招待が見つかりません"
@@ -1672,7 +1775,7 @@ msgstr "メディアプレビューは読み込まれません。"
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "まだ生のIRCトラフィックを取得していません。接続するかメッセージを送信してください。"
+msgstr "ã¾ã çã®IRCãã©ãã£ãã¯ãåå¾ãã¦ãã¾ãããæ¥ç¶ãããã¡ãã»ã¼ã¸ãéä¿¡ãã¦ãã ããã"
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "結果が見つかりません"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "サーバーが選択されていません。まずサイドバーからサーバーを選択してください。招待リンクはサーバーごとに管理されます。"
+msgstr "ãµã¼ãã¼ã鏿ããã¦ãã¾ãããã¾ããµã¤ããã¼ãããµã¼ãã¼ã鏿ãã¦ãã ãããæå¾
ãªã³ã¯ã¯ãµã¼ãã¼ãã¨ã«ç®¡çããã¾ãã"
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "トピックが設定されていません"
msgid "No unread mentions or messages"
msgstr "未読のメンションやメッセージはありません"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "アップストリームネットワークがまだありません。"
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "利用可能なユーザーがいません"
@@ -1784,6 +1891,10 @@ msgstr "おっと!ネットワーク分割が発生しました!⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "開く"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "チャンネル設定を開く"
@@ -1887,6 +1998,10 @@ msgstr "プライベートチャットをピン留め"
msgid "Pin this private message conversation"
msgstr "このプライベートメッセージの会話をピン留めする"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "平文"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "DMを送る"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "ポート"
@@ -2006,6 +2122,7 @@ msgstr "このメッセージにリアクションしました"
msgid "Read more"
msgstr "もっと読む"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "理由"
msgid "Reason (optional)"
msgstr "理由(任意)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "サーバーに再接続"
@@ -2031,7 +2149,7 @@ msgstr "サーバーに再接続"
#: src/components/ui/InvitationsPanel.tsx
#: src/components/ui/InvitationsPanel.tsx
msgid "Refresh"
-msgstr "更新"
+msgstr "æ´æ°"
#: src/components/ui/AddServerModal.tsx
msgid "Register for an account"
@@ -2095,6 +2213,7 @@ msgstr "ルール"
msgid "Safe"
msgstr "安全"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "シーク"
msgid "Select a channel"
msgstr "チャンネルを選択"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "ネットワークを選択"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "メンバーを選択"
@@ -2276,6 +2399,10 @@ msgstr "ネットワーク上のサーバーオペレーターがメッセージ
msgid "Server Password"
msgstr "サーバーパスワード"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "サーバーパスワード (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "サーバー間通信に暗号化されていない接続が使用される可能性があります"
@@ -2380,6 +2507,12 @@ msgstr "サインイン日時"
msgid "Software:"
msgstr "ソフトウェア:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "soju バウンサー(制御)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "名前順で並び替え"
@@ -2457,7 +2590,11 @@ msgstr "この画像の有効期限が切れています"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "このリンクから登録した人数"
+msgstr "ãã®ãªã³ã¯ããç»é²ãã人æ°"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "これにより、ネットワークが soju バウンサーから削除されます。再度使用するには、再追加する必要があります。"
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "このサーバーは拡張プロフィールメタデータ(IRCv3 MET
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "このサーバーは招待リンクに対応していません(<0>obby.world/invitation0>ケイパビリティが告知されていません)。通常通りチャットは利用できます。このパネルはobbyircdベースのネットワーク用です。"
+msgstr "ãã®ãµã¼ãã¼ã¯æå¾
ãªã³ã¯ã«å¯¾å¿ãã¦ãã¾ããï¼<0>obby.world/invitation0>ã±ã¤ãããªãã£ãåç¥ããã¦ãã¾ããï¼ãé常éããã£ããã¯å©ç¨ã§ãã¾ãããã®ããã«ã¯obbyircdãã¼ã¹ã®ãããã¯ã¼ã¯ç¨ã§ãã"
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "このサーバーは1種類の接続タイプのみサポートしています"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "以下の {0} 個のバインドされたネットワークも閉じられます。"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "以下のバインドされたネットワークも閉じられます。"
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "時間(分)"
@@ -2479,6 +2625,10 @@ msgstr "時間(分)"
msgid "Time Window (seconds)"
msgstr "時間ウィンドウ(秒)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "トピック:"
msgid "Total: {0}"
msgstr "合計:{0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "トランスポート"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "信頼できるソース"
@@ -2615,7 +2769,7 @@ msgstr "ワイルドカードを使用してください:* は任意の文字
#: src/components/ui/InvitationsPanel.tsx
msgid "used"
-msgstr "使用済み"
+msgstr "ä½¿ç¨æ¸ã¿"
#: src/components/message/JsonLogMessage.tsx
#: src/components/ui/UserProfileModal.tsx
@@ -2641,6 +2795,7 @@ msgstr "ユーザープロフィール"
msgid "User Settings"
msgstr "ユーザー設定"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "広め — より広範な保護範囲"
msgid "Will default to 'no reason' if left empty"
msgstr "空欄の場合は「理由なし」がデフォルトになります"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "はい、削除します"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "保存されていない変更があります。保存せずに閉じて
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "まだ招待リンクを作成していません。上のフォームから最初の招待リンクを作成してください。"
+msgstr "ã¾ã æå¾
ãªã³ã¯ã使ãã¦ãã¾ãããä¸ã®ãã©ã¼ã ããæåã®æå¾
ãªã³ã¯ã使ãã¦ãã ããã"
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "{target} を {channel} に招待しました"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "<0>{0}0> に接続しています。"
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "認証用のアカウントパスワード"
@@ -2822,13 +2986,17 @@ msgstr "認証用のアカウントパスワード"
msgid "Your account username for authentication"
msgstr "認証用のアカウントユーザー名"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "バウンサーにはまだネットワークがありません。最初のネットワークを追加して始めましょう。"
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "すべてのサーバーで使用するデフォルトのニックネーム"
#: src/components/ui/InvitationsPanel.tsx
msgid "Your invite links"
-msgstr "あなたの招待リンク"
+msgstr "ããªãã®æå¾
ãªã³ã¯"
#: src/components/ui/UserSettings.tsx
msgid "Your messages and settings are stored locally on your device"
diff --git a/src/locales/ko/messages.mjs b/src/locales/ko/messages.mjs
index 067e3cdb..fefb60bf 100644
--- a/src/locales/ko/messages.mjs
+++ b/src/locales/ko/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"잘못된 패턴 형식입니다. nick!user@host 형식을 사용하세요 (와일드카드 * 허용)\"],\"+6NQQA\":[\"일반 지원 채널\"],\"+6NyRG\":[\"클라이언트\"],\"+K0AvT\":[\"연결 끊기\"],\"+cyFdH\":[\"자리 비움 설정 시 기본 메시지\"],\"+mVPqU\":[\"메시지에서 마크다운 서식 렌더링\"],\"+vqCJH\":[\"인증을 위한 계정 사용자 이름\"],\"+yPBXI\":[\"파일 선택\"],\"+zy2Nq\":[\"유형\"],\"/09cao\":[\"낮은 링크 보안 (레벨 \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"채널 외부 사용자는 메시지를 보낼 수 없습니다\"],\"/4C8U0\":[\"모두 복사\"],\"/6BzZF\":[\"멤버 목록 전환\"],\"/AkXyp\":[\"확인하시겠습니까?\"],\"/TNOPk\":[\"자리 비움 중\"],\"/XQgft\":[\"채널 탐색\"],\"/cF7Rs\":[\"볼륨\"],\"/dqduX\":[\"다음 페이지\"],\"/fc3q4\":[\"모든 콘텐츠\"],\"/kISDh\":[\"알림 소리 활성화\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"오디오\"],\"/rfkZe\":[\"멘션 및 메시지에 소리 재생\"],\"0/0ZGA\":[\"채널 이름 마스크\"],\"0D6j7U\":[\"사용자 정의 규칙에 대해 더 알아보기 →\"],\"0XsHcR\":[\"사용자 추방\"],\"0ZpE//\":[\"사용자 수순 정렬\"],\"0bEPwz\":[\"자리 비움 설정\"],\"0dGkPt\":[\"채널 목록 펼치기\"],\"0gS7M5\":[\"표시 이름\"],\"0kS+M8\":[\"예시네트워크\"],\"0rgoY7\":[\"직접 선택한 서버에만 연결합니다\"],\"0wdd7X\":[\"참여\"],\"0wkVYx\":[\"비공개 메시지\"],\"111uHX\":[\"링크 미리보기\"],\"196EG4\":[\"비공개 채팅 삭제\"],\"1DSr1i\":[\"계정 등록\"],\"1O/24y\":[\"채널 목록 전환\"],\"1VPJJ2\":[\"외부 링크 경고\"],\"1ZC/dv\":[\"읽지 않은 멘션이나 메시지가 없습니다\"],\"1pO1zi\":[\"서버 이름은 필수입니다\"],\"1uwfzQ\":[\"채널 주제 보기\"],\"268g7c\":[\"표시 이름 입력\"],\"2F9+AZ\":[\"아직 캡처된 IRC 원시 트래픽이 없습니다. 연결하거나 메시지를 보내보세요.\"],\"2FOFq1\":[\"네트워크 서버 운영자가 메시지를 읽을 수 있습니다\"],\"2FYpfJ\":[\"더 보기\"],\"2HF1Y2\":[[\"inviter\"],\"이(가) \",[\"target\"],\"을(를) \",[\"channel\"],\"에 초대했습니다\"],\"2I70QL\":[\"사용자 프로필 정보 보기\"],\"2QYdmE\":[\"사용자:\"],\"2QpEjG\":[\"퇴장했습니다\"],\"2YE223\":[\"#\",[\"0\"],\"에 메시지 (Enter로 줄 바꿈, Shift+Enter로 전송)\"],\"2bimFY\":[\"서버 비밀번호 사용\"],\"2iTmdZ\":[\"로컬 저장소:\"],\"2odkwe\":[\"엄격 - 더 강력한 보호\"],\"2uDhbA\":[\"초대할 사용자 이름 입력\"],\"2ygf/L\":[\"← 뒤로\"],\"2zEgxj\":[\"GIF 검색...\"],\"3RdPhl\":[\"채널 이름 변경\"],\"3THokf\":[\"Voice 사용자\"],\"3TSz9S\":[\"최소화\"],\"3jBDvM\":[\"채널 표시 이름\"],\"3ryuFU\":[\"앱 개선을 위한 선택적 충돌 보고서\"],\"3uBF/8\":[\"뷰어 닫기\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"계정 이름 입력...\"],\"4/Rr0R\":[\"현재 채널에 사용자 초대\"],\"4EZrJN\":[\"규칙\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"플러드 프로필 (+F)\"],\"4RZQRK\":[\"지금 뭐 하세요?\"],\"4hfTrB\":[\"닉네임\"],\"4n99LO\":[\"이미 \",[\"0\"],\"에 있음\"],\"4t6vMV\":[\"짧은 메시지의 경우 자동으로 한 줄 모드로 전환\"],\"4vsHmf\":[\"시간 (분)\"],\"5+INAX\":[\"나를 멘션한 메시지 강조 표시\"],\"5R5Pv/\":[\"Oper 이름\"],\"678PKt\":[\"네트워크 이름\"],\"6Aih4U\":[\"오프라인\"],\"6CO3WE\":[\"채널 참여에 필요한 비밀번호입니다. 키를 제거하려면 비워두세요.\"],\"6HhMs3\":[\"QUIT 메시지\"],\"6V3Ea3\":[\"복사됨\"],\"6lGV3K\":[\"접기\"],\"6yFOEi\":[\"oper 비밀번호 입력...\"],\"7+IHTZ\":[\"파일 선택 안 함\"],\"73hrRi\":[\"nick!user@host (예: spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"비공개 메시지 보내기\"],\"7U1W7c\":[\"매우 완화\"],\"7Y1YQj\":[\"실명:\"],\"7YHArF\":[\"— 뷰어에서 열기\"],\"7fjnVl\":[\"사용자 검색...\"],\"7jL88x\":[\"이 메시지를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.\"],\"7nGhhM\":[\"무슨 생각을 하고 계신가요?\"],\"7sEpu1\":[\"멤버 — \",[\"0\"],\"명\"],\"7sNhEz\":[\"사용자 이름\"],\"8H0Q+x\":[\"프로필에 대해 더 알아보기 →\"],\"8Phu0A\":[\"사용자가 닉네임을 변경할 때 표시\"],\"8XTG9e\":[\"oper 비밀번호 입력\"],\"8XsV2J\":[\"다시 보내기\"],\"8ZsakT\":[\"비밀번호\"],\"8kR84m\":[\"외부 링크를 열려고 합니다:\"],\"8lCgih\":[\"규칙 제거\"],\"8o3dPc\":[\"업로드할 파일을 끌어다 놓으세요\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"other\":[[\"joinCount\"],\"번 참가했음\"]}]],\"9BMLnJ\":[\"서버에 재연결\"],\"9OEgyT\":[\"반응 추가\"],\"9PQ8m2\":[\"G-Line (글로벌 밴)\"],\"9Qs99X\":[\"이메일:\"],\"9QupBP\":[\"패턴 제거\"],\"9bG48P\":[\"보내는 중\"],\"9f5f0u\":[\"개인정보 보호에 관한 문의사항이 있으신가요? 문의처:\"],\"9unqs3\":[\"자리비움:\"],\"9v3hwv\":[\"서버를 찾을 수 없습니다.\"],\"9zb2WA\":[\"연결 중\"],\"A1taO8\":[\"검색\"],\"A2adVi\":[\"입력 중 알림 보내기\"],\"A9Rhec\":[\"채널 이름\"],\"AWOSPo\":[\"확대\"],\"AXSpEQ\":[\"연결 시 Oper 인증\"],\"AeXO77\":[\"계정\"],\"AhNP40\":[\"탐색\"],\"Ai2U7L\":[\"호스트\"],\"AjBQnf\":[\"닉네임 변경됨\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"답장 취소\"],\"ApSx0O\":[\"\\\"\",[\"searchQuery\"],\"\\\"와 일치하는 메시지 \",[\"0\"],\"개 발견\"],\"AxPAXW\":[\"결과를 찾을 수 없습니다\"],\"AyNqAB\":[\"채팅에 모든 서버 이벤트 표시\"],\"B/QqGw\":[\"자리 비움 (AFK)\"],\"B8AaMI\":[\"이 필드는 필수입니다\"],\"BA2c49\":[\"서버가 고급 LIST 필터링을 지원하지 않습니다\"],\"BDKt3I\":[[\"0\"],\"님, \",[\"1\"],\"님, \",[\"2\"],\"님 외 \",[\"3\"],\"명이 입력 중...\"],\"BGul2A\":[\"저장되지 않은 변경 사항이 있습니다. 저장하지 않고 닫으시겠습니까?\"],\"BIf9fi\":[\"상태 메시지\"],\"BPm98R\":[\"선택된 서버가 없습니다. 먼저 사이드바에서 서버를 선택하세요. 초대 링크는 서버별로 관리됩니다.\"],\"BZz3md\":[\"개인 웹사이트\"],\"Bgm/H7\":[\"여러 줄 텍스트 입력 허용\"],\"BiQIl1\":[\"이 비공개 메시지 대화 고정\"],\"BlNZZ2\":[\"클릭하여 메시지로 이동\"],\"Bowq3c\":[\"운영자만 채널 주제를 변경할 수 있음\"],\"Btozzp\":[\"이 이미지가 만료되었습니다\"],\"Bycfjm\":[\"전체: \",[\"0\"]],\"C6IBQc\":[\"전체 JSON 복사\"],\"C9L9wL\":[\"데이터 수집\"],\"CDq4wC\":[\"사용자 제재\"],\"CHVRxG\":[\"@\",[\"0\"],\"에게 메시지 (Shift+Enter로 줄 바꿈)\"],\"CN9zdR\":[\"Oper 이름과 비밀번호는 필수입니다\"],\"CW3sYa\":[[\"emoji\"],\" 반응 추가\"],\"CaAkqd\":[\"QUIT 표시\"],\"CbvaYj\":[\"닉네임으로 차단\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"채널 선택\"],\"CsekCi\":[\"기본\"],\"D+NlUC\":[\"시스템\"],\"D28t6+\":[\"참여했다가 퇴장했습니다\"],\"DB8zMK\":[\"적용\"],\"DBcWHr\":[\"사용자 정의 알림 소리 파일\"],\"DTy9Xw\":[\"미디어 미리보기\"],\"Dj4pSr\":[\"안전한 비밀번호를 선택하세요\"],\"Du+zn+\":[\"검색 중...\"],\"Du2T2f\":[\"설정을 찾을 수 없습니다\"],\"DwsSVQ\":[\"필터 적용 및 새로 고침\"],\"E3W/zd\":[\"기본 닉네임\"],\"E6nRW7\":[\"URL 복사\"],\"E703RG\":[\"모드:\"],\"EAeu1Z\":[\"초대 보내기\"],\"EFKJQT\":[\"설정\"],\"EGPQBv\":[\"사용자 정의 플러드 규칙 (+f)\"],\"ELik0r\":[\"전체 개인정보 처리방침 보기\"],\"EPbeC2\":[\"채널 주제 보기 또는 편집\"],\"EQCDNT\":[\"oper 사용자 이름 입력...\"],\"EUvulZ\":[\"\\\"\",[\"searchQuery\"],\"\\\"와 일치하는 메시지 1개 발견\"],\"EatZYJ\":[\"다음 이미지\"],\"EdQY6l\":[\"없음\"],\"EnqLYU\":[\"서버 검색...\"],\"F0OKMc\":[\"서버 편집\"],\"F6Int2\":[\"강조 표시 활성화\"],\"FDoLyE\":[\"최대 사용자 수\"],\"FUU/hZ\":[\"채팅에서 로드할 외부 미디어의 범위를 제어하세요.\"],\"Fdp03t\":[\"켜기\"],\"FfPWR0\":[\"모달\"],\"FjkaiT\":[\"축소\"],\"FlqOE9\":[\"이것이 의미하는 바:\"],\"FolHNl\":[\"계정 및 인증 관리\"],\"Fp2Dif\":[\"서버에서 나갔습니다\"],\"G5KmCc\":[\"GZ-Line (글로벌 Z-Line)\"],\"GDs0lz\":[\"<0>위험:0> 민감한 정보(메시지, 비공개 대화, 인증 정보)가 네트워크 관리자나 IRC 서버 간에 위치한 공격자에게 노출될 수 있습니다.\"],\"GR+2I3\":[\"초대 마스크 추가 (예: nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"팝업 서버 알림 닫기\"],\"GdhD7H\":[\"확인하려면 다시 클릭하세요\"],\"GlHnXw\":[\"닉네임 변경 실패: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"미리보기:\"],\"GtmO8/\":[\"보낸 사람\"],\"GtuHUQ\":[\"서버에서 이 채널의 이름을 변경합니다. 모든 사용자에게 새 이름이 표시됩니다.\"],\"GuGfFX\":[\"검색 전환\"],\"GxkJXS\":[\"업로드 중...\"],\"GzbwnK\":[\"채널에 참여했습니다\"],\"GzsUDB\":[\"확장 프로필\"],\"H/PnT8\":[\"이모지 삽입\"],\"H6Izzl\":[\"선호하는 색상 코드\"],\"H9jIv+\":[\"입장/퇴장 표시\"],\"HAKBY9\":[\"파일 업로드\"],\"HdE1If\":[\"채널\"],\"Hk4AW9\":[\"선호하는 표시 이름\"],\"HmHDk7\":[\"멤버 선택\"],\"HrQzPU\":[[\"networkName\"],\"의 채널\"],\"I2tXQ5\":[\"@\",[\"0\"],\"에게 메시지 (Enter로 줄 바꿈, Shift+Enter로 전송)\"],\"I6bw/h\":[\"사용자 차단\"],\"I92Z+b\":[\"알림 활성화\"],\"I9D72S\":[\"이 메시지를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.\"],\"IA+1wo\":[\"사용자가 채널에서 추방될 때 표시\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"정보:\"],\"IUwGEM\":[\"변경 사항 저장\"],\"IVeGK6\":[[\"0\"],\"님, \",[\"1\"],\"님, \",[\"2\"],\"님이 입력 중...\"],\"IgrLD/\":[\"일시 정지\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"답장\"],\"IoHMnl\":[\"최대값은 \",[\"0\"],\"입니다\"],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"연결 중...\"],\"J5T9NW\":[\"사용자 정보\"],\"J8Y5+z\":[\"이런! 네트워크 분리! ⚠️\"],\"JBHkBA\":[\"채널을 나갔습니다\"],\"JCwL0Q\":[\"사유 입력 (선택 사항)\"],\"JFciKP\":[\"전환\"],\"JXGkhG\":[\"채널 이름 변경 (운영자 전용)\"],\"JcD7qf\":[\"추가 작업\"],\"JdkA+c\":[\"비밀 채널 (+s)\"],\"Jmu12l\":[\"서버 채널\"],\"JvQ++s\":[\"마크다운 활성화\"],\"K2jwh/\":[\"WHOIS 데이터를 사용할 수 없습니다\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"메시지 삭제\"],\"KKBlUU\":[\"임베드\"],\"KM0pLb\":[\"채널에 오신 것을 환영합니다!\"],\"KR6W2h\":[\"사용자 무시 해제\"],\"KV+Bi1\":[\"초대 전용 (+i)\"],\"KdCtwE\":[\"카운터를 초기화하기 전에 플러드 활동을 모니터링할 초 단위 시간\"],\"Kkezga\":[\"서버 비밀번호\"],\"KsiQ/8\":[\"채널 참여를 위해 초대가 필요합니다\"],\"L+gB/D\":[\"채널 정보\"],\"LC1a7n\":[\"IRC 서버에서 서버 간 링크의 보안 수준이 낮다고 보고했습니다. 즉, 메시지가 네트워크의 IRC 서버 간에 전달될 때 적절히 암호화되지 않거나 SSL/TLS 인증서가 올바르게 검증되지 않을 수 있습니다.\"],\"LNfLR5\":[\"추방 표시\"],\"LQb0W/\":[\"모든 이벤트 표시\"],\"LU7/yA\":[\"UI에 표시할 대체 이름입니다. 공백, 이모지, 특수 문자를 포함할 수 있습니다. IRC 명령에는 실제 채널 이름(\",[\"channelName\"],\")이 사용됩니다.\"],\"LUb9O7\":[\"올바른 서버 포트가 필요합니다\"],\"LV4fT6\":[\"설명 (선택사항, 예: \\\"3분기 베타 테스터\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"개인정보 처리방침\"],\"LcuSDR\":[\"프로필 정보 및 메타데이터 관리\"],\"LqLS9B\":[\"닉네임 변경 표시\"],\"LsDQt2\":[\"채널 설정\"],\"LtI9AS\":[\"소유자\"],\"LuNhhL\":[\"이 메시지에 반응했습니다\"],\"M/AZNG\":[\"아바타 이미지의 URL\"],\"M/WIer\":[\"메시지 보내기\"],\"M8er/5\":[\"이름:\"],\"MHk+7g\":[\"이전 이미지\"],\"MRorGe\":[\"사용자에게 PM\"],\"MVbSGP\":[\"시간 창 (초)\"],\"MkpcsT\":[\"메시지와 설정은 기기에 로컬로 저장됩니다\"],\"N/hDSy\":[\"봇으로 표시 - 보통 'on' 또는 비워두기\"],\"N7TQbE\":[[\"channelName\"],\"에 사용자 초대\"],\"NCca/o\":[\"기본 닉네임 입력...\"],\"Nqs6B9\":[\"모든 외부 미디어를 표시합니다. 모든 URL이 알 수 없는 서버에 요청을 보낼 수 있습니다.\"],\"Nt+9O7\":[\"원시 TCP 대신 WebSocket 사용\"],\"NxIHzc\":[\"사용자 연결 끊기\"],\"O+v/cL\":[\"서버의 모든 채널 검색\"],\"ODwSCk\":[\"GIF 보내기\"],\"OGQ5kK\":[\"알림 소리 및 강조 표시 설정\"],\"OIPt1Z\":[\"멤버 목록 사이드바 표시 또는 숨기기\"],\"OKSNq/\":[\"매우 엄격\"],\"ONWvwQ\":[\"업로드\"],\"OVKoQO\":[\"인증을 위한 계정 비밀번호\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - IRC를 미래로\"],\"OhCpra\":[\"주제 설정…\"],\"OkltoQ\":[\"닉네임으로 \",[\"username\"],\" 차단 (동일 닉으로 재입장 방지)\"],\"P+t/Te\":[\"추가 데이터 없음\"],\"P42Wcc\":[\"안전\"],\"PD38l0\":[\"채널 아바타 미리보기\"],\"PD9mEt\":[\"메시지를 입력하세요...\"],\"PPqfdA\":[\"채널 구성 설정 열기\"],\"PSCjfZ\":[\"이 채널에 표시될 주제입니다. 모든 사용자가 주제를 볼 수 있습니다.\"],\"PZCecv\":[\"PDF 미리보기\"],\"PeLgsC\":[[\"c\",\"plural\",{\"other\":[[\"c\"],\"번\"]}]],\"PguS2C\":[\"예외 마스크 추가 (예: nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[[\"0\"],\"개 채널 중 \",[\"displayedChannelsCount\"],\"개 표시\"],\"PqhVlJ\":[\"사용자 차단 (호스트마스크)\"],\"Q+chwU\":[\"사용자명:\"],\"Q2QY4/\":[\"이 초대 삭제\"],\"Q6hhn8\":[\"환경설정\"],\"QF4a34\":[\"사용자 이름을 입력하세요\"],\"QGqSZ2\":[\"색상 및 서식\"],\"QJQd1J\":[\"프로필 편집\"],\"QSzGDE\":[\"유휴\"],\"QUlny5\":[[\"0\"],\"에 오신 것을 환영합니다!\"],\"Qoq+GP\":[\"더 보기\"],\"QuSkCF\":[\"채널 필터링...\"],\"QwUrDZ\":[\"주제를 변경했습니다: \",[\"topic\"]],\"R0UH07\":[[\"1\"],\"개 중 \",[\"0\"],\"번째 이미지\"],\"R7SsBE\":[\"음소거\"],\"R8rf1X\":[\"클릭하여 주제 설정\"],\"RArB3D\":[[\"username\"],\"에 의해 \",[\"channelName\"],\"에서 추방당했습니다\"],\"RI3cWd\":[\"ObsidianIRC와 함께 IRC의 세계를 탐험하세요\"],\"RIfHS5\":[\"새 초대 링크 생성\"],\"RMMaN5\":[\"발언권 제한 (+m)\"],\"RWw9Lg\":[\"모달 닫기\"],\"RZ2BuZ\":[[\"account\"],\" 계정 등록에 인증이 필요합니다: \",[\"message\"]],\"RySp6q\":[\"댓글 숨기기\"],\"SPKQTd\":[\"닉네임은 필수입니다\"],\"SPVjfj\":[\"비워두면 기본값인 '사유 없음'이 사용됩니다\"],\"SQKPvQ\":[\"사용자 초대\"],\"SkZcl+\":[\"미리 정의된 플러드 방지 프로필을 선택하세요. 각 프로필은 다양한 사용 사례에 맞게 균형 잡힌 보호 설정을 제공합니다.\"],\"Slr+3C\":[\"최소 사용자 수\"],\"Spnlre\":[[\"target\"],\"을(를) \",[\"channel\"],\"에 초대했습니다\"],\"T/ckN5\":[\"뷰어에서 열기\"],\"T91vKp\":[\"재생\"],\"TV2Wdu\":[\"데이터 처리 방식 및 개인정보 보호 방법을 알아보세요.\"],\"TgFpwD\":[\"적용 중...\"],\"TkzSFB\":[\"변경 사항 없음\"],\"TtserG\":[\"실명 입력\"],\"Ttz9J1\":[\"비밀번호 입력...\"],\"Tz0i8g\":[\"설정\"],\"U3pytU\":[\"관리자\"],\"UDb2YD\":[\"반응\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"아직 초대 링크를 생성하지 않았습니다. 위 양식을 사용하여 첫 링크를 만들어보세요.\"],\"UGT5vp\":[\"설정 저장\"],\"UV5hLB\":[\"차단된 사용자가 없습니다\"],\"Uaj3Nd\":[\"상태 메시지\"],\"Ue3uny\":[\"기본값 (프로필 없음)\"],\"UkARhe\":[\"기본 - 표준 보호\"],\"Umn7Cj\":[\"아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!\"],\"UtUIRh\":[\"이전 메시지 \",[\"0\"],\"개\"],\"UwzP+U\":[\"보안 연결\"],\"V0/A4O\":[\"채널 소유자\"],\"V4qgxE\":[\"생성 시각 이전 (분 전)\"],\"V8yTm6\":[\"검색 지우기\"],\"VJMMyz\":[\"ObsidianIRC - IRC를 미래로\"],\"VJScHU\":[\"사유\"],\"VLsmVV\":[\"알림 음소거\"],\"VbyRUy\":[\"댓글\"],\"Vmx0mQ\":[\"설정자:\"],\"VqnIZz\":[\"개인정보 처리방침 및 데이터 관행 보기\"],\"VrMygG\":[\"최소 길이는 \",[\"0\"],\"자입니다\"],\"VrnTui\":[\"프로필에 표시되는 대명사\"],\"W8E3qn\":[\"인증된 계정\"],\"WAakm9\":[\"채널 삭제\"],\"WFxTHC\":[\"차단 마스크 추가 (예: nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"서버 호스트는 필수입니다\"],\"WRYdXW\":[\"오디오 재생 위치\"],\"WUOH5B\":[\"사용자 무시\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"other\":[[\"1\"],\"개 더 보기\"]}]],\"WYxRzo\":[\"초대 링크 생성 및 관리\"],\"Wd38W1\":[\"일반 네트워크 초대를 보내려면 채널을 비워두세요. 설명은 본인 기록용일 뿐이며 이 목록에서 본인에게만 표시됩니다.\"],\"Weq9zb\":[\"일반\"],\"Wfj7Sk\":[\"알림 소리 음소거 또는 해제\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"사용자 프로필\"],\"X6S3lt\":[\"설정, 채널, 서버 검색...\"],\"XEHan5\":[\"계속 진행\"],\"XI1+wb\":[\"잘못된 형식\"],\"XIXeuC\":[\"@\",[\"0\"],\"에게 메시지 보내기\"],\"XMS+k4\":[\"비공개 메시지 시작\"],\"XWgxXq\":[\"앨범\"],\"Xd7+IT\":[\"비공개 채팅 고정 해제\"],\"Xm/s+u\":[\"화면 표시\"],\"Xp2n93\":[\"서버의 신뢰할 수 있는 파일 호스트의 미디어를 표시합니다. 외부 서비스에 요청이 전송되지 않습니다.\"],\"XvjC4F\":[\"저장 중...\"],\"Y/qryO\":[\"검색 결과와 일치하는 사용자가 없습니다\"],\"YAqRpI\":[[\"account\"],\" 계정 등록 성공: \",[\"message\"]],\"YEfzvP\":[\"주제 보호 (+t)\"],\"YQOn6a\":[\"멤버 목록 접기\"],\"YRCoE9\":[\"채널 Op\"],\"YURQaF\":[\"프로필 보기\"],\"YdBSvr\":[\"미디어 표시 및 외부 콘텐츠 제어\"],\"Yj6U3V\":[\"중앙 서버 없음:\"],\"YjvpGx\":[\"대명사\"],\"YqH4l4\":[\"키 없음\"],\"YyUPpV\":[\"계정:\"],\"ZJSWfw\":[\"서버 연결 종료 시 표시할 메시지\"],\"ZR1dJ4\":[\"초대\"],\"ZdWg0V\":[\"브라우저에서 열기\"],\"ZhRBbl\":[\"메시지 검색…\"],\"Zmcu3y\":[\"고급 필터\"],\"a2/8e5\":[\"주제 설정 이후 (분 전)\"],\"aHKcKc\":[\"이전 페이지\"],\"aJTbXX\":[\"Oper 비밀번호\"],\"aQryQv\":[\"이미 존재하는 패턴입니다\"],\"aW9pLN\":[\"채널에 허용되는 최대 사용자 수입니다. 제한 없이 두려면 비워두세요.\"],\"ah4fmZ\":[\"YouTube, Vimeo, SoundCloud 등 알려진 서비스의 미리보기도 표시합니다.\"],\"aifXak\":[\"이 채널에 미디어가 없습니다\"],\"ap2zBz\":[\"완화\"],\"az8lvo\":[\"끄기\"],\"azXSNo\":[\"멤버 목록 펼치기\"],\"azdliB\":[\"계정에 로그인\"],\"b26wlF\":[\"그녀/그녀의\"],\"bD/+Ei\":[\"엄격\"],\"bQ6BJn\":[\"상세한 플러드 방지 규칙을 설정하세요. 각 규칙은 모니터링할 활동 유형과 임계값 초과 시 취할 조치를 지정합니다.\"],\"beV7+y\":[\"사용자가 \",[\"channelName\"],\" 참여 초대를 받게 됩니다.\"],\"bk84cH\":[\"자리 비움 메시지\"],\"bkHdLj\":[\"IRC 서버 추가\"],\"bmQLn5\":[\"규칙 추가\"],\"bwRvnp\":[\"작업\"],\"c8+EVZ\":[\"인증된 계정\"],\"cGYUlD\":[\"미디어 미리보기가 로드되지 않습니다.\"],\"cLF98o\":[\"댓글 보기 (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"사용 가능한 사용자가 없습니다\"],\"cSgpoS\":[\"비공개 채팅 고정\"],\"cde3ce\":[\"<0>\",[\"0\"],\"0>에게 메시지\"],\"chQsxg\":[\"형식화된 출력 복사\"],\"cl/A5J\":[[\"__DEFAULT_IRC_SERVER_NAME__\"],\"에 오신 것을 환영합니다!\"],\"cnGeoo\":[\"삭제\"],\"coPLXT\":[\"IRC 통신 내용은 서버에 저장되지 않습니다\"],\"crYH/6\":[\"SoundCloud 플레이어\"],\"d3sis4\":[\"서버 추가\"],\"d9aN5k\":[\"채널에서 \",[\"username\"],\" 제거\"],\"dEgA5A\":[\"취소\"],\"dGi1We\":[\"이 비공개 메시지 대화 고정 해제\"],\"dJVuyC\":[[\"channelName\"],\"에서 나갔습니다 (\",[\"reason\"],\")\"],\"dMtLDE\":[\"받는 사람\"],\"dXqxlh\":[\"<0>⚠️ 보안 위험!0> 이 연결은 도청 또는 중간자 공격에 취약할 수 있습니다.\"],\"da9Q/R\":[\"채널 모드 변경됨\"],\"dhJN3N\":[\"댓글 보기\"],\"dj2xTE\":[\"알림 닫기\"],\"dpCzmC\":[\"플러드 방지 설정\"],\"e9dQpT\":[\"이 링크를 새 탭에서 여시겠습니까?\"],\"ePK91l\":[\"편집\"],\"eYBDuB\":[\"이미지를 업로드하거나 동적 크기 조정을 위해 \",[\"size\"],\" 대체가 있는 URL을 제공하세요\"],\"edBbee\":[\"호스트마스크로 \",[\"username\"],\" 차단 (동일 IP/호스트에서 재입장 방지)\"],\"ekfzWq\":[\"사용자 설정\"],\"elPDWs\":[\"IRC 클라이언트 환경을 맞춤 설정하세요\"],\"eu2osY\":[\"<0>💡 권장사항:0> 이 서버를 신뢰하고 위험을 충분히 이해한 경우에만 계속하세요. 이 연결을 통해 민감한 정보나 비밀번호를 공유하지 마세요.\"],\"euEhbr\":[[\"channel\"],\"에 참여하려면 클릭\"],\"ez3vLd\":[\"여러 줄 입력 활성화\"],\"f0J5Ki\":[\"서버 간 통신에 암호화되지 않은 연결이 사용될 수 있습니다\"],\"f9BHJk\":[\"사용자 경고\"],\"fDOLLd\":[\"채널을 찾을 수 없습니다.\"],\"ffzDkB\":[\"익명 분석:\"],\"fq1GF9\":[\"사용자가 서버에서 연결을 끊을 때 표시\"],\"gEF57C\":[\"이 서버는 하나의 연결 유형만 지원합니다\"],\"gJuLUI\":[\"무시 목록\"],\"gNzMrk\":[\"현재 아바타\"],\"gjPWyO\":[\"닉네임 입력...\"],\"gz6UQ3\":[\"최대화\"],\"h6razj\":[\"채널 이름 마스크 제외\"],\"hG6jnw\":[\"설정된 주제 없음\"],\"hG89Ed\":[\"이미지\"],\"hYgDIe\":[\"생성\"],\"hZ6znB\":[\"포트\"],\"ha+Bz5\":[\"예: 100:1440\"],\"he3ygx\":[\"복사\"],\"hehnjM\":[\"횟수\"],\"hzdLuQ\":[\"Voice 이상의 권한을 가진 사용자만 발언 가능\"],\"i0qMbr\":[\"홈\"],\"iDNBZe\":[\"알림\"],\"iH8pgl\":[\"뒤로\"],\"iL9SZg\":[\"사용자 차단 (닉네임)\"],\"iNt+3c\":[\"이미지로 돌아가기\"],\"iQvi+a\":[\"이 서버의 낮은 링크 보안에 대해 다시 경고하지 않음\"],\"iSLIjg\":[\"연결\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"서버 호스트\"],\"idD8Ev\":[\"저장됨\"],\"iivqkW\":[\"접속 시각\"],\"ij+Elv\":[\"이미지 미리보기\"],\"ilIWp7\":[\"알림 전환\"],\"iuaqvB\":[\"와일드카드로 *를 사용하세요. 예: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"봇\"],\"j2DGR0\":[\"호스트마스크로 차단\"],\"jA4uoI\":[\"주제:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"사유 (선택 사항)\"],\"jUV7CU\":[\"아바타 업로드\"],\"jW5Uwh\":[\"로드할 외부 미디어의 범위를 제어합니다. 끄기 / 안전 / 신뢰할 수 있는 출처 / 모든 콘텐츠.\"],\"jXzms5\":[\"첨부 옵션\"],\"jZlrte\":[\"색상\"],\"jfC/xh\":[\"연락처\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"이전 메시지 불러오기\"],\"k3ID0F\":[\"멤버 필터링…\"],\"k65gsE\":[\"자세히 보기\"],\"k7Zgob\":[\"연결 취소\"],\"kAVx5h\":[\"초대가 없습니다\"],\"kCLEPU\":[\"연결된 서버\"],\"kF5LKb\":[\"무시된 패턴:\"],\"kGeOx/\":[[\"0\"],\" 참가\"],\"kITKr8\":[\"채널 모드 불러오는 중...\"],\"kPpPsw\":[\"당신은 IRC Operator입니다\"],\"kWJmRL\":[\"나\"],\"kfcRb0\":[\"아바타\"],\"kjMqSj\":[\"JSON 복사\"],\"krViRy\":[\"JSON으로 복사하려면 클릭\"],\"ks71ra\":[\"예외\"],\"kw4lRv\":[\"채널 Halfop\"],\"kxgIRq\":[\"시작하려면 채널을 선택하거나 추가하세요.\"],\"ky6dWe\":[\"아바타 미리보기\"],\"l+GxCv\":[\"채널 불러오는 중...\"],\"l+IUVW\":[[\"account\"],\" 계정 인증 성공: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"other\":[[\"reconnectCount\"],\"번 재연결됨\"]}]],\"l1l8sj\":[[\"0\"],\"일 전\"],\"l5NhnV\":[\"#채널 (선택사항)\"],\"l5jmzx\":[[\"0\"],\"님과 \",[\"1\"],\"님이 입력 중...\"],\"lCF0wC\":[\"새로 고침\"],\"lHy8N5\":[\"채널 더 불러오는 중...\"],\"lasgrr\":[\"사용됨\"],\"lbpf14\":[[\"value\"],\" 참여\"],\"lfFsZ4\":[\"채널\"],\"lkNdiH\":[\"계정 이름\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"이미지 업로드\"],\"loQxaJ\":[\"돌아왔습니다\"],\"lvfaxv\":[\"홈\"],\"m16xKo\":[\"추가\"],\"m8flAk\":[\"미리보기 (아직 업로드되지 않음)\"],\"mEPxTp\":[\"<0>⚠️ 주의하세요!0> 신뢰할 수 있는 출처의 링크만 여세요. 악성 링크는 보안이나 개인정보를 침해할 수 있습니다.\"],\"mHGdhG\":[\"서버 정보\"],\"mHS8lb\":[\"#\",[\"0\"],\"에 메시지 보내기\"],\"mMYBD9\":[\"광역 - 더 넓은 보호 범위\"],\"mTGsPd\":[\"채널 주제\"],\"mU8j6O\":[\"외부 메시지 차단 (+n)\"],\"mZp8FL\":[\"자동으로 한 줄 모드로 전환\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"댓글 (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"인증된 사용자\"],\"mwtcGl\":[\"댓글 닫기\"],\"mzI/c+\":[\"다운로드\"],\"n3fGRk\":[[\"0\"],\"이(가) 설정\"],\"nE9jsU\":[\"완화 - 덜 공격적인 보호\"],\"nNflMD\":[\"채널 나가기\"],\"nPXkBi\":[\"WHOIS 데이터 불러오는 중...\"],\"nQnxxF\":[\"#\",[\"0\"],\"에 메시지 (Shift+Enter로 줄 바꿈)\"],\"nWMRxa\":[\"고정 해제\"],\"nkC032\":[\"플러드 프로필 없음\"],\"o69z4d\":[[\"username\"],\"에게 경고 메시지 보내기\"],\"o9ylQi\":[\"GIF를 검색하여 시작하세요\"],\"oFGkER\":[\"서버 알림\"],\"oOi11l\":[\"맨 아래로 스크롤\"],\"oPYIL5\":[\"네트워크\"],\"oQEzQR\":[\"새 DM\"],\"oXOSPE\":[\"온라인\"],\"oal760\":[\"서버 링크에 대한 중간자 공격이 가능합니다\"],\"oeqmmJ\":[\"신뢰할 수 있는 출처\"],\"optX0N\":[[\"0\"],\"시간 전\"],\"ovBPCi\":[\"기본값\"],\"p0Z69r\":[\"패턴은 비워둘 수 없습니다\"],\"p1KgtK\":[\"오디오를 불러오지 못했습니다\"],\"p59pEv\":[\"추가 세부정보\"],\"p7sRI6\":[\"입력 중임을 다른 사람에게 알림\"],\"pBm1od\":[\"비밀 채널\"],\"pNmiXx\":[\"모든 서버에 사용할 기본 닉네임\"],\"pUUo9G\":[\"호스트명:\"],\"pVGPmz\":[\"계정 비밀번호\"],\"peNE68\":[\"영구\"],\"plhHQt\":[\"데이터 없음\"],\"pm6+q5\":[\"보안 경고\"],\"pn5qSs\":[\"추가 정보\"],\"q0cR4S\":[\"이제 **\",[\"newNick\"],\"**(으)로 알려져 있습니다\"],\"qFcunY\":[\"LIST 또는 NAMES 명령에 채널이 표시되지 않음\"],\"qLpTm/\":[[\"emoji\"],\" 반응 제거\"],\"qVkGWK\":[\"고정\"],\"qY8wNa\":[\"홈페이지\"],\"qb0xJ7\":[\"와일드카드 사용: *는 임의의 문자열, ?는 임의의 단일 문자. 예: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"채널 키 (+k)\"],\"qtoOYG\":[\"제한 없음\"],\"r1W2AS\":[\"파일 호스트 이미지\"],\"rIPR2O\":[\"주제 설정 이전 (분 전)\"],\"rMMSYo\":[\"최대 길이는 \",[\"0\"],\"자입니다\"],\"rWtzQe\":[\"네트워크가 분리되었다가 재연결되었습니다. ✅\"],\"rYG2u6\":[\"잠시만 기다려 주세요...\"],\"rdUucN\":[\"미리보기\"],\"rjGI/Q\":[\"개인정보 보호\"],\"rk8iDX\":[\"GIF 불러오는 중...\"],\"rn6SBY\":[\"음소거 해제\"],\"s/UKqq\":[\"채널에서 추방되었습니다\"],\"s8cATI\":[[\"channelName\"],\"에 참가했습니다\"],\"sCO9ue\":[\"<0>\",[\"serverName\"],\"0>에 대한 연결에 다음과 같은 보안 문제가 있습니다:\"],\"sGH11W\":[\"서버\"],\"sHI1H+\":[\"이제 **\",[\"newNick\"],\"**(으)로 알려져 있습니다\"],\"sJyV04\":[[\"inviter\"],\"이(가) 당신을 \",[\"channel\"],\"에 초대했습니다\"],\"sby+1/\":[\"클릭하여 복사\"],\"sfN25C\":[\"실명 또는 전체 이름\"],\"sliuzR\":[\"링크 열기\"],\"sqrO9R\":[\"사용자 정의 멘션\"],\"sr6RdJ\":[\"Shift+Enter로 여러 줄 입력\"],\"swrCpB\":[[\"user\"],\"이(가) 채널을 \",[\"oldName\"],\"에서 \",[\"newName\"],\"(으)로 이름을 변경했습니다\",[\"0\"]],\"sxkWRg\":[\"고급\"],\"t/YqKh\":[\"제거\"],\"t47eHD\":[\"이 서버에서의 고유 식별자\"],\"tAkAh0\":[\"동적 크기 조정을 위한 \",[\"size\"],\" 대체가 있는 URL. 예: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"채널 목록 사이드바 표시 또는 숨기기\"],\"tfDRzk\":[\"저장\"],\"tiBsJk\":[[\"channelName\"],\"에서 나갔습니다\"],\"tt4/UD\":[\"서버를 나갔습니다 (\",[\"reason\"],\")\"],\"u0TcnO\":[\"닉네임 {nick}이(가) 이미 사용 중입니다. {newNick}(으)로 다시 시도합니다\"],\"u0a8B4\":[\"관리 권한을 위해 IRC Operator로 인증\"],\"u0rWFU\":[\"생성 시각 이후 (분 전)\"],\"u72w3t\":[\"무시할 사용자 및 패턴\"],\"u7jc2L\":[\"서버를 나갔습니다\"],\"uAQUqI\":[\"상태\"],\"uB85T3\":[\"저장 실패: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC 서버:\"],\"ukyW4o\":[\"내 초대 링크\"],\"usSSr/\":[\"확대/축소 수준\"],\"v7uvcf\":[\"소프트웨어:\"],\"vE8kb+\":[\"줄 바꿈은 Shift+Enter (Enter로 전송)\"],\"vERlcd\":[\"프로필\"],\"vK0RL8\":[\"주제 없음\"],\"vSJd18\":[\"동영상\"],\"vXIe7J\":[\"언어\"],\"vaHYxN\":[\"실명\"],\"vhjbKr\":[\"자리 비움\"],\"w4NYox\":[[\"title\"],\" 클라이언트\"],\"w8xQRx\":[\"잘못된 값\"],\"wFjjxZ\":[[\"username\"],\"에 의해 \",[\"channelName\"],\"에서 추방당했습니다 (\",[\"reason\"],\")\"],\"wGjaGl\":[\"차단 예외가 없습니다\"],\"wPrGnM\":[\"채널 관리자\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"사용자가 채널에 입장하거나 퇴장할 때 표시\"],\"whqZ9r\":[\"강조할 추가 단어 또는 문구\"],\"wm7RV4\":[\"알림 소리\"],\"wz/Yoq\":[\"서버 간 전달 시 메시지가 도청될 수 있습니다\"],\"x3+y8b\":[\"이 링크를 통해 등록한 사람 수\"],\"xCJdfg\":[\"지우기\"],\"xOTzt5\":[\"방금\"],\"xUHRTR\":[\"연결 시 자동으로 operator로 인증\"],\"xWHwwQ\":[\"차단 목록\"],\"xYilR2\":[\"미디어\"],\"xbi8D6\":[\"이 서버는 초대 링크를 지원하지 않습니다 (<0>obby.world/invitation0> 기능이 광고되지 않음). 일반 채팅은 계속할 수 있으며, 이 패널은 obbyircd 기반 네트워크용입니다.\"],\"xceQrO\":[\"보안 웹소켓만 지원됩니다\"],\"xdtXa+\":[\"채널-이름\"],\"xfXC7q\":[\"텍스트 채널\"],\"xlCYOE\":[\"메시지를 더 불러오는 중...\"],\"xlhswE\":[\"최솟값은 \",[\"0\"],\"입니다\"],\"xq97Ci\":[\"단어나 문구 추가...\"],\"xuRqRq\":[\"클라이언트 제한 (+l)\"],\"xwF+7J\":[[\"0\"],\"님이 입력 중...\"],\"y1eoq1\":[\"링크 복사\"],\"yNeucF\":[\"이 서버는 확장 프로필 메타데이터(IRCv3 METADATA 확장)를 지원하지 않습니다. 아바타, 표시 이름, 상태 등의 추가 필드를 사용할 수 없습니다.\"],\"yPlrca\":[\"채널 아바타\"],\"yQE2r9\":[\"로딩 중\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Oper 사용자 이름\"],\"yYOzWD\":[\"로그\"],\"yfx9Re\":[\"IRC operator 비밀번호\"],\"ygCKqB\":[\"정지\"],\"ymDxJx\":[\"IRC operator 사용자 이름\"],\"yrpRsQ\":[\"이름순 정렬\"],\"yz7wBu\":[\"닫기\"],\"zJw+jA\":[\"모드 설정: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"분 전\"],\"zebeLu\":[\"oper 사용자 이름 입력\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"잘못된 패턴 형식입니다. nick!user@host 형식을 사용하세요 (와일드카드 * 허용)\"],\"+6NQQA\":[\"일반 지원 채널\"],\"+6NyRG\":[\"클라이언트\"],\"+K0AvT\":[\"연결 끊기\"],\"+cyFdH\":[\"자리 비움 설정 시 기본 메시지\"],\"+mVPqU\":[\"메시지에서 마크다운 서식 렌더링\"],\"+vqCJH\":[\"인증을 위한 계정 사용자 이름\"],\"+yPBXI\":[\"파일 선택\"],\"+zy2Nq\":[\"유형\"],\"/09cao\":[\"낮은 링크 보안 (레벨 \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"채널 외부 사용자는 메시지를 보낼 수 없습니다\"],\"/4C8U0\":[\"모ë ë³µì¬\"],\"/6BzZF\":[\"멤버 목록 전환\"],\"/AkXyp\":[\"íì¸íìê²\xA0ìµëê¹?\"],\"/TNOPk\":[\"자리 비움 중\"],\"/XQgft\":[\"채널 탐색\"],\"/cF7Rs\":[\"볼륨\"],\"/dqduX\":[\"다음 페이지\"],\"/fc3q4\":[\"모든 콘텐츠\"],\"/kISDh\":[\"알림 소리 활성화\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"오디오\"],\"/rfkZe\":[\"멘션 및 메시지에 소리 재생\"],\"0/0ZGA\":[\"채널 이름 마스크\"],\"0D6j7U\":[\"사용자 정의 규칙에 대해 더 알아보기 →\"],\"0XsHcR\":[\"사용자 추방\"],\"0ZpE//\":[\"사용자 수순 정렬\"],\"0bEPwz\":[\"자리 비움 설정\"],\"0dGkPt\":[\"채널 목록 펼치기\"],\"0gS7M5\":[\"표시 이름\"],\"0kS+M8\":[\"예시네트워크\"],\"0rgoY7\":[\"직접 선택한 서버에만 연결합니다\"],\"0wdd7X\":[\"참여\"],\"0wkVYx\":[\"비공개 메시지\"],\"111uHX\":[\"링크 미리보기\"],\"196EG4\":[\"비공개 채팅 삭제\"],\"1DSr1i\":[\"계정 등록\"],\"1O/24y\":[\"채널 목록 전환\"],\"1TNIig\":[\"열기\"],\"1VPJJ2\":[\"외부 링크 경고\"],\"1ZC/dv\":[\"읽지 않은 멘션이나 메시지가 없습니다\"],\"1pO1zi\":[\"서버 이름은 필수입니다\"],\"1uwfzQ\":[\"채널 주제 보기\"],\"268g7c\":[\"표시 이름 입력\"],\"2CEOW6\":[\"soju 바운서를 통해 바인딩된 네트워크\"],\"2F9+AZ\":[\"ìì§ ìº¡ì²ë IRC ìì í¸ëí½ì´ ììµëë¤. ì°ê²°íê±°ë ë©ìì§ë¥¼ ë³´ë´ë³´ì¸ì.\"],\"2FOFq1\":[\"네트워크 서버 운영자가 메시지를 읽을 수 있습니다\"],\"2FYpfJ\":[\"더 보기\"],\"2HF1Y2\":[[\"inviter\"],\"이(가) \",[\"target\"],\"을(를) \",[\"channel\"],\"에 초대했습니다\"],\"2I70QL\":[\"사용자 프로필 정보 보기\"],\"2QYdmE\":[\"사용자:\"],\"2QpEjG\":[\"퇴장했습니다\"],\"2YE223\":[\"#\",[\"0\"],\"에 메시지 (Enter로 줄 바꿈, Shift+Enter로 전송)\"],\"2bimFY\":[\"서버 비밀번호 사용\"],\"2iTmdZ\":[\"로컬 저장소:\"],\"2odkwe\":[\"엄격 - 더 강력한 보호\"],\"2uDhbA\":[\"초대할 사용자 이름 입력\"],\"2ygf/L\":[\"← 뒤로\"],\"2zEgxj\":[\"GIF 검색...\"],\"3RdPhl\":[\"채널 이름 변경\"],\"3THokf\":[\"Voice 사용자\"],\"3TSz9S\":[\"최소화\"],\"3jBDvM\":[\"채널 표시 이름\"],\"3ryuFU\":[\"앱 개선을 위한 선택적 충돌 보고서\"],\"3uBF/8\":[\"뷰어 닫기\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"계정 이름 입력...\"],\"4/Rr0R\":[\"현재 채널에 사용자 초대\"],\"4EZrJN\":[\"규칙\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"플러드 프로필 (+F)\"],\"4RZQRK\":[\"지금 뭐 하세요?\"],\"4hfTrB\":[\"닉네임\"],\"4n99LO\":[\"이미 \",[\"0\"],\"에 있음\"],\"4t6vMV\":[\"짧은 메시지의 경우 자동으로 한 줄 모드로 전환\"],\"4vsHmf\":[\"시간 (분)\"],\"4x/Axu\":[\"바운서에 아직 네트워크가 없습니다. 시작하려면 하나를 추가하세요.\"],\"5+INAX\":[\"나를 멘션한 메시지 강조 표시\"],\"5R5Pv/\":[\"Oper 이름\"],\"678PKt\":[\"네트워크 이름\"],\"6Aih4U\":[\"오프라인\"],\"6CO3WE\":[\"채널 참여에 필요한 비밀번호입니다. 키를 제거하려면 비워두세요.\"],\"6HhMs3\":[\"QUIT 메시지\"],\"6V3Ea3\":[\"복사됨\"],\"6lGV3K\":[\"접기\"],\"6yFOEi\":[\"oper 비밀번호 입력...\"],\"7+IHTZ\":[\"파일 선택 안 함\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (예: spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"비공개 메시지 보내기\"],\"7U1W7c\":[\"매우 완화\"],\"7Y1YQj\":[\"실명:\"],\"7YHArF\":[\"— 뷰어에서 열기\"],\"7fjnVl\":[\"사용자 검색...\"],\"7jL88x\":[\"이 메시지를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.\"],\"7nGhhM\":[\"무슨 생각을 하고 계신가요?\"],\"7sEpu1\":[\"멤버 — \",[\"0\"],\"명\"],\"7sNhEz\":[\"사용자 이름\"],\"8H0Q+x\":[\"프로필에 대해 더 알아보기 →\"],\"8Phu0A\":[\"사용자가 닉네임을 변경할 때 표시\"],\"8XTG9e\":[\"oper 비밀번호 입력\"],\"8XsV2J\":[\"다시 보내기\"],\"8ZsakT\":[\"비밀번호\"],\"8kR84m\":[\"외부 링크를 열려고 합니다:\"],\"8lCgih\":[\"규칙 제거\"],\"8o3dPc\":[\"ì
ë¡ëí\xA0 íì¼ì ëì´ë¤ ëì¼ì¸ì\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"other\":[[\"joinCount\"],\"번 참가했음\"]}]],\"9BMLnJ\":[\"서버에 재연결\"],\"9OEgyT\":[\"반응 추가\"],\"9PQ8m2\":[\"G-Line (글로벌 밴)\"],\"9Qs99X\":[\"이메일:\"],\"9QupBP\":[\"패턴 제거\"],\"9W7tl5\":[\"(변경 없음)\"],\"9bG48P\":[\"보내는 중\"],\"9f5f0u\":[\"개인정보 보호에 관한 문의사항이 있으신가요? 문의처:\"],\"9iweoP\":[[\"0\"],\"의 네트워크\"],\"9unqs3\":[\"자리비움:\"],\"9v3hwv\":[\"서버를 찾을 수 없습니다.\"],\"9zb2WA\":[\"연결 중\"],\"A1taO8\":[\"검색\"],\"A2adVi\":[\"입력 중 알림 보내기\"],\"A9Rhec\":[\"채널 이름\"],\"AWOSPo\":[\"확대\"],\"AXSpEQ\":[\"연결 시 Oper 인증\"],\"AdKRCX\":[\"<0>\",[\"0\"],\"0>에 연결되어 있습니다.\"],\"AeXO77\":[\"계정\"],\"AhNP40\":[\"탐색\"],\"Ai2U7L\":[\"호스트\"],\"AjBQnf\":[\"닉네임 변경됨\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"답장 취소\"],\"ApSx0O\":[\"\\\"\",[\"searchQuery\"],\"\\\"와 일치하는 메시지 \",[\"0\"],\"개 발견\"],\"AxPAXW\":[\"결과를 찾을 수 없습니다\"],\"AyNqAB\":[\"채팅에 모든 서버 이벤트 표시\"],\"B/QqGw\":[\"자리 비움 (AFK)\"],\"B0sB2k\":[\"평문\"],\"B8AaMI\":[\"이 필드는 필수입니다\"],\"BA2c49\":[\"서버가 고급 LIST 필터링을 지원하지 않습니다\"],\"BDKt3I\":[[\"0\"],\"님, \",[\"1\"],\"님, \",[\"2\"],\"님 외 \",[\"3\"],\"명이 입력 중...\"],\"BGul2A\":[\"저장되지 않은 변경 사항이 있습니다. 저장하지 않고 닫으시겠습니까?\"],\"BIf9fi\":[\"상태 메시지\"],\"BOJWfb\":[\"soju 바운서에서 연결을 끊으시겠습니까?\"],\"BPm98R\":[\"ì\xA0íë ìë²ê° ììµëë¤. 먼ì\xA0 ì¬ì´ëë°ìì ìë²ë¥¼ ì\xA0ííì¸ì. ì´ë ë§í¬ë ìë²ë³ë¡ ê´ë¦¬ë©ëë¤.\"],\"BZz3md\":[\"개인 웹사이트\"],\"Bgm/H7\":[\"여러 줄 텍스트 입력 허용\"],\"BiQIl1\":[\"이 비공개 메시지 대화 고정\"],\"BlNZZ2\":[\"클릭하여 메시지로 이동\"],\"Bowq3c\":[\"운영자만 채널 주제를 변경할 수 있음\"],\"Btozzp\":[\"이 이미지가 만료되었습니다\"],\"Bycfjm\":[\"전체: \",[\"0\"]],\"C6IBQc\":[\"전체 JSON 복사\"],\"C9L9wL\":[\"데이터 수집\"],\"CDq4wC\":[\"사용자 제재\"],\"CHVRxG\":[\"@\",[\"0\"],\"에게 메시지 (Shift+Enter로 줄 바꿈)\"],\"CN9zdR\":[\"Oper 이름과 비밀번호는 필수입니다\"],\"CW3sYa\":[[\"emoji\"],\" 반응 추가\"],\"CaAkqd\":[\"QUIT 표시\"],\"CbvaYj\":[\"닉네임으로 차단\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"채널 선택\"],\"CsekCi\":[\"기본\"],\"D+NlUC\":[\"시스템\"],\"D28t6+\":[\"참여했다가 퇴장했습니다\"],\"DB8zMK\":[\"적용\"],\"DBcWHr\":[\"사용자 정의 알림 소리 파일\"],\"DTy9Xw\":[\"미디어 미리보기\"],\"Dj4pSr\":[\"안전한 비밀번호를 선택하세요\"],\"Du+zn+\":[\"검색 중...\"],\"Du2T2f\":[\"설정을 찾을 수 없습니다\"],\"DwsSVQ\":[\"필터 적용 및 새로 고침\"],\"E3W/zd\":[\"기본 닉네임\"],\"E6nRW7\":[\"URL 복사\"],\"E703RG\":[\"모드:\"],\"EAeu1Z\":[\"초대 보내기\"],\"EFKJQT\":[\"설정\"],\"EGPQBv\":[\"사용자 정의 플러드 규칙 (+f)\"],\"ELik0r\":[\"전체 개인정보 처리방침 보기\"],\"EPbeC2\":[\"채널 주제 보기 또는 편집\"],\"EQCDNT\":[\"oper 사용자 이름 입력...\"],\"EUvulZ\":[\"\\\"\",[\"searchQuery\"],\"\\\"와 일치하는 메시지 1개 발견\"],\"EatZYJ\":[\"다음 이미지\"],\"EdQY6l\":[\"없음\"],\"EnqLYU\":[\"서버 검색...\"],\"F0OKMc\":[\"서버 편집\"],\"F6Int2\":[\"강조 표시 활성화\"],\"FDoLyE\":[\"최대 사용자 수\"],\"FUU/hZ\":[\"채팅에서 로드할 외부 미디어의 범위를 제어하세요.\"],\"Fdp03t\":[\"켜기\"],\"FfPWR0\":[\"모달\"],\"FjkaiT\":[\"축소\"],\"FlqOE9\":[\"이것이 의미하는 바:\"],\"FolHNl\":[\"계정 및 인증 관리\"],\"Fp2Dif\":[\"서버에서 나갔습니다\"],\"G5KmCc\":[\"GZ-Line (글로벌 Z-Line)\"],\"GDs0lz\":[\"<0>위험:0> 민감한 정보(메시지, 비공개 대화, 인증 정보)가 네트워크 관리자나 IRC 서버 간에 위치한 공격자에게 노출될 수 있습니다.\"],\"GR+2I3\":[\"초대 마스크 추가 (예: nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"팝업 서버 알림 닫기\"],\"GdhD7H\":[\"íì¸íë\xA0¤ë©´ ë¤ì í´ë¦íì¸ì\"],\"GjRZex\":[\"네트워크 연결을 끊으시겠습니까?\"],\"GlHnXw\":[\"닉네임 변경 실패: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"미리보기:\"],\"GtmO8/\":[\"보낸 사람\"],\"GtuHUQ\":[\"서버에서 이 채널의 이름을 변경합니다. 모든 사용자에게 새 이름이 표시됩니다.\"],\"GuGfFX\":[\"검색 전환\"],\"GxkJXS\":[\"업로드 중...\"],\"GzbwnK\":[\"채널에 참여했습니다\"],\"GzsUDB\":[\"확장 프로필\"],\"H/PnT8\":[\"이모지 삽입\"],\"H6Izzl\":[\"선호하는 색상 코드\"],\"H9jIv+\":[\"입장/퇴장 표시\"],\"HAKBY9\":[\"파일 업로드\"],\"HdE1If\":[\"채널\"],\"Hk4AW9\":[\"선호하는 표시 이름\"],\"HmHDk7\":[\"멤버 선택\"],\"HrQzPU\":[[\"networkName\"],\"의 채널\"],\"I2tXQ5\":[\"@\",[\"0\"],\"에게 메시지 (Enter로 줄 바꿈, Shift+Enter로 전송)\"],\"I6bw/h\":[\"사용자 차단\"],\"I92Z+b\":[\"알림 활성화\"],\"I9D72S\":[\"이 메시지를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.\"],\"IA+1wo\":[\"사용자가 채널에서 추방될 때 표시\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"정보:\"],\"IUwGEM\":[\"변경 사항 저장\"],\"IVeGK6\":[[\"0\"],\"님, \",[\"1\"],\"님, \",[\"2\"],\"님이 입력 중...\"],\"IgrLD/\":[\"일시 정지\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"답장\"],\"IoHMnl\":[\"최대값은 \",[\"0\"],\"입니다\"],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"연결 중...\"],\"J5T9NW\":[\"사용자 정보\"],\"J8Y5+z\":[\"이런! 네트워크 분리! ⚠️\"],\"JBHkBA\":[\"채널을 나갔습니다\"],\"JCwL0Q\":[\"사유 입력 (선택 사항)\"],\"JFciKP\":[\"전환\"],\"JXGkhG\":[\"채널 이름 변경 (운영자 전용)\"],\"JcD7qf\":[\"추가 작업\"],\"JdkA+c\":[\"비밀 채널 (+s)\"],\"Jmu12l\":[\"서버 채널\"],\"JoQY+E\":[\"이렇게 하면 네트워크가 soju 바운서에서 제거됩니다. 다시 사용하려면 다시 추가해야 합니다.\"],\"JvQ++s\":[\"마크다운 활성화\"],\"K2jwh/\":[\"WHOIS 데이터를 사용할 수 없습니다\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"메시지 삭제\"],\"KKBlUU\":[\"임베드\"],\"KM0pLb\":[\"채널에 오신 것을 환영합니다!\"],\"KR6W2h\":[\"사용자 무시 해제\"],\"KV+Bi1\":[\"초대 전용 (+i)\"],\"KdCtwE\":[\"카운터를 초기화하기 전에 플러드 활동을 모니터링할 초 단위 시간\"],\"Kkezga\":[\"서버 비밀번호\"],\"KsiQ/8\":[\"채널 참여를 위해 초대가 필요합니다\"],\"L+gB/D\":[\"채널 정보\"],\"LC1a7n\":[\"IRC 서버에서 서버 간 링크의 보안 수준이 낮다고 보고했습니다. 즉, 메시지가 네트워크의 IRC 서버 간에 전달될 때 적절히 암호화되지 않거나 SSL/TLS 인증서가 올바르게 검증되지 않을 수 있습니다.\"],\"LEwpeL\":[\"soju 바운서(제어)\"],\"LNfLR5\":[\"추방 표시\"],\"LP+1Z7\":[\"네트워크 추가\"],\"LQb0W/\":[\"모든 이벤트 표시\"],\"LU7/yA\":[\"UI에 표시할 대체 이름입니다. 공백, 이모지, 특수 문자를 포함할 수 있습니다. IRC 명령에는 실제 채널 이름(\",[\"channelName\"],\")이 사용됩니다.\"],\"LUb9O7\":[\"올바른 서버 포트가 필요합니다\"],\"LV4fT6\":[\"ì¤ëª
(ì\xA0íì¬í, ì: \\\"3ë¶ê¸° ë²\xA0í í
ì¤í°\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"개인정보 처리방침\"],\"LcuSDR\":[\"프로필 정보 및 메타데이터 관리\"],\"LqLS9B\":[\"닉네임 변경 표시\"],\"LsDQt2\":[\"채널 설정\"],\"LtI9AS\":[\"소유자\"],\"LuNhhL\":[\"이 메시지에 반응했습니다\"],\"M/AZNG\":[\"아바타 이미지의 URL\"],\"M/WIer\":[\"메시지 보내기\"],\"M8er/5\":[\"이름:\"],\"MHk+7g\":[\"이전 이미지\"],\"MRorGe\":[\"사용자에게 PM\"],\"MVbSGP\":[\"시간 창 (초)\"],\"MkpcsT\":[\"메시지와 설정은 기기에 로컬로 저장됩니다\"],\"MzPdC2\":[\"서버 비밀번호 (PASS)\"],\"N/hDSy\":[\"봇으로 표시 - 보통 'on' 또는 비워두기\"],\"N6j2JH\":[[\"0\"],\" 편집\"],\"N7TQbE\":[[\"channelName\"],\"에 사용자 초대\"],\"NCca/o\":[\"기본 닉네임 입력...\"],\"Nqs6B9\":[\"모든 외부 미디어를 표시합니다. 모든 URL이 알 수 없는 서버에 요청을 보낼 수 있습니다.\"],\"Nt+9O7\":[\"원시 TCP 대신 WebSocket 사용\"],\"NxIHzc\":[\"사용자 연결 끊기\"],\"O+v/cL\":[\"서버의 모든 채널 검색\"],\"OCGpR4\":[\"(상속)\"],\"ODwSCk\":[\"GIF 보내기\"],\"OGQ5kK\":[\"알림 소리 및 강조 표시 설정\"],\"OIPt1Z\":[\"멤버 목록 사이드바 표시 또는 숨기기\"],\"OKSNq/\":[\"매우 엄격\"],\"ONWvwQ\":[\"업로드\"],\"OVKoQO\":[\"인증을 위한 계정 비밀번호\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - IRC를 미래로\"],\"OhCpra\":[\"주제 설정…\"],\"OkltoQ\":[\"닉네임으로 \",[\"username\"],\" 차단 (동일 닉으로 재입장 방지)\"],\"P+t/Te\":[\"추가 데이터 없음\"],\"P42Wcc\":[\"안전\"],\"PD38l0\":[\"채널 아바타 미리보기\"],\"PD9mEt\":[\"메시지를 입력하세요...\"],\"PPqfdA\":[\"채널 구성 설정 열기\"],\"PSCjfZ\":[\"이 채널에 표시될 주제입니다. 모든 사용자가 주제를 볼 수 있습니다.\"],\"PZCecv\":[\"PDF 미리보기\"],\"PeLgsC\":[[\"c\",\"plural\",{\"other\":[[\"c\"],\"번\"]}]],\"PguS2C\":[\"예외 마스크 추가 (예: nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[[\"0\"],\"개 채널 중 \",[\"displayedChannelsCount\"],\"개 표시\"],\"PqhVlJ\":[\"사용자 차단 (호스트마스크)\"],\"Q+chwU\":[\"사용자명:\"],\"Q2QY4/\":[\"ì´ ì´ë ìì\xA0\"],\"Q3v9Wc\":[\"예, 삭제\"],\"Q6hhn8\":[\"환경설정\"],\"QF4a34\":[\"사용자 이름을 입력하세요\"],\"QGqSZ2\":[\"색상 및 서식\"],\"QJQd1J\":[\"프로필 편집\"],\"QSzGDE\":[\"유휴\"],\"QUlny5\":[[\"0\"],\"에 오신 것을 환영합니다!\"],\"Qoq+GP\":[\"더 보기\"],\"QuSkCF\":[\"채널 필터링...\"],\"QwUrDZ\":[\"주제를 변경했습니다: \",[\"topic\"]],\"R0UH07\":[[\"1\"],\"개 중 \",[\"0\"],\"번째 이미지\"],\"R7SsBE\":[\"음소거\"],\"R8rf1X\":[\"클릭하여 주제 설정\"],\"RArB3D\":[[\"username\"],\"에 의해 \",[\"channelName\"],\"에서 추방당했습니다\"],\"RI3cWd\":[\"ObsidianIRC와 함께 IRC의 세계를 탐험하세요\"],\"RIfHS5\":[\"ì ì´ë ë§í¬ ìì±\"],\"RMMaN5\":[\"발언권 제한 (+m)\"],\"RWw9Lg\":[\"모달 닫기\"],\"RZ2BuZ\":[[\"account\"],\" 계정 등록에 인증이 필요합니다: \",[\"message\"]],\"RySp6q\":[\"댓글 숨기기\"],\"S5Togi\":[\"바운서에서 네트워크 불러오는 중…\"],\"SPKQTd\":[\"닉네임은 필수입니다\"],\"SPVjfj\":[\"비워두면 기본값인 '사유 없음'이 사용됩니다\"],\"SQKPvQ\":[\"사용자 초대\"],\"STmlpb\":[\"네트워크 목록으로 돌아가기\"],\"SkZcl+\":[\"미리 정의된 플러드 방지 프로필을 선택하세요. 각 프로필은 다양한 사용 사례에 맞게 균형 잡힌 보호 설정을 제공합니다.\"],\"Slr+3C\":[\"최소 사용자 수\"],\"Spnlre\":[[\"target\"],\"을(를) \",[\"channel\"],\"에 초대했습니다\"],\"T/ckN5\":[\"뷰어에서 열기\"],\"T91vKp\":[\"재생\"],\"TV2Wdu\":[\"데이터 처리 방식 및 개인정보 보호 방법을 알아보세요.\"],\"TgFpwD\":[\"적용 중...\"],\"TkzSFB\":[\"변경 사항 없음\"],\"TtserG\":[\"실명 입력\"],\"Ttz9J1\":[\"비밀번호 입력...\"],\"Tz0i8g\":[\"설정\"],\"U3pytU\":[\"관리자\"],\"UDb2YD\":[\"반응\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"ìì§ ì´ë ë§í¬ë¥¼ ìì±íì§ ìììµëë¤. ì ììì ì¬ì©íì¬ ì²« ë§í¬ë¥¼ ë§ë¤ì´ë³´ì¸ì.\"],\"UGT5vp\":[\"설정 저장\"],\"UV5hLB\":[\"차단된 사용자가 없습니다\"],\"Uaj3Nd\":[\"상태 메시지\"],\"Ue3uny\":[\"기본값 (프로필 없음)\"],\"UkARhe\":[\"기본 - 표준 보호\"],\"Umn7Cj\":[\"아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!\"],\"UtUIRh\":[\"이전 메시지 \",[\"0\"],\"개\"],\"UwzP+U\":[\"보안 연결\"],\"V0/A4O\":[\"채널 소유자\"],\"V0zZWc\":[\"아래의 바인딩된 네트워크도 함께 종료됩니다.\"],\"V4qgxE\":[\"생성 시각 이전 (분 전)\"],\"V8yTm6\":[\"검색 지우기\"],\"VJMMyz\":[\"ObsidianIRC - IRC를 미래로\"],\"VJScHU\":[\"사유\"],\"VLsmVV\":[\"알림 음소거\"],\"VbyRUy\":[\"댓글\"],\"Vmx0mQ\":[\"설정자:\"],\"VqnIZz\":[\"개인정보 처리방침 및 데이터 관행 보기\"],\"VrMygG\":[\"최소 길이는 \",[\"0\"],\"자입니다\"],\"VrnTui\":[\"프로필에 표시되는 대명사\"],\"W8E3qn\":[\"인증된 계정\"],\"WAakm9\":[\"채널 삭제\"],\"WFxTHC\":[\"차단 마스크 추가 (예: nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"서버 호스트는 필수입니다\"],\"WRYdXW\":[\"오디오 재생 위치\"],\"WUOH5B\":[\"사용자 무시\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"other\":[[\"1\"],\"개 더 보기\"]}]],\"WYxRzo\":[\"ì´ë ë§í¬ ìì± ë° ê´ë¦¬\"],\"Wd38W1\":[\"ì¼ë° ë¤í¸ìí¬ ì´ë를 ë³´ë´ë\xA0¤ë©´ ì±ëì ë¹ìëì¸ì. ì¤ëª
ì ë³¸ì¸ ê¸°ë¡ì©ì¼ ë¿ì´ë©° ì´ ëª©ë¡ìì 본ì¸ìê²ë§ íìë©ëë¤.\"],\"Weq9zb\":[\"일반\"],\"Wfj7Sk\":[\"알림 소리 음소거 또는 해제\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"사용자 프로필\"],\"X6S3lt\":[\"설정, 채널, 서버 검색...\"],\"XEHan5\":[\"계속 진행\"],\"XI1+wb\":[\"잘못된 형식\"],\"XIXeuC\":[\"@\",[\"0\"],\"에게 메시지 보내기\"],\"XMS+k4\":[\"비공개 메시지 시작\"],\"XWgxXq\":[\"앨범\"],\"Xd7+IT\":[\"비공개 채팅 고정 해제\"],\"Xm/s+u\":[\"화면 표시\"],\"Xp2n93\":[\"서버의 신뢰할 수 있는 파일 호스트의 미디어를 표시합니다. 외부 서비스에 요청이 전송되지 않습니다.\"],\"XvjC4F\":[\"저장 중...\"],\"Y/qryO\":[\"검색 결과와 일치하는 사용자가 없습니다\"],\"YAqRpI\":[[\"account\"],\" 계정 등록 성공: \",[\"message\"]],\"YEfzvP\":[\"주제 보호 (+t)\"],\"YQOn6a\":[\"멤버 목록 접기\"],\"YRCoE9\":[\"채널 Op\"],\"YURQaF\":[\"프로필 보기\"],\"YdBSvr\":[\"미디어 표시 및 외부 콘텐츠 제어\"],\"Yj6U3V\":[\"중앙 서버 없음:\"],\"YjvpGx\":[\"대명사\"],\"YqH4l4\":[\"키 없음\"],\"YyUPpV\":[\"계정:\"],\"ZJSWfw\":[\"서버 연결 종료 시 표시할 메시지\"],\"ZR1dJ4\":[\"초대\"],\"ZdWg0V\":[\"브라우저에서 열기\"],\"ZhRBbl\":[\"메시지 검색…\"],\"Zmcu3y\":[\"고급 필터\"],\"a0bHay\":[\"<0>\",[\"0\"],\"0> 연결을 끊으시겠습니까?\"],\"a2/8e5\":[\"주제 설정 이후 (분 전)\"],\"aHKcKc\":[\"이전 페이지\"],\"aJTbXX\":[\"Oper 비밀번호\"],\"aQryQv\":[\"이미 존재하는 패턴입니다\"],\"aW9pLN\":[\"채널에 허용되는 최대 사용자 수입니다. 제한 없이 두려면 비워두세요.\"],\"ah4fmZ\":[\"YouTube, Vimeo, SoundCloud 등 알려진 서비스의 미리보기도 표시합니다.\"],\"aifXak\":[\"이 채널에 미디어가 없습니다\"],\"ap2zBz\":[\"완화\"],\"az8lvo\":[\"끄기\"],\"azXSNo\":[\"멤버 목록 펼치기\"],\"azdliB\":[\"계정에 로그인\"],\"b26wlF\":[\"그녀/그녀의\"],\"bD/+Ei\":[\"엄격\"],\"bQ6BJn\":[\"상세한 플러드 방지 규칙을 설정하세요. 각 규칙은 모니터링할 활동 유형과 임계값 초과 시 취할 조치를 지정합니다.\"],\"beV7+y\":[\"사용자가 \",[\"channelName\"],\" 참여 초대를 받게 됩니다.\"],\"bk84cH\":[\"자리 비움 메시지\"],\"bkHdLj\":[\"IRC 서버 추가\"],\"bmQLn5\":[\"규칙 추가\"],\"bv4cFj\":[\"전송 방식\"],\"bwRvnp\":[\"작업\"],\"c8+EVZ\":[\"인증된 계정\"],\"cGYUlD\":[\"미디어 미리보기가 로드되지 않습니다.\"],\"cLF98o\":[\"댓글 보기 (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"사용 가능한 사용자가 없습니다\"],\"cSgpoS\":[\"비공개 채팅 고정\"],\"cXeEKu\":[\"아래의 바인딩된 네트워크 \",[\"0\"],\"개도 함께 종료됩니다.\"],\"cde3ce\":[\"<0>\",[\"0\"],\"0>에게 메시지\"],\"chQsxg\":[\"형식화된 출력 복사\"],\"cl/A5J\":[[\"__DEFAULT_IRC_SERVER_NAME__\"],\"에 오신 것을 환영합니다!\"],\"cnGeoo\":[\"삭제\"],\"coPLXT\":[\"IRC 통신 내용은 서버에 저장되지 않습니다\"],\"crYH/6\":[\"SoundCloud 플레이어\"],\"cv5DQb\":[\"호스트가 설정되지 않음\"],\"d3sis4\":[\"서버 추가\"],\"d9aN5k\":[\"채널에서 \",[\"username\"],\" 제거\"],\"dEgA5A\":[\"취소\"],\"dGi1We\":[\"이 비공개 메시지 대화 고정 해제\"],\"dJVuyC\":[[\"channelName\"],\"에서 나갔습니다 (\",[\"reason\"],\")\"],\"dMtLDE\":[\"받는 사람\"],\"dXqxlh\":[\"<0>⚠️ 보안 위험!0> 이 연결은 도청 또는 중간자 공격에 취약할 수 있습니다.\"],\"da9Q/R\":[\"채널 모드 변경됨\"],\"dhJN3N\":[\"댓글 보기\"],\"dj2xTE\":[\"알림 닫기\"],\"dpCzmC\":[\"플러드 방지 설정\"],\"e9dQpT\":[\"이 링크를 새 탭에서 여시겠습니까?\"],\"ePK91l\":[\"편집\"],\"eYBDuB\":[\"이미지를 업로드하거나 동적 크기 조정을 위해 \",[\"size\"],\" 대체가 있는 URL을 제공하세요\"],\"edBbee\":[\"호스트마스크로 \",[\"username\"],\" 차단 (동일 IP/호스트에서 재입장 방지)\"],\"ekfzWq\":[\"사용자 설정\"],\"elPDWs\":[\"IRC 클라이언트 환경을 맞춤 설정하세요\"],\"eu2osY\":[\"<0>💡 권장사항:0> 이 서버를 신뢰하고 위험을 충분히 이해한 경우에만 계속하세요. 이 연결을 통해 민감한 정보나 비밀번호를 공유하지 마세요.\"],\"euEhbr\":[[\"channel\"],\"에 참여하려면 클릭\"],\"ez3vLd\":[\"여러 줄 입력 활성화\"],\"f0J5Ki\":[\"서버 간 통신에 암호화되지 않은 연결이 사용될 수 있습니다\"],\"f9BHJk\":[\"사용자 경고\"],\"fDOLLd\":[\"채널을 찾을 수 없습니다.\"],\"ffzDkB\":[\"익명 분석:\"],\"fq1GF9\":[\"사용자가 서버에서 연결을 끊을 때 표시\"],\"gCldcN\":[\"강조 색상 변경\"],\"gEF57C\":[\"이 서버는 하나의 연결 유형만 지원합니다\"],\"gJuLUI\":[\"무시 목록\"],\"gNzMrk\":[\"현재 아바타\"],\"gjPWyO\":[\"닉네임 입력...\"],\"gz6UQ3\":[\"최대화\"],\"h6/IMX\":[\"첫 번째 네트워크 추가\"],\"h6razj\":[\"채널 이름 마스크 제외\"],\"hG6jnw\":[\"설정된 주제 없음\"],\"hG89Ed\":[\"이미지\"],\"hYgDIe\":[\"ìì±\"],\"hZ6znB\":[\"포트\"],\"ha+Bz5\":[\"예: 100:1440\"],\"he3ygx\":[\"ë³µì¬\"],\"hehnjM\":[\"횟수\"],\"hzdLuQ\":[\"Voice 이상의 권한을 가진 사용자만 발언 가능\"],\"i0qMbr\":[\"홈\"],\"iDNBZe\":[\"알림\"],\"iH8pgl\":[\"뒤로\"],\"iL9SZg\":[\"사용자 차단 (닉네임)\"],\"iNt+3c\":[\"이미지로 돌아가기\"],\"iQvi+a\":[\"이 서버의 낮은 링크 보안에 대해 다시 경고하지 않음\"],\"iSLIjg\":[\"연결\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"서버 호스트\"],\"idD8Ev\":[\"저장됨\"],\"iivqkW\":[\"접속 시각\"],\"ij+Elv\":[\"이미지 미리보기\"],\"ilIWp7\":[\"알림 전환\"],\"iuaqvB\":[\"와일드카드로 *를 사용하세요. 예: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"봇\"],\"j2DGR0\":[\"호스트마스크로 차단\"],\"jA4uoI\":[\"주제:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"사유 (선택 사항)\"],\"jUV7CU\":[\"아바타 업로드\"],\"jW5Uwh\":[\"로드할 외부 미디어의 범위를 제어합니다. 끄기 / 안전 / 신뢰할 수 있는 출처 / 모든 콘텐츠.\"],\"jXzms5\":[\"첨부 옵션\"],\"jZlrte\":[\"색상\"],\"jfC/xh\":[\"연락처\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"이전 메시지 불러오기\"],\"k3ID0F\":[\"멤버 필터링…\"],\"k65gsE\":[\"자세히 보기\"],\"k7Zgob\":[\"연결 취소\"],\"kAVx5h\":[\"초대가 없습니다\"],\"kCLEPU\":[\"연결된 서버\"],\"kF5LKb\":[\"무시된 패턴:\"],\"kGeOx/\":[[\"0\"],\" 참가\"],\"kITKr8\":[\"채널 모드 불러오는 중...\"],\"kPpPsw\":[\"당신은 IRC Operator입니다\"],\"kWJmRL\":[\"나\"],\"kfcRb0\":[\"아바타\"],\"kjMqSj\":[\"JSON 복사\"],\"krViRy\":[\"JSON으로 복사하려면 클릭\"],\"ks71ra\":[\"예외\"],\"kw4lRv\":[\"채널 Halfop\"],\"kxgIRq\":[\"시작하려면 채널을 선택하거나 추가하세요.\"],\"ky6dWe\":[\"아바타 미리보기\"],\"l+GxCv\":[\"채널 불러오는 중...\"],\"l+IUVW\":[[\"account\"],\" 계정 인증 성공: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"other\":[[\"reconnectCount\"],\"번 재연결됨\"]}]],\"l1l8sj\":[[\"0\"],\"ì¼ ì\xA0\"],\"l5NhnV\":[\"#ì±ë (ì\xA0íì¬í)\"],\"l5jmzx\":[[\"0\"],\"님과 \",[\"1\"],\"님이 입력 중...\"],\"lCF0wC\":[\"ìë¡ ê³\xA0침\"],\"lHy8N5\":[\"채널 더 불러오는 중...\"],\"lasgrr\":[\"ì¬ì©ë¨\"],\"lbpf14\":[[\"value\"],\" 참여\"],\"lfFsZ4\":[\"채널\"],\"lkNdiH\":[\"계정 이름\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"이미지 업로드\"],\"loQxaJ\":[\"돌아왔습니다\"],\"lvfaxv\":[\"홈\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"추가\"],\"m8flAk\":[\"미리보기 (아직 업로드되지 않음)\"],\"mEPxTp\":[\"<0>⚠️ 주의하세요!0> 신뢰할 수 있는 출처의 링크만 여세요. 악성 링크는 보안이나 개인정보를 침해할 수 있습니다.\"],\"mHGdhG\":[\"서버 정보\"],\"mHS8lb\":[\"#\",[\"0\"],\"에 메시지 보내기\"],\"mMYBD9\":[\"광역 - 더 넓은 보호 범위\"],\"mTGsPd\":[\"채널 주제\"],\"mU8j6O\":[\"외부 메시지 차단 (+n)\"],\"mZp8FL\":[\"자동으로 한 줄 모드로 전환\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"댓글 (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"인증된 사용자\"],\"mwtcGl\":[\"댓글 닫기\"],\"myL0MR\":[\"이 네트워크를 삭제하시겠습니까?\"],\"mzI/c+\":[\"다운로드\"],\"n3fGRk\":[[\"0\"],\"이(가) 설정\"],\"n5+j9l\":[\"예: <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"완화 - 덜 공격적인 보호\"],\"nNflMD\":[\"채널 나가기\"],\"nPXkBi\":[\"WHOIS 데이터 불러오는 중...\"],\"nQnxxF\":[\"#\",[\"0\"],\"에 메시지 (Shift+Enter로 줄 바꿈)\"],\"nWMRxa\":[\"고정 해제\"],\"nkC032\":[\"플러드 프로필 없음\"],\"o69z4d\":[[\"username\"],\"에게 경고 메시지 보내기\"],\"o9ylQi\":[\"GIF를 검색하여 시작하세요\"],\"oFGkER\":[\"서버 알림\"],\"oOi11l\":[\"맨 아래로 스크롤\"],\"oPYIL5\":[\"ë¤í¸ìí¬\"],\"oQEzQR\":[\"새 DM\"],\"oXOSPE\":[\"온라인\"],\"oal760\":[\"서버 링크에 대한 중간자 공격이 가능합니다\"],\"oeqmmJ\":[\"신뢰할 수 있는 출처\"],\"optX0N\":[[\"0\"],\"ìê° ì\xA0\"],\"ovBPCi\":[\"기본값\"],\"p0Z69r\":[\"패턴은 비워둘 수 없습니다\"],\"p1KgtK\":[\"오디오를 불러오지 못했습니다\"],\"p59pEv\":[\"추가 세부정보\"],\"p7sRI6\":[\"입력 중임을 다른 사람에게 알림\"],\"pBm1od\":[\"비밀 채널\"],\"pNmiXx\":[\"모든 서버에 사용할 기본 닉네임\"],\"pUUo9G\":[\"호스트명:\"],\"pVGPmz\":[\"계정 비밀번호\"],\"peNE68\":[\"영구\"],\"plhHQt\":[\"데이터 없음\"],\"pm6+q5\":[\"보안 경고\"],\"pn5qSs\":[\"추가 정보\"],\"q0cR4S\":[\"이제 **\",[\"newNick\"],\"**(으)로 알려져 있습니다\"],\"qFcunY\":[\"LIST 또는 NAMES 명령에 채널이 표시되지 않음\"],\"qLpTm/\":[[\"emoji\"],\" 반응 제거\"],\"qVkGWK\":[\"고정\"],\"qY8wNa\":[\"홈페이지\"],\"qb0xJ7\":[\"와일드카드 사용: *는 임의의 문자열, ?는 임의의 단일 문자. 예: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"채널 키 (+k)\"],\"qtoOYG\":[\"제한 없음\"],\"r1W2AS\":[\"파일 호스트 이미지\"],\"rIPR2O\":[\"주제 설정 이전 (분 전)\"],\"rMMSYo\":[\"최대 길이는 \",[\"0\"],\"자입니다\"],\"rWtzQe\":[\"네트워크가 분리되었다가 재연결되었습니다. ✅\"],\"rYG2u6\":[\"잠시만 기다려 주세요...\"],\"rdUucN\":[\"미리보기\"],\"rjGI/Q\":[\"개인정보 보호\"],\"rk8iDX\":[\"GIF 불러오는 중...\"],\"rn6SBY\":[\"음소거 해제\"],\"s/UKqq\":[\"채널에서 추방되었습니다\"],\"s7oqXR\":[\"네트워크 선택\"],\"s8cATI\":[[\"channelName\"],\"에 참가했습니다\"],\"sCO9ue\":[\"<0>\",[\"serverName\"],\"0>에 대한 연결에 다음과 같은 보안 문제가 있습니다:\"],\"sGH11W\":[\"서버\"],\"sHI1H+\":[\"이제 **\",[\"newNick\"],\"**(으)로 알려져 있습니다\"],\"sJyV04\":[[\"inviter\"],\"이(가) 당신을 \",[\"channel\"],\"에 초대했습니다\"],\"sUBSbK\":[\"아직 업스트림 네트워크가 없습니다.\"],\"sby+1/\":[\"클릭하여 복사\"],\"sfN25C\":[\"실명 또는 전체 이름\"],\"sliuzR\":[\"링크 열기\"],\"sqrO9R\":[\"사용자 정의 멘션\"],\"sr6RdJ\":[\"Shift+Enter로 여러 줄 입력\"],\"swrCpB\":[[\"user\"],\"이(가) 채널을 \",[\"oldName\"],\"에서 \",[\"newName\"],\"(으)로 이름을 변경했습니다\",[\"0\"]],\"sxkWRg\":[\"고급\"],\"t/YqKh\":[\"제거\"],\"t47eHD\":[\"이 서버에서의 고유 식별자\"],\"tAkAh0\":[\"동적 크기 조정을 위한 \",[\"size\"],\" 대체가 있는 URL. 예: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"채널 목록 사이드바 표시 또는 숨기기\"],\"tfDRzk\":[\"저장\"],\"tiBsJk\":[[\"channelName\"],\"에서 나갔습니다\"],\"tt4/UD\":[\"서버를 나갔습니다 (\",[\"reason\"],\")\"],\"u0TcnO\":[\"닉네임 {nick}이(가) 이미 사용 중입니다. {newNick}(으)로 다시 시도합니다\"],\"u0a8B4\":[\"관리 권한을 위해 IRC Operator로 인증\"],\"u0rWFU\":[\"생성 시각 이후 (분 전)\"],\"u72w3t\":[\"무시할 사용자 및 패턴\"],\"u7jc2L\":[\"서버를 나갔습니다\"],\"uAQUqI\":[\"상태\"],\"uB85T3\":[\"저장 실패: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC 서버:\"],\"ukyW4o\":[\"ë´ ì´ë ë§í¬\"],\"usSSr/\":[\"확대/축소 수준\"],\"v7uvcf\":[\"소프트웨어:\"],\"vE8kb+\":[\"줄 바꿈은 Shift+Enter (Enter로 전송)\"],\"vERlcd\":[\"프로필\"],\"vK0RL8\":[\"주제 없음\"],\"vSJd18\":[\"동영상\"],\"vXIe7J\":[\"언어\"],\"vaHYxN\":[\"실명\"],\"vhjbKr\":[\"자리 비움\"],\"w/nogd\":[[\"0\"],\"개 네트워크\",[\"1\"],\" — 참가할 항목 선택\"],\"w4NYox\":[[\"title\"],\" 클라이언트\"],\"w8xQRx\":[\"잘못된 값\"],\"wFjjxZ\":[[\"username\"],\"에 의해 \",[\"channelName\"],\"에서 추방당했습니다 (\",[\"reason\"],\")\"],\"wGjaGl\":[\"차단 예외가 없습니다\"],\"wPrGnM\":[\"채널 관리자\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"사용자가 채널에 입장하거나 퇴장할 때 표시\"],\"whqZ9r\":[\"강조할 추가 단어 또는 문구\"],\"wm7RV4\":[\"알림 소리\"],\"wz/Yoq\":[\"서버 간 전달 시 메시지가 도청될 수 있습니다\"],\"x3+y8b\":[\"ì´ ë§í¬ë¥¼ íµí´ ë±ë¡í ì¬ë ì\"],\"xCJdfg\":[\"지우기\"],\"xOTzt5\":[\"ë°©ê¸\"],\"xUHRTR\":[\"연결 시 자동으로 operator로 인증\"],\"xWHwwQ\":[\"차단 목록\"],\"xYilR2\":[\"미디어\"],\"xbi8D6\":[\"ì´ ìë²ë ì´ë ë§í¬ë¥¼ ì§ìíì§ ììµëë¤ (<0>obby.world/invitation0> 기ë¥ì´ ê´ê³\xA0ëì§ ìì). ì¼ë° ì±í
ì ê³ìí\xA0 ì ìì¼ë©°, ì´ í¨ëì obbyircd ê¸°ë° ë¤í¸ìí¬ì©ì
ëë¤.\"],\"xceQrO\":[\"보안 웹소켓만 지원됩니다\"],\"xdtXa+\":[\"채널-이름\"],\"xfXC7q\":[\"텍스트 채널\"],\"xlCYOE\":[\"메시지를 더 불러오는 중...\"],\"xlhswE\":[\"최솟값은 \",[\"0\"],\"입니다\"],\"xq97Ci\":[\"단어나 문구 추가...\"],\"xuRqRq\":[\"클라이언트 제한 (+l)\"],\"xwF+7J\":[[\"0\"],\"님이 입력 중...\"],\"y1eoq1\":[\"ë§í¬ ë³µì¬\"],\"yJztBY\":[\"네트워크 삭제\"],\"yNeucF\":[\"이 서버는 확장 프로필 메타데이터(IRCv3 METADATA 확장)를 지원하지 않습니다. 아바타, 표시 이름, 상태 등의 추가 필드를 사용할 수 없습니다.\"],\"yPlrca\":[\"채널 아바타\"],\"yQE2r9\":[\"로딩 중\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Oper 사용자 이름\"],\"yYOzWD\":[\"로그\"],\"yfx9Re\":[\"IRC operator 비밀번호\"],\"ygCKqB\":[\"정지\"],\"ymDxJx\":[\"IRC operator 사용자 이름\"],\"yrpRsQ\":[\"이름순 정렬\"],\"yz7wBu\":[\"닫기\"],\"zJw+jA\":[\"모드 설정: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"ë¶ ì\xA0\"],\"zebeLu\":[\"oper 사용자 이름 입력\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/ko/messages.po b/src/locales/ko/messages.po
index 6ea3506c..1bbf0fb5 100644
--- a/src/locales/ko/messages.po
+++ b/src/locales/ko/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - IRC를 미래로"
msgid "— open in viewer"
msgstr "— 뷰어에서 열기"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(상속)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(변경 없음)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0}님과 {1}님이 입력 중..."
msgid "{0} is typing..."
msgstr "{0}님이 입력 중..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0}개 네트워크{1} — 참가할 항목 선택"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -69,17 +85,17 @@ msgstr "{0}님, {1}님, {2}님 외 {3}명이 입력 중..."
#. placeholder {0}: Math.floor(secs / 86400)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}d ago"
-msgstr "{0}일 전"
+msgstr "{0}ì¼ ì "
#. placeholder {0}: Math.floor(secs / 3600)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}h ago"
-msgstr "{0}시간 전"
+msgstr "{0}ìê° ì "
#. placeholder {0}: Math.floor(secs / 60)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}m ago"
-msgstr "{0}분 전"
+msgstr "{0}ë¶ ì "
#: src/lib/eventGrouping.ts
msgid "{c, plural, one {1 time} other {{c} times}}"
@@ -115,7 +131,7 @@ msgstr "*spam*"
#: src/components/ui/InvitationsPanel.tsx
msgid "#channel (optional)"
-msgstr "#채널 (선택사항)"
+msgstr "#ì±ë (ì íì¬í)"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "#new-channel-name"
@@ -205,6 +221,12 @@ msgstr "초대 마스크 추가 (예: nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "IRC 서버 추가"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "네트워크 추가"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "규칙 추가"
msgid "Add Server"
msgstr "서버 추가"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "첫 번째 네트워크 추가"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "추가 세부정보"
@@ -377,6 +403,10 @@ msgstr "뒤로"
msgid "Back to image"
msgstr "이미지로 돌아가기"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "네트워크 목록으로 돌아가기"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "호스트마스크로 {username} 차단 (동일 IP/호스트에서 재입장 방지)"
@@ -424,6 +454,10 @@ msgstr "서버의 모든 채널 검색"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "연결 취소"
msgid "Cancel reply"
msgstr "답장 취소"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "강조 색상 변경"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "채널 이름 변경 (운영자 전용)"
@@ -564,7 +603,7 @@ msgstr "검색 지우기"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "확인하려면 다시 클릭하세요"
+msgstr "íì¸íë ¤ë©´ ë¤ì í´ë¦íì¸ì"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "클라이언트 제한 (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -666,9 +707,10 @@ msgstr "알림 소리 및 강조 표시 설정"
#: src/components/ui/InvitationsPanel.tsx
msgid "Confirm?"
-msgstr "확인하시겠습니까?"
+msgstr "íì¸íìê² ìµëê¹?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "연결"
@@ -711,11 +753,11 @@ msgstr "복사됨"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy"
-msgstr "복사"
+msgstr "ë³µì¬"
#: src/components/ui/RawLogViewer.tsx
msgid "Copy all"
-msgstr "모두 복사"
+msgstr "모ë ë³µì¬"
#: src/components/message/JsonLogMessage.tsx
msgid "Copy entire JSON"
@@ -731,7 +773,7 @@ msgstr "JSON 복사"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy link"
-msgstr "링크 복사"
+msgstr "ë§í¬ ë³µì¬"
#: src/components/ui/ExternalLinkWarningModal.tsx
msgid "Copy URL"
@@ -739,15 +781,15 @@ msgstr "URL 복사"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create"
-msgstr "생성"
+msgstr "ìì±"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "새 초대 링크 생성"
+msgstr "ì ì´ë ë§í¬ ìì±"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "초대 링크 생성 및 관리"
+msgstr "ì´ë ë§í¬ ìì± ë° ê´ë¦¬"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,27 +856,52 @@ msgstr "채널 삭제"
msgid "Delete message"
msgstr "메시지 삭제"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "네트워크 삭제"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "비공개 채팅 삭제"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "이 초대 삭제"
+msgstr "ì´ ì´ë ìì "
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "이 메시지를 삭제하시겠습니까? 이 작업은 취소할 수 없습니다."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "이 네트워크를 삭제하시겠습니까?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "설명 (선택사항, 예: \"3분기 베타 테스터\")"
+msgstr "ì¤ëª
(ì íì¬í, ì: \"3ë¶ê¸° ë² í í
ì¤í°\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "연결 끊기"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "<0>{0}0> 연결을 끊으시겠습니까?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "soju 바운서에서 연결을 끊으시겠습니까?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "네트워크 연결을 끊으시겠습니까?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "채널 탐색"
@@ -891,20 +958,31 @@ msgstr "다운로드"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "업로드할 파일을 끌어다 놓으세요"
+msgstr "ì
ë¡ëí íì¼ì ëì´ë¤ ëì¼ì¸ì"
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "예: <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "예: 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "편집"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "{0} 편집"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "프로필 편집"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "홈"
msgid "Homepage"
msgstr "홈페이지"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "호스트"
@@ -1285,7 +1364,7 @@ msgstr "채널에 참여했습니다"
#: src/components/ui/InvitationsPanel.tsx
msgid "just now"
-msgstr "방금"
+msgstr "ë°©ê¸"
#: src/components/ui/ModerationModal.tsx
#: src/components/ui/UserContextMenu.tsx
@@ -1323,7 +1402,7 @@ msgstr "채널 나가기"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "일반 네트워크 초대를 보내려면 채널을 비워두세요. 설명은 본인 기록용일 뿐이며 이 목록에서 본인에게만 표시됩니다."
+msgstr "ì¼ë° ë¤í¸ìí¬ ì´ë를 ë³´ë´ë ¤ë©´ ì±ëì ë¹ìëì¸ì. ì¤ëª
ì ë³¸ì¸ ê¸°ë¡ì©ì¼ ë¿ì´ë©° ì´ ëª©ë¡ìì 본ì¸ìê²ë§ íìë©ëë¤."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "채널을 나갔습니다"
msgid "Let others know when you are typing"
msgstr "입력 중임을 다른 사람에게 알림"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "링크 미리보기"
@@ -1375,6 +1458,10 @@ msgstr "GIF 불러오는 중..."
msgid "Loading more channels..."
msgstr "채널 더 불러오는 중..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "바운서에서 네트워크 불러오는 중…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "WHOIS 데이터 불러오는 중..."
@@ -1563,12 +1650,23 @@ msgstr "이름:"
#: src/components/ui/InvitationsPanel.tsx
msgid "network"
-msgstr "네트워크"
+msgstr "ë¤í¸ìí¬"
+
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "soju 바운서를 통해 바인딩된 네트워크"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "네트워크 이름"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "{0}의 네트워크"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "새 DM"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (예: spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "파일 선택 안 함"
msgid "No flood profile"
msgstr "플러드 프로필 없음"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "호스트가 설정되지 않음"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "초대가 없습니다"
@@ -1672,7 +1775,7 @@ msgstr "미디어 미리보기가 로드되지 않습니다."
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "아직 캡처된 IRC 원시 트래픽이 없습니다. 연결하거나 메시지를 보내보세요."
+msgstr "ìì§ ìº¡ì²ë IRC ìì í¸ëí½ì´ ììµëë¤. ì°ê²°íê±°ë ë©ìì§ë¥¼ ë³´ë´ë³´ì¸ì."
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "결과를 찾을 수 없습니다"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "선택된 서버가 없습니다. 먼저 사이드바에서 서버를 선택하세요. 초대 링크는 서버별로 관리됩니다."
+msgstr "ì íë ìë²ê° ììµëë¤. 먼ì ì¬ì´ëë°ìì ìë²ë¥¼ ì ííì¸ì. ì´ë ë§í¬ë ìë²ë³ë¡ ê´ë¦¬ë©ëë¤."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "설정된 주제 없음"
msgid "No unread mentions or messages"
msgstr "읽지 않은 멘션이나 메시지가 없습니다"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "아직 업스트림 네트워크가 없습니다."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "사용 가능한 사용자가 없습니다"
@@ -1784,6 +1891,10 @@ msgstr "이런! 네트워크 분리! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "열기"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "채널 구성 설정 열기"
@@ -1887,6 +1998,10 @@ msgstr "비공개 채팅 고정"
msgid "Pin this private message conversation"
msgstr "이 비공개 메시지 대화 고정"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "평문"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "사용자에게 PM"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "포트"
@@ -2006,6 +2122,7 @@ msgstr "이 메시지에 반응했습니다"
msgid "Read more"
msgstr "더 보기"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "사유"
msgid "Reason (optional)"
msgstr "사유 (선택 사항)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "서버에 재연결"
@@ -2031,7 +2149,7 @@ msgstr "서버에 재연결"
#: src/components/ui/InvitationsPanel.tsx
#: src/components/ui/InvitationsPanel.tsx
msgid "Refresh"
-msgstr "새로 고침"
+msgstr "ìë¡ ê³ ì¹¨"
#: src/components/ui/AddServerModal.tsx
msgid "Register for an account"
@@ -2095,6 +2213,7 @@ msgstr "규칙"
msgid "Safe"
msgstr "안전"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "탐색"
msgid "Select a channel"
msgstr "채널 선택"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "네트워크 선택"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "멤버 선택"
@@ -2276,6 +2399,10 @@ msgstr "네트워크 서버 운영자가 메시지를 읽을 수 있습니다"
msgid "Server Password"
msgstr "서버 비밀번호"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "서버 비밀번호 (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "서버 간 통신에 암호화되지 않은 연결이 사용될 수 있습니다"
@@ -2380,6 +2507,12 @@ msgstr "접속 시각"
msgid "Software:"
msgstr "소프트웨어:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "soju 바운서(제어)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "이름순 정렬"
@@ -2457,7 +2590,11 @@ msgstr "이 이미지가 만료되었습니다"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "이 링크를 통해 등록한 사람 수"
+msgstr "ì´ ë§í¬ë¥¼ íµí´ ë±ë¡í ì¬ë ì"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "이렇게 하면 네트워크가 soju 바운서에서 제거됩니다. 다시 사용하려면 다시 추가해야 합니다."
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "이 서버는 확장 프로필 메타데이터(IRCv3 METADATA 확장)를
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "이 서버는 초대 링크를 지원하지 않습니다 (<0>obby.world/invitation0> 기능이 광고되지 않음). 일반 채팅은 계속할 수 있으며, 이 패널은 obbyircd 기반 네트워크용입니다."
+msgstr "ì´ ìë²ë ì´ë ë§í¬ë¥¼ ì§ìíì§ ììµëë¤ (<0>obby.world/invitation0> 기ë¥ì´ ê´ê³ ëì§ ìì). ì¼ë° ì±í
ì ê³ìí ì ìì¼ë©°, ì´ í¨ëì obbyircd ê¸°ë° ë¤í¸ìí¬ì©ì
ëë¤."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "이 서버는 하나의 연결 유형만 지원합니다"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "아래의 바인딩된 네트워크 {0}개도 함께 종료됩니다."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "아래의 바인딩된 네트워크도 함께 종료됩니다."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "시간 (분)"
@@ -2479,6 +2625,10 @@ msgstr "시간 (분)"
msgid "Time Window (seconds)"
msgstr "시간 창 (초)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "주제:"
msgid "Total: {0}"
msgstr "전체: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "전송 방식"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "신뢰할 수 있는 출처"
@@ -2615,7 +2769,7 @@ msgstr "와일드카드 사용: *는 임의의 문자열, ?는 임의의 단일
#: src/components/ui/InvitationsPanel.tsx
msgid "used"
-msgstr "사용됨"
+msgstr "ì¬ì©ë¨"
#: src/components/message/JsonLogMessage.tsx
#: src/components/ui/UserProfileModal.tsx
@@ -2641,6 +2795,7 @@ msgstr "사용자 프로필"
msgid "User Settings"
msgstr "사용자 설정"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "광역 - 더 넓은 보호 범위"
msgid "Will default to 'no reason' if left empty"
msgstr "비워두면 기본값인 '사유 없음'이 사용됩니다"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "예, 삭제"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "저장되지 않은 변경 사항이 있습니다. 저장하지 않고
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "아직 초대 링크를 생성하지 않았습니다. 위 양식을 사용하여 첫 링크를 만들어보세요."
+msgstr "ìì§ ì´ë ë§í¬ë¥¼ ìì±íì§ ìììµëë¤. ì ììì ì¬ì©íì¬ ì²« ë§í¬ë¥¼ ë§ë¤ì´ë³´ì¸ì."
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "{target}을(를) {channel}에 초대했습니다"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "<0>{0}0>에 연결되어 있습니다."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "인증을 위한 계정 비밀번호"
@@ -2822,13 +2986,17 @@ msgstr "인증을 위한 계정 비밀번호"
msgid "Your account username for authentication"
msgstr "인증을 위한 계정 사용자 이름"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "바운서에 아직 네트워크가 없습니다. 시작하려면 하나를 추가하세요."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "모든 서버에 사용할 기본 닉네임"
#: src/components/ui/InvitationsPanel.tsx
msgid "Your invite links"
-msgstr "내 초대 링크"
+msgstr "ë´ ì´ë ë§í¬"
#: src/components/ui/UserSettings.tsx
msgid "Your messages and settings are stored locally on your device"
diff --git a/src/locales/nl/messages.mjs b/src/locales/nl/messages.mjs
index 495f055d..ce702820 100644
--- a/src/locales/nl/messages.mjs
+++ b/src/locales/nl/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Ongeldig patroonformaat. Gebruik het formaat nick!gebruiker@host (jokerteken * toegestaan)\"],\"+6NQQA\":[\"Algemeen ondersteuningskanaal\"],\"+6NyRG\":[\"Client\"],\"+K0AvT\":[\"Verbreken\"],\"+cyFdH\":[\"Standaardbericht wanneer je jezelf als afwezig markeert\"],\"+mVPqU\":[\"Markdown-opmaak in berichten weergeven\"],\"+vqCJH\":[\"Je accountgebruikersnaam voor authenticatie\"],\"+yPBXI\":[\"Bestand kiezen\"],\"+zy2Nq\":[\"Type\"],\"/09cao\":[\"Lage verbindingsbeveiliging (niveau \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Gebruikers buiten het kanaal kunnen er geen berichten naar sturen\"],\"/4C8U0\":[\"Alles kopiëren\"],\"/6BzZF\":[\"Ledenlijst aan/uit\"],\"/AkXyp\":[\"Bevestigen?\"],\"/TNOPk\":[\"Gebruiker is afwezig\"],\"/XQgft\":[\"Ontdekken\"],\"/cF7Rs\":[\"Volume\"],\"/dqduX\":[\"Volgende pagina\"],\"/fc3q4\":[\"Alle inhoud\"],\"/kISDh\":[\"Meldingsgeluiden inschakelen\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Geluiden afspelen voor vermeldingen en berichten\"],\"0/0ZGA\":[\"Kanaalnaammasker\"],\"0D6j7U\":[\"Meer informatie over aangepaste regels →\"],\"0XsHcR\":[\"Gebruiker verwijderen\"],\"0ZpE//\":[\"Sorteren op gebruikers\"],\"0bEPwz\":[\"Afwezig instellen\"],\"0dGkPt\":[\"Kanaallijst uitvouwen\"],\"0gS7M5\":[\"Weergavenaam\"],\"0kS+M8\":[\"VoorbeeldNET\"],\"0rgoY7\":[\"Alleen verbinden met servers die jij kiest\"],\"0wdd7X\":[\"Deelnemen\"],\"0wkVYx\":[\"Privéberichten\"],\"111uHX\":[\"Linkvoorbeeldweergave\"],\"196EG4\":[\"Privégesprek verwijderen\"],\"1DSr1i\":[\"Registreren voor een account\"],\"1O/24y\":[\"Kanaallijst aan/uit\"],\"1VPJJ2\":[\"Waarschuwing externe link\"],\"1ZC/dv\":[\"Geen ongelezen vermeldingen of berichten\"],\"1pO1zi\":[\"Servernaam is vereist\"],\"1uwfzQ\":[\"Kanaalonderwerp bekijken\"],\"268g7c\":[\"Weergavenaam invoeren\"],\"2F9+AZ\":[\"Nog geen ruw IRC-verkeer vastgelegd. Probeer verbinding te maken of een bericht te sturen.\"],\"2FOFq1\":[\"Serveroperators op het netwerk kunnen mogelijk je berichten lezen\"],\"2FYpfJ\":[\"Meer\"],\"2HF1Y2\":[[\"inviter\"],\" heeft \",[\"target\"],\" uitgenodigd om deel te nemen aan \",[\"channel\"]],\"2I70QL\":[\"Gebruikersprofielinformatie bekijken\"],\"2QYdmE\":[\"Gebruikers:\"],\"2QpEjG\":[\"heeft verlaten\"],\"2YE223\":[\"Bericht in #\",[\"0\"],\" (Enter voor nieuwe regel, Shift+Enter om te verzenden)\"],\"2bimFY\":[\"Serverwachtwoord gebruiken\"],\"2iTmdZ\":[\"Lokale opslag:\"],\"2odkwe\":[\"Streng — Agressievere beveiliging\"],\"2uDhbA\":[\"Gebruikersnaam invoeren om uit te nodigen\"],\"2ygf/L\":[\"← Terug\"],\"2zEgxj\":[\"GIF's zoeken...\"],\"3RdPhl\":[\"Kanaal hernoemen\"],\"3THokf\":[\"Gebruiker met voice\"],\"3TSz9S\":[\"Minimaliseren\"],\"3jBDvM\":[\"Weergavenaam van kanaal\"],\"3ryuFU\":[\"Optionele crashrapporten om de app te verbeteren\"],\"3uBF/8\":[\"Viewer sluiten\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Voer accountnaam in...\"],\"4/Rr0R\":[\"Een gebruiker uitnodigen voor het huidige kanaal\"],\"4EZrJN\":[\"Regels\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Floodprofiel (+F)\"],\"4RZQRK\":[\"Wat ben je aan het doen?\"],\"4hfTrB\":[\"Nickname\"],\"4n99LO\":[\"Al in \",[\"0\"]],\"4t6vMV\":[\"Automatisch overschakelen naar één regel voor korte berichten\"],\"4vsHmf\":[\"Tijd (min)\"],\"5+INAX\":[\"Berichten markeren die jou vermelden\"],\"5R5Pv/\":[\"Oper-naam\"],\"678PKt\":[\"Netwerknaam\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Wachtwoord vereist om het kanaal te betreden. Laat leeg om de sleutel te verwijderen.\"],\"6HhMs3\":[\"Afsluitbericht\"],\"6V3Ea3\":[\"Gekopieerd\"],\"6lGV3K\":[\"Minder weergeven\"],\"6yFOEi\":[\"Voer oper-wachtwoord in...\"],\"7+IHTZ\":[\"Geen bestand gekozen\"],\"73hrRi\":[\"nick!gebruiker@host (bijv. spam*!*@*, *!*@slechtehost.com)\"],\"7QkKyN\":[\"Privébericht sturen\"],\"7U1W7c\":[\"Zeer ontspannen\"],\"7Y1YQj\":[\"Echte naam:\"],\"7YHArF\":[\"— openen in viewer\"],\"7fjnVl\":[\"Gebruikers zoeken...\"],\"7jL88x\":[\"Dit bericht verwijderen? Dit kan niet ongedaan worden gemaakt.\"],\"7nGhhM\":[\"Waar denk je aan?\"],\"7sEpu1\":[\"Leden — \",[\"0\"]],\"7sNhEz\":[\"Gebruikersnaam\"],\"8H0Q+x\":[\"Meer informatie over profielen →\"],\"8Phu0A\":[\"Weergeven wanneer gebruikers hun nickname wijzigen\"],\"8XTG9e\":[\"Oper-wachtwoord invoeren\"],\"8XsV2J\":[\"Opnieuw verzenden\"],\"8ZsakT\":[\"Wachtwoord\"],\"8kR84m\":[\"Je staat op het punt een externe link te openen:\"],\"8lCgih\":[\"Regel verwijderen\"],\"8o3dPc\":[\"Bestanden hier neerzetten om te uploaden\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"deed mee\"],\"other\":[\"deed \",[\"joinCount\"],\" keer mee\"]}]],\"9BMLnJ\":[\"Opnieuw verbinden met server\"],\"9OEgyT\":[\"Reactie toevoegen\"],\"9PQ8m2\":[\"G-Line (globale ban)\"],\"9Qs99X\":[\"E-mail:\"],\"9QupBP\":[\"Patroon verwijderen\"],\"9bG48P\":[\"Bezig met verzenden\"],\"9f5f0u\":[\"Vragen over privacy? Neem contact met ons op:\"],\"9unqs3\":[\"Afwezig:\"],\"9v3hwv\":[\"Geen servers gevonden.\"],\"9zb2WA\":[\"Verbinding maken\"],\"A1taO8\":[\"Zoeken\"],\"A2adVi\":[\"Typemeldingen verzenden\"],\"A9Rhec\":[\"Kanaalnaam\"],\"AWOSPo\":[\"Inzoomen\"],\"AXSpEQ\":[\"Oper bij verbinding\"],\"AeXO77\":[\"Account\"],\"AhNP40\":[\"Spoelen\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Nickname gewijzigd\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Antwoord annuleren\"],\"ApSx0O\":[[\"0\"],\" berichten gevonden die overeenkomen met \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Geen resultaten gevonden\"],\"AyNqAB\":[\"Alle servergebeurtenissen in de chat weergeven\"],\"B/QqGw\":[\"Niet achter het toetsenbord\"],\"B8AaMI\":[\"Dit veld is vereist\"],\"BA2c49\":[\"Server ondersteunt geen geavanceerde LIST-filtering\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" en \",[\"3\"],\" anderen typen...\"],\"BGul2A\":[\"Je hebt niet-opgeslagen wijzigingen. Weet je zeker dat je wilt sluiten zonder op te slaan?\"],\"BIf9fi\":[\"Je statusbericht\"],\"BPm98R\":[\"Er is geen server geselecteerd. Kies eerst een server uit de zijbalk; uitnodigingslinks worden per server beheerd.\"],\"BZz3md\":[\"Je persoonlijke website\"],\"Bgm/H7\":[\"Meerdere tekstregels invoeren toestaan\"],\"BiQIl1\":[\"Dit privéberichtgesprek vastmaken\"],\"BlNZZ2\":[\"Klik om naar bericht te springen\"],\"Bowq3c\":[\"Alleen operators kunnen het kanaalonderwerp wijzigen\"],\"Btozzp\":[\"Deze afbeelding is verlopen\"],\"Bycfjm\":[\"Totaal: \",[\"0\"]],\"C6IBQc\":[\"Kopieer volledige JSON\"],\"C9L9wL\":[\"Gegevensverzameling\"],\"CDq4wC\":[\"Gebruiker modereren\"],\"CHVRxG\":[\"Bericht aan @\",[\"0\"],\" (Shift+Enter voor nieuwe regel)\"],\"CN9zdR\":[\"Oper-naam en wachtwoord zijn vereist\"],\"CW3sYa\":[\"Reactie toevoegen \",[\"emoji\"]],\"CaAkqd\":[\"Afmeldingen weergeven\"],\"CbvaYj\":[\"Bannen via nickname\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Selecteer een kanaal\"],\"CsekCi\":[\"Normaal\"],\"D+NlUC\":[\"Systeem\"],\"D28t6+\":[\"is toegetreden en vertrokken\"],\"DB8zMK\":[\"Toepassen\"],\"DBcWHr\":[\"Aangepast meldingsgeluidsbestand\"],\"DTy9Xw\":[\"Mediavoorbeeldweergaven\"],\"Dj4pSr\":[\"Kies een veilig wachtwoord\"],\"Du+zn+\":[\"Zoeken...\"],\"Du2T2f\":[\"Instelling niet gevonden\"],\"DwsSVQ\":[\"Filters toepassen en vernieuwen\"],\"E3W/zd\":[\"Standaard nickname\"],\"E6nRW7\":[\"URL kopiëren\"],\"E703RG\":[\"Modi:\"],\"EAeu1Z\":[\"Uitnodiging verzenden\"],\"EFKJQT\":[\"Instelling\"],\"EGPQBv\":[\"Aangepaste floodregels (+f)\"],\"ELik0r\":[\"Volledig privacybeleid bekijken\"],\"EPbeC2\":[\"Kanaalonderwerp bekijken of bewerken\"],\"EQCDNT\":[\"Voer oper-gebruikersnaam in...\"],\"EUvulZ\":[\"1 bericht gevonden dat overeenkomt met \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Volgende afbeelding\"],\"EdQY6l\":[\"Geen\"],\"EnqLYU\":[\"Servers zoeken...\"],\"F0OKMc\":[\"Server bewerken\"],\"F6Int2\":[\"Markeringen inschakelen\"],\"FDoLyE\":[\"Max. gebruikers\"],\"FUU/hZ\":[\"Bepaalt hoeveel externe media in de chat worden geladen.\"],\"Fdp03t\":[\"aan\"],\"FfPWR0\":[\"Venster\"],\"FjkaiT\":[\"Uitzoomen\"],\"FlqOE9\":[\"Wat dit betekent:\"],\"FolHNl\":[\"Je account en authenticatie beheren\"],\"Fp2Dif\":[\"De server verlaten\"],\"G5KmCc\":[\"GZ-Line (globale Z-Line)\"],\"GDs0lz\":[\"<0>Risico:0> Gevoelige informatie (berichten, privégesprekken, authenticatiegegevens) kan worden blootgesteld aan netwerkbeheerders of aanvallers tussen IRC-servers.\"],\"GR+2I3\":[\"Uitnodigingsmasker toevoegen (bijv. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Losgekoppelde serverberichten sluiten\"],\"GdhD7H\":[\"Klik nogmaals om te bevestigen\"],\"GlHnXw\":[\"Nickname wijziging mislukt: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Voorbeeld:\"],\"GtmO8/\":[\"van\"],\"GtuHUQ\":[\"Dit kanaal op de server hernoemen. Alle gebruikers zien de nieuwe naam.\"],\"GuGfFX\":[\"Zoeken aan/uit\"],\"GxkJXS\":[\"Uploaden...\"],\"GzbwnK\":[\"Het kanaal betreden\"],\"GzsUDB\":[\"Uitgebreid profiel\"],\"H/PnT8\":[\"Emoji invoegen\"],\"H6Izzl\":[\"Je voorkeurkleurcode\"],\"H9jIv+\":[\"Aanmeldingen/vertrekken weergeven\"],\"HAKBY9\":[\"Bestanden uploaden\"],\"HdE1If\":[\"Kanaal\"],\"Hk4AW9\":[\"Je voorkeurweergavenaam\"],\"HmHDk7\":[\"Lid selecteren\"],\"HrQzPU\":[\"Kanalen op \",[\"networkName\"]],\"I2tXQ5\":[\"Bericht aan @\",[\"0\"],\" (Enter voor nieuwe regel, Shift+Enter om te verzenden)\"],\"I6bw/h\":[\"Gebruiker bannen\"],\"I92Z+b\":[\"Meldingen inschakelen\"],\"I9D72S\":[\"Weet je zeker dat je dit bericht wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.\"],\"IA+1wo\":[\"Weergeven wanneer gebruikers uit kanalen worden verwijderd\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Wijzigingen opslaan\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" en \",[\"2\"],\" zijn aan het typen...\"],\"IgrLD/\":[\"Pauzeren\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Beantwoorden\"],\"IoHMnl\":[\"Maximale waarde is \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Verbinding maken...\"],\"J5T9NW\":[\"Gebruikersinformatie\"],\"J8Y5+z\":[\"Oeps! Netwerksplitsing! ⚠️\"],\"JBHkBA\":[\"Het kanaal verlaten\"],\"JCwL0Q\":[\"Reden invoeren (optioneel)\"],\"JFciKP\":[\"Aan/uit\"],\"JXGkhG\":[\"Kanaalnaam wijzigen (alleen operators)\"],\"JcD7qf\":[\"Meer acties\"],\"JdkA+c\":[\"Geheim (+s)\"],\"Jmu12l\":[\"Serverkanalen\"],\"JvQ++s\":[\"Markdown inschakelen\"],\"K2jwh/\":[\"Geen WHOIS-gegevens beschikbaar\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Bericht verwijderen\"],\"KKBlUU\":[\"Insluiten\"],\"KM0pLb\":[\"Welkom in het kanaal!\"],\"KR6W2h\":[\"Gebruiker niet meer negeren\"],\"KV+Bi1\":[\"Alleen op uitnodiging (+i)\"],\"KdCtwE\":[\"Hoeveel seconden floodactiviteit bewaken voordat tellers worden gereset\"],\"Kkezga\":[\"Serverwachtwoord\"],\"KsiQ/8\":[\"Gebruikers moeten worden uitgenodigd om het kanaal te betreden\"],\"L+gB/D\":[\"Kanaalinformatie\"],\"LC1a7n\":[\"De IRC-server heeft gemeld dat de server-naar-serververbindingen een laag beveiligingsniveau hebben. Dit betekent dat wanneer je berichten worden doorgegeven tussen IRC-servers in het netwerk, ze mogelijk niet correct worden versleuteld of dat de SSL/TLS-certificaten niet correct worden gevalideerd.\"],\"LNfLR5\":[\"Kicks weergeven\"],\"LQb0W/\":[\"Alle gebeurtenissen weergeven\"],\"LU7/yA\":[\"Alternatieve naam voor weergave in de interface. Mag spaties, emoji en speciale tekens bevatten. De echte kanaalnaam (\",[\"channelName\"],\") wordt nog steeds gebruikt voor IRC-opdrachten.\"],\"LUb9O7\":[\"Een geldige serverpoort is vereist\"],\"LV4fT6\":[\"Beschrijving (optioneel, bijv. \\\"Bètatesters K3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Privacybeleid\"],\"LcuSDR\":[\"Je profielgegevens en metadata beheren\"],\"LqLS9B\":[\"Nicknamewijzigingen weergeven\"],\"LsDQt2\":[\"Kanaalinstellingen\"],\"LtI9AS\":[\"Eigenaar\"],\"LuNhhL\":[\"reageerde op dit bericht\"],\"M/AZNG\":[\"URL naar je avatarafbeelding\"],\"M/WIer\":[\"Bericht verzenden\"],\"M8er/5\":[\"Naam:\"],\"MHk+7g\":[\"Vorige afbeelding\"],\"MRorGe\":[\"Gebruiker een PM sturen\"],\"MVbSGP\":[\"Tijdvenster (seconden)\"],\"MkpcsT\":[\"Je berichten en instellingen worden lokaal op je apparaat opgeslagen\"],\"N/hDSy\":[\"Markeren als bot — gewoonlijk 'aan' of leeg\"],\"N7TQbE\":[\"Gebruiker uitnodigen voor \",[\"channelName\"]],\"NCca/o\":[\"Voer standaard bijnaam in...\"],\"Nqs6B9\":[\"Toont alle externe media. Elke URL kan een verzoek naar een onbekende server veroorzaken.\"],\"Nt+9O7\":[\"WebSocket gebruiken in plaats van raw TCP\"],\"NxIHzc\":[\"Gebruiker verbreken\"],\"O+v/cL\":[\"Alle kanalen op de server bekijken\"],\"ODwSCk\":[\"Een GIF verzenden\"],\"OGQ5kK\":[\"Meldingsgeluiden en markeringen instellen\"],\"OIPt1Z\":[\"Zijbalk met ledenlijst weergeven of verbergen\"],\"OKSNq/\":[\"Zeer streng\"],\"ONWvwQ\":[\"Uploaden\"],\"OVKoQO\":[\"Je accountwachtwoord voor authenticatie\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - IRC naar de toekomst brengen\"],\"OhCpra\":[\"Een onderwerp instellen…\"],\"OkltoQ\":[[\"username\"],\" bannen via nickname (voorkomt dat ze opnieuw deelnemen met dezelfde nick)\"],\"P+t/Te\":[\"Geen aanvullende gegevens\"],\"P42Wcc\":[\"Veilig\"],\"PD38l0\":[\"Kanaalavatar voorbeeldweergave\"],\"PD9mEt\":[\"Typ een bericht...\"],\"PPqfdA\":[\"Kanaelconfiguratie-instellingen openen\"],\"PSCjfZ\":[\"Het onderwerp dat voor dit kanaal wordt weergegeven. Alle gebruikers kunnen het onderwerp zien.\"],\"PZCecv\":[\"PDF-voorbeeld\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 keer\"],\"other\":[[\"c\"],\" keer\"]}]],\"PguS2C\":[\"Uitzonderingsmasker toevoegen (bijv. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[[\"displayedChannelsCount\"],\" van \",[\"0\"],\" kanalen weergegeven\"],\"PqhVlJ\":[\"Gebruiker bannen (via hostmasker)\"],\"Q+chwU\":[\"Gebruikersnaam:\"],\"Q2QY4/\":[\"Deze uitnodiging verwijderen\"],\"Q6hhn8\":[\"Voorkeuren\"],\"QF4a34\":[\"Voer een gebruikersnaam in\"],\"QGqSZ2\":[\"Kleur en opmaak\"],\"QJQd1J\":[\"Profiel bewerken\"],\"QSzGDE\":[\"Inactief\"],\"QUlny5\":[\"Welkom bij \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Meer lezen\"],\"QuSkCF\":[\"Kanalen filteren...\"],\"QwUrDZ\":[\"heeft het onderwerp gewijzigd naar: \",[\"topic\"]],\"R0UH07\":[\"Afbeelding \",[\"0\"],\" van \",[\"1\"]],\"R7SsBE\":[\"Dempen\"],\"R8rf1X\":[\"Klik om onderwerp in te stellen\"],\"RArB3D\":[\"werd gekickt uit \",[\"channelName\"],\" door \",[\"username\"]],\"RI3cWd\":[\"Ontdek de wereld van IRC met ObsidianIRC\"],\"RIfHS5\":[\"Een nieuwe uitnodigingslink aanmaken\"],\"RMMaN5\":[\"Gemodereerd (+m)\"],\"RWw9Lg\":[\"Venster sluiten\"],\"RZ2BuZ\":[\"Accountregistratie voor \",[\"account\"],\" vereist verificatie: \",[\"message\"]],\"RySp6q\":[\"Reacties verbergen\"],\"SPKQTd\":[\"Nickname is vereist\"],\"SPVjfj\":[\"Standaard 'geen reden' als leeggelaten\"],\"SQKPvQ\":[\"Gebruiker uitnodigen\"],\"SkZcl+\":[\"Kies een vooraf ingesteld floodbeveililingsprofiel. Deze profielen bieden evenwichtige beveiligingsinstellingen voor verschillende toepassingen.\"],\"Slr+3C\":[\"Min. gebruikers\"],\"Spnlre\":[\"Je hebt \",[\"target\"],\" uitgenodigd om deel te nemen aan \",[\"channel\"]],\"T/ckN5\":[\"Openen in viewer\"],\"T91vKp\":[\"Afspelen\"],\"TV2Wdu\":[\"Lees hoe we met je gegevens omgaan en je privacy beschermen.\"],\"TgFpwD\":[\"Toepassen...\"],\"TkzSFB\":[\"Geen wijzigingen\"],\"TtserG\":[\"Echte naam invoeren\"],\"Ttz9J1\":[\"Voer wachtwoord in...\"],\"Tz0i8g\":[\"Instellingen\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reageren\"],\"UE4KO5\":[\"*kanaal*\"],\"UETAwW\":[\"Je hebt nog geen uitnodigingslinks aangemaakt. Gebruik het formulier hierboven om je eerste aan te maken.\"],\"UGT5vp\":[\"Instellingen opslaan\"],\"UV5hLB\":[\"Geen bannen gevonden\"],\"Uaj3Nd\":[\"Statusberichten\"],\"Ue3uny\":[\"Standaard (geen profiel)\"],\"UkARhe\":[\"Normaal — Standaardbeveiliging\"],\"Umn7Cj\":[\"Nog geen reacties. Wees de eerste!\"],\"UtUIRh\":[[\"0\"],\" oudere berichten\"],\"UwzP+U\":[\"Beveiligde verbinding\"],\"V0/A4O\":[\"Kanaaleigenaar\"],\"V4qgxE\":[\"Aangemaakt voor (min geleden)\"],\"V8yTm6\":[\"Zoekopdracht wissen\"],\"VJMMyz\":[\"ObsidianIRC — IRC de toekomst in\"],\"VJScHU\":[\"Reden\"],\"VLsmVV\":[\"Meldingen dempen\"],\"VbyRUy\":[\"Reacties\"],\"Vmx0mQ\":[\"Ingesteld door:\"],\"VqnIZz\":[\"Ons privacybeleid en gegevenspraktijken bekijken\"],\"VrMygG\":[\"Minimale lengte is \",[\"0\"]],\"VrnTui\":[\"Je voornaamwoorden, weergegeven in je profiel\"],\"W8E3qn\":[\"Geverifieerd account\"],\"WAakm9\":[\"Kanaal verwijderen\"],\"WFxTHC\":[\"Banmasker toevoegen (bijv. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Serverhost is vereist\"],\"WRYdXW\":[\"Audiopositie\"],\"WUOH5B\":[\"Gebruiker negeren\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Toon 1 item meer\"],\"other\":[\"Toon \",[\"1\"],\" items meer\"]}]],\"WYxRzo\":[\"Beheer en maak je uitnodigingslinks aan\"],\"Wd38W1\":[\"Laat het kanaal leeg voor een algemene netwerkuitnodiging. De beschrijving is alleen voor je eigen administratie — alleen voor jou zichtbaar in deze lijst.\"],\"Weq9zb\":[\"Algemeen\"],\"Wfj7Sk\":[\"Meldingsgeluiden dempen of activeren\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Gebruikersprofiel\"],\"X6S3lt\":[\"Instellingen, kanalen, servers zoeken...\"],\"XEHan5\":[\"Toch doorgaan\"],\"XI1+wb\":[\"Ongeldig formaat\"],\"XIXeuC\":[\"Bericht aan @\",[\"0\"]],\"XMS+k4\":[\"Privébericht starten\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Privégesprek losmaken\"],\"Xm/s+u\":[\"Weergave\"],\"Xp2n93\":[\"Toont media van de vertrouwde bestandshost van je server. Er worden geen verzoeken gedaan aan externe diensten.\"],\"XvjC4F\":[\"Opslaan...\"],\"Y/qryO\":[\"Geen gebruikers gevonden die overeenkomen met je zoekopdracht\"],\"YAqRpI\":[\"Accountregistratie voor \",[\"account\"],\" geslaagd: \",[\"message\"]],\"YEfzvP\":[\"Beveiligd onderwerp (+t)\"],\"YQOn6a\":[\"Ledenlijst inklappen\"],\"YRCoE9\":[\"Kanaaloperator\"],\"YURQaF\":[\"Profiel bekijken\"],\"YdBSvr\":[\"Mediaweergave en externe inhoud beheren\"],\"Yj6U3V\":[\"Geen centrale server:\"],\"YjvpGx\":[\"Voornaamwoorden\"],\"YqH4l4\":[\"Geen sleutel\"],\"YyUPpV\":[\"Account:\"],\"ZJSWfw\":[\"Bericht dat wordt weergegeven wanneer je de verbinding met de server verbreekt\"],\"ZR1dJ4\":[\"Uitnodigingen\"],\"ZdWg0V\":[\"Openen in browser\"],\"ZhRBbl\":[\"Berichten zoeken…\"],\"Zmcu3y\":[\"Geavanceerde filters\"],\"a2/8e5\":[\"Onderwerp ingesteld na (min geleden)\"],\"aHKcKc\":[\"Vorige pagina\"],\"aJTbXX\":[\"Oper-wachtwoord\"],\"aQryQv\":[\"Patroon bestaat al\"],\"aW9pLN\":[\"Maximaal aantal toegestane gebruikers in het kanaal. Laat leeg voor geen limiet.\"],\"ah4fmZ\":[\"Toont ook voorbeeldweergaven van YouTube, Vimeo, SoundCloud en vergelijkbare bekende diensten.\"],\"aifXak\":[\"Geen media in dit kanaal\"],\"ap2zBz\":[\"Ontspannen\"],\"az8lvo\":[\"Uit\"],\"azXSNo\":[\"Ledenlijst uitvouwen\"],\"azdliB\":[\"Aanmelden bij een account\"],\"b26wlF\":[\"zij/haar\"],\"bD/+Ei\":[\"Streng\"],\"bQ6BJn\":[\"Stel gedetailleerde floodbeveiligingsregels in. Elke regel bepaalt welk type activiteit wordt bewaakt en welke actie wordt ondernomen als drempelwaarden worden overschreden.\"],\"beV7+y\":[\"De gebruiker ontvangt een uitnodiging om deel te nemen aan \",[\"channelName\"],\".\"],\"bk84cH\":[\"Afwezigheidsbericht\"],\"bkHdLj\":[\"IRC-server toevoegen\"],\"bmQLn5\":[\"Regel toevoegen\"],\"bwRvnp\":[\"Actie\"],\"c8+EVZ\":[\"Geverifieerd account\"],\"cGYUlD\":[\"Er worden geen mediavoorbeeldweergaven geladen.\"],\"cLF98o\":[\"Reacties tonen (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Geen gebruikers beschikbaar\"],\"cSgpoS\":[\"Privégesprek vastmaken\"],\"cde3ce\":[\"Bericht aan <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Kopieer opgemaakte uitvoer\"],\"cl/A5J\":[\"Welkom bij \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Verwijderen\"],\"coPLXT\":[\"We slaan je IRC-communicatie niet op onze servers op\"],\"crYH/6\":[\"SoundCloud-speler\"],\"d3sis4\":[\"Server toevoegen\"],\"d9aN5k\":[[\"username\"],\" uit het kanaal verwijderen\"],\"dEgA5A\":[\"Annuleren\"],\"dGi1We\":[\"Dit privéberichtgesprek losmaken\"],\"dJVuyC\":[\"heeft \",[\"channelName\"],\" verlaten (\",[\"reason\"],\")\"],\"dMtLDE\":[\"aan\"],\"dXqxlh\":[\"<0>⚠️ Beveiligingsrisico!0> Deze verbinding kan kwetsbaar zijn voor onderschepping of man-in-the-middle-aanvallen.\"],\"da9Q/R\":[\"Kanaalmodi gewijzigd\"],\"dhJN3N\":[\"Reacties tonen\"],\"dj2xTE\":[\"Melding sluiten\"],\"dpCzmC\":[\"Floodbeveiligingsinstellingen\"],\"e9dQpT\":[\"Wil je deze link in een nieuw tabblad openen?\"],\"ePK91l\":[\"Bewerken\"],\"eYBDuB\":[\"Upload een afbeelding of geef een URL op met optionele \",[\"size\"],\"-vervanging voor dynamische grootte\"],\"edBbee\":[[\"username\"],\" bannen via hostmasker (voorkomt dat ze opnieuw deelnemen via hetzelfde IP/host)\"],\"ekfzWq\":[\"Gebruikersinstellingen\"],\"elPDWs\":[\"Pas je IRC-clientervaring aan\"],\"eu2osY\":[\"<0>💡 Aanbeveling:0> Ga alleen verder als je deze server vertrouwt en de risico's begrijpt. Deel geen gevoelige informatie of wachtwoorden via deze verbinding.\"],\"euEhbr\":[\"Klik om deel te nemen aan \",[\"channel\"]],\"ez3vLd\":[\"Meerdere regels invoer inschakelen\"],\"f0J5Ki\":[\"Server-naar-servercommunicatie kan niet-versleutelde verbindingen gebruiken\"],\"f9BHJk\":[\"Gebruiker waarschuwen\"],\"fDOLLd\":[\"Geen kanalen gevonden.\"],\"ffzDkB\":[\"Anonieme analyses:\"],\"fq1GF9\":[\"Weergeven wanneer gebruikers de verbinding met de server verbreken\"],\"gEF57C\":[\"Deze server ondersteunt slechts één verbindingstype\"],\"gJuLUI\":[\"Negeerlijst\"],\"gNzMrk\":[\"Huidige avatar\"],\"gjPWyO\":[\"Voer bijnaam in...\"],\"gz6UQ3\":[\"Maximaliseren\"],\"h6razj\":[\"Kanaalnaammasker uitsluiten\"],\"hG6jnw\":[\"Geen onderwerp ingesteld\"],\"hG89Ed\":[\"Afbeelding\"],\"hYgDIe\":[\"Aanmaken\"],\"hZ6znB\":[\"Poort\"],\"ha+Bz5\":[\"bijv. 100:1440\"],\"he3ygx\":[\"Kopiëren\"],\"hehnjM\":[\"Aantal\"],\"hzdLuQ\":[\"Alleen gebruikers met voice of hoger kunnen spreken\"],\"i0qMbr\":[\"Start\"],\"iDNBZe\":[\"Meldingen\"],\"iH8pgl\":[\"Terug\"],\"iL9SZg\":[\"Gebruiker bannen (via nickname)\"],\"iNt+3c\":[\"Terug naar afbeelding\"],\"iQvi+a\":[\"Niet meer waarschuwen over lage verbindingsbeveiliging voor deze server\"],\"iSLIjg\":[\"Verbinden\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Serverhost\"],\"idD8Ev\":[\"Opgeslagen\"],\"iivqkW\":[\"Aangemeld op\"],\"ij+Elv\":[\"Afbeeldingsvoorbeeldweergave\"],\"ilIWp7\":[\"Meldingen aan/uit\"],\"iuaqvB\":[\"Gebruik * voor jokertekens. Voorbeelden: slechtegebruiker!*@*, *!*@spammer.com, trol*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Bannen via hostmasker\"],\"jA4uoI\":[\"Onderwerp:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Reden (optioneel)\"],\"jUV7CU\":[\"Avatar uploaden\"],\"jW5Uwh\":[\"Bepaal hoeveel externe media worden geladen. Uit / Veilig / Vertrouwde bronnen / Alle inhoud.\"],\"jXzms5\":[\"Bijlageopties\"],\"jZlrte\":[\"Kleur\"],\"jfC/xh\":[\"Contact\"],\"jywMpv\":[\"#nieuwe-kanaalnaam\"],\"k112DD\":[\"Oudere berichten laden\"],\"k3ID0F\":[\"Leden filteren…\"],\"k65gsE\":[\"Diepgaande analyse\"],\"k7Zgob\":[\"Verbinding annuleren\"],\"kAVx5h\":[\"Geen uitnodigingen gevonden\"],\"kCLEPU\":[\"Verbonden met\"],\"kF5LKb\":[\"Genegeerde patronen:\"],\"kGeOx/\":[\"Deelnemen aan \",[\"0\"]],\"kITKr8\":[\"Kanaalmodi laden...\"],\"kPpPsw\":[\"Je bent een IRC Operator\"],\"kWJmRL\":[\"Jij\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Kopieer JSON\"],\"krViRy\":[\"Klik om te kopiëren als JSON\"],\"ks71ra\":[\"Uitzonderingen\"],\"kw4lRv\":[\"Kanaal half-operator\"],\"kxgIRq\":[\"Selecteer of voeg een kanaal toe om te beginnen.\"],\"ky6dWe\":[\"Avatarvoorbeeldweergave\"],\"l+GxCv\":[\"Kanalen laden...\"],\"l+IUVW\":[\"Accountverificatie voor \",[\"account\"],\" geslaagd: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"opnieuw verbonden\"],\"other\":[[\"reconnectCount\"],\" keer opnieuw verbonden\"]}]],\"l1l8sj\":[[\"0\"],\"d geleden\"],\"l5NhnV\":[\"#kanaal (optioneel)\"],\"l5jmzx\":[[\"0\"],\" en \",[\"1\"],\" zijn aan het typen...\"],\"lCF0wC\":[\"Vernieuwen\"],\"lHy8N5\":[\"Meer kanalen laden...\"],\"lasgrr\":[\"gebruikt\"],\"lbpf14\":[\"Deelnemen aan \",[\"value\"]],\"lfFsZ4\":[\"Kanalen\"],\"lkNdiH\":[\"Accountnaam\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Afbeelding uploaden\"],\"loQxaJ\":[\"Ik ben terug\"],\"lvfaxv\":[\"START\"],\"m16xKo\":[\"Toevoegen\"],\"m8flAk\":[\"Voorbeeld (nog niet geüpload)\"],\"mEPxTp\":[\"<0>⚠️ Wees voorzichtig!0> Open alleen links van vertrouwde bronnen. Kwaadaardige links kunnen je beveiliging of privacy in gevaar brengen.\"],\"mHGdhG\":[\"Serverinformatie\"],\"mHS8lb\":[\"Bericht in #\",[\"0\"]],\"mMYBD9\":[\"Breed — Ruimere beveiligingsscope\"],\"mTGsPd\":[\"Kanaalonderwerp\"],\"mU8j6O\":[\"Geen externe berichten (+n)\"],\"mZp8FL\":[\"Automatisch terugvallen op één regel\"],\"mdQu8G\":[\"JouwNickname\"],\"miSSBQ\":[\"Reacties (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Gebruiker is geverifieerd\"],\"mwtcGl\":[\"Reacties sluiten\"],\"mzI/c+\":[\"Downloaden\"],\"n3fGRk\":[\"ingesteld door \",[\"0\"]],\"nE9jsU\":[\"Ontspannen — Minder agressieve beveiliging\"],\"nNflMD\":[\"Kanaal verlaten\"],\"nPXkBi\":[\"WHOIS-gegevens laden...\"],\"nQnxxF\":[\"Bericht in #\",[\"0\"],\" (Shift+Enter voor nieuwe regel)\"],\"nWMRxa\":[\"Losmaken\"],\"nkC032\":[\"Geen floodprofiel\"],\"o69z4d\":[\"Een waarschuwingsbericht sturen naar \",[\"username\"]],\"o9ylQi\":[\"Zoek naar GIF's om te beginnen\"],\"oFGkER\":[\"Serverberichten\"],\"oOi11l\":[\"Naar beneden scrollen\"],\"oPYIL5\":[\"netwerk\"],\"oQEzQR\":[\"Nieuw DM\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Man-in-the-middle-aanvallen op serververbindingen zijn mogelijk\"],\"oeqmmJ\":[\"Vertrouwde bronnen\"],\"optX0N\":[[\"0\"],\"u geleden\"],\"ovBPCi\":[\"Standaard\"],\"p0Z69r\":[\"Patroon kan niet leeg zijn\"],\"p1KgtK\":[\"Audio laden mislukt\"],\"p59pEv\":[\"Extra details\"],\"p7sRI6\":[\"Laat anderen weten wanneer je typt\"],\"pBm1od\":[\"Geheim kanaal\"],\"pNmiXx\":[\"Je standaard nickname voor alle servers\"],\"pUUo9G\":[\"Hostnaam:\"],\"pVGPmz\":[\"Accountwachtwoord\"],\"peNE68\":[\"Permanent\"],\"plhHQt\":[\"Geen gegevens\"],\"pm6+q5\":[\"Beveiligingswaarschuwing\"],\"pn5qSs\":[\"Aanvullende informatie\"],\"q0cR4S\":[\"is nu bekend als **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanaal verschijnt niet in LIST- of NAMES-opdrachten\"],\"qLpTm/\":[\"Reactie \",[\"emoji\"],\" verwijderen\"],\"qVkGWK\":[\"Vastmaken\"],\"qY8wNa\":[\"Startpagina\"],\"qb0xJ7\":[\"Gebruik jokertekens: * komt overeen met een reeks, ? met één teken. Voorbeelden: nick!*@*, *!*@host.com, *!*gebruiker@*\"],\"qhzpRq\":[\"Kanaalsleutel (+k)\"],\"qtoOYG\":[\"Geen limiet\"],\"r1W2AS\":[\"Afbeelding van bestandshost\"],\"rIPR2O\":[\"Onderwerp ingesteld voor (min geleden)\"],\"rMMSYo\":[\"Maximale lengte is \",[\"0\"]],\"rWtzQe\":[\"Het netwerk splitste en herverbond. ✅\"],\"rYG2u6\":[\"Even wachten...\"],\"rdUucN\":[\"Voorbeeld\"],\"rjGI/Q\":[\"Privacy\"],\"rk8iDX\":[\"GIF's laden...\"],\"rn6SBY\":[\"Dempen opheffen\"],\"s/UKqq\":[\"Uit het kanaal verwijderd\"],\"s8cATI\":[\"heeft \",[\"channelName\"],\" betreden\"],\"sCO9ue\":[\"De verbinding met <0>\",[\"serverName\"],\"0> heeft de volgende beveiligingsproblemen:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"is nu bekend als **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" heeft je uitgenodigd om deel te nemen aan \",[\"channel\"]],\"sby+1/\":[\"Klik om te kopiëren\"],\"sfN25C\":[\"Je echte of volledige naam\"],\"sliuzR\":[\"Link openen\"],\"sqrO9R\":[\"Aangepaste vermeldingen\"],\"sr6RdJ\":[\"Meerdere regels met Shift+Enter\"],\"swrCpB\":[\"Het kanaal is hernoemd van \",[\"oldName\"],\" naar \",[\"newName\"],\" door \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Geavanceerd\"],\"t/YqKh\":[\"Verwijderen\"],\"t47eHD\":[\"Je unieke identificatie op deze server\"],\"tAkAh0\":[\"URL met optionele \",[\"size\"],\"-vervanging voor dynamische grootte. Voorbeeld: https://voorbeeld.com/avatar/\",[\"size\"],\"/kanaal.jpg\"],\"tXLJS3\":[\"Zijbalk met kanaallijst weergeven of verbergen\"],\"tfDRzk\":[\"Opslaan\"],\"tiBsJk\":[\"heeft \",[\"channelName\"],\" verlaten\"],\"tt4/UD\":[\"verliet de server (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Nickname {nick} is al in gebruik, probeer opnieuw met {newNick}\"],\"u0a8B4\":[\"Authenticeren als IRC Operator voor beheerderstoegang\"],\"u0rWFU\":[\"Aangemaakt na (min geleden)\"],\"u72w3t\":[\"Gebruikers en patronen om te negeren\"],\"u7jc2L\":[\"verliet de server\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Opslaan mislukt: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC-servers:\"],\"ukyW4o\":[\"Jouw uitnodigingslinks\"],\"usSSr/\":[\"Zoomniveau\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Gebruik Shift+Enter voor nieuwe regels (Enter verstuurt)\"],\"vERlcd\":[\"Profiel\"],\"vK0RL8\":[\"Geen onderwerp\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Taal\"],\"vaHYxN\":[\"Echte naam\"],\"vhjbKr\":[\"Afwezig\"],\"w4NYox\":[[\"title\"],\" client\"],\"w8xQRx\":[\"Ongeldige waarde\"],\"wFjjxZ\":[\"werd gekickt uit \",[\"channelName\"],\" door \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Geen uitzonderingen op bannen gevonden\"],\"wPrGnM\":[\"Kanaalbeheerder\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Weergeven wanneer gebruikers kanalen betreden of verlaten\"],\"whqZ9r\":[\"Extra woorden of zinnen om te markeren\"],\"wm7RV4\":[\"Meldingsgeluid\"],\"wz/Yoq\":[\"Je berichten kunnen worden onderschept wanneer ze tussen servers worden doorgegeven\"],\"x3+y8b\":[\"Zoveel mensen hebben zich via deze link geregistreerd\"],\"xCJdfg\":[\"Wissen\"],\"xOTzt5\":[\"zojuist\"],\"xUHRTR\":[\"Automatisch als operator authenticeren bij verbinden\"],\"xWHwwQ\":[\"Bannen\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"Deze server ondersteunt geen uitnodigingslinks (de<0>obby.world/invitation0>-capability wordt niet aangekondigd). Je kunt nog gewoon chatten; dit paneel is voor netwerken die op obbyircd draaien.\"],\"xceQrO\":[\"Alleen beveiligde WebSockets worden ondersteund\"],\"xdtXa+\":[\"kanaalnaam\"],\"xfXC7q\":[\"Tekstkanalen\"],\"xlCYOE\":[\"Meer berichten ophalen...\"],\"xlhswE\":[\"Minimale waarde is \",[\"0\"]],\"xq97Ci\":[\"Voeg een woord of zin toe...\"],\"xuRqRq\":[\"Clientlimiet (+l)\"],\"xwF+7J\":[[\"0\"],\" typt...\"],\"y1eoq1\":[\"Link kopiëren\"],\"yNeucF\":[\"Deze server ondersteunt geen uitgebreide profielmetadata (IRCv3 METADATA-extensie). Extra velden zoals avatar, weergavenaam en status zijn niet beschikbaar.\"],\"yPlrca\":[\"Kanaalavatar\"],\"yQE2r9\":[\"Laden\"],\"ySU+JY\":[\"jouw@email.com\"],\"yTX1Rt\":[\"Oper-gebruikersnaam\"],\"yYOzWD\":[\"logboeken\"],\"yfx9Re\":[\"IRC operator-wachtwoord\"],\"ygCKqB\":[\"Stoppen\"],\"ymDxJx\":[\"IRC operator-gebruikersnaam\"],\"yrpRsQ\":[\"Sorteren op naam\"],\"yz7wBu\":[\"Sluiten\"],\"zJw+jA\":[\"stelt modus in: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"m geleden\"],\"zebeLu\":[\"Oper-gebruikersnaam invoeren\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Ongeldig patroonformaat. Gebruik het formaat nick!gebruiker@host (jokerteken * toegestaan)\"],\"+6NQQA\":[\"Algemeen ondersteuningskanaal\"],\"+6NyRG\":[\"Client\"],\"+K0AvT\":[\"Verbreken\"],\"+cyFdH\":[\"Standaardbericht wanneer je jezelf als afwezig markeert\"],\"+mVPqU\":[\"Markdown-opmaak in berichten weergeven\"],\"+vqCJH\":[\"Je accountgebruikersnaam voor authenticatie\"],\"+yPBXI\":[\"Bestand kiezen\"],\"+zy2Nq\":[\"Type\"],\"/09cao\":[\"Lage verbindingsbeveiliging (niveau \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Gebruikers buiten het kanaal kunnen er geen berichten naar sturen\"],\"/4C8U0\":[\"Alles kopiëren\"],\"/6BzZF\":[\"Ledenlijst aan/uit\"],\"/AkXyp\":[\"Bevestigen?\"],\"/TNOPk\":[\"Gebruiker is afwezig\"],\"/XQgft\":[\"Ontdekken\"],\"/cF7Rs\":[\"Volume\"],\"/dqduX\":[\"Volgende pagina\"],\"/fc3q4\":[\"Alle inhoud\"],\"/kISDh\":[\"Meldingsgeluiden inschakelen\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Geluiden afspelen voor vermeldingen en berichten\"],\"0/0ZGA\":[\"Kanaalnaammasker\"],\"0D6j7U\":[\"Meer informatie over aangepaste regels →\"],\"0XsHcR\":[\"Gebruiker verwijderen\"],\"0ZpE//\":[\"Sorteren op gebruikers\"],\"0bEPwz\":[\"Afwezig instellen\"],\"0dGkPt\":[\"Kanaallijst uitvouwen\"],\"0gS7M5\":[\"Weergavenaam\"],\"0kS+M8\":[\"VoorbeeldNET\"],\"0rgoY7\":[\"Alleen verbinden met servers die jij kiest\"],\"0wdd7X\":[\"Deelnemen\"],\"0wkVYx\":[\"Privéberichten\"],\"111uHX\":[\"Linkvoorbeeldweergave\"],\"196EG4\":[\"Privégesprek verwijderen\"],\"1DSr1i\":[\"Registreren voor een account\"],\"1O/24y\":[\"Kanaallijst aan/uit\"],\"1TNIig\":[\"Openen\"],\"1VPJJ2\":[\"Waarschuwing externe link\"],\"1ZC/dv\":[\"Geen ongelezen vermeldingen of berichten\"],\"1pO1zi\":[\"Servernaam is vereist\"],\"1uwfzQ\":[\"Kanaalonderwerp bekijken\"],\"268g7c\":[\"Weergavenaam invoeren\"],\"2CEOW6\":[\"Netwerk gebonden via soju-bouncer\"],\"2F9+AZ\":[\"Nog geen ruw IRC-verkeer vastgelegd. Probeer verbinding te maken of een bericht te sturen.\"],\"2FOFq1\":[\"Serveroperators op het netwerk kunnen mogelijk je berichten lezen\"],\"2FYpfJ\":[\"Meer\"],\"2HF1Y2\":[[\"inviter\"],\" heeft \",[\"target\"],\" uitgenodigd om deel te nemen aan \",[\"channel\"]],\"2I70QL\":[\"Gebruikersprofielinformatie bekijken\"],\"2QYdmE\":[\"Gebruikers:\"],\"2QpEjG\":[\"heeft verlaten\"],\"2YE223\":[\"Bericht in #\",[\"0\"],\" (Enter voor nieuwe regel, Shift+Enter om te verzenden)\"],\"2bimFY\":[\"Serverwachtwoord gebruiken\"],\"2iTmdZ\":[\"Lokale opslag:\"],\"2odkwe\":[\"Streng — Agressievere beveiliging\"],\"2uDhbA\":[\"Gebruikersnaam invoeren om uit te nodigen\"],\"2ygf/L\":[\"← Terug\"],\"2zEgxj\":[\"GIF's zoeken...\"],\"3RdPhl\":[\"Kanaal hernoemen\"],\"3THokf\":[\"Gebruiker met voice\"],\"3TSz9S\":[\"Minimaliseren\"],\"3jBDvM\":[\"Weergavenaam van kanaal\"],\"3ryuFU\":[\"Optionele crashrapporten om de app te verbeteren\"],\"3uBF/8\":[\"Viewer sluiten\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Voer accountnaam in...\"],\"4/Rr0R\":[\"Een gebruiker uitnodigen voor het huidige kanaal\"],\"4EZrJN\":[\"Regels\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Floodprofiel (+F)\"],\"4RZQRK\":[\"Wat ben je aan het doen?\"],\"4hfTrB\":[\"Nickname\"],\"4n99LO\":[\"Al in \",[\"0\"]],\"4t6vMV\":[\"Automatisch overschakelen naar één regel voor korte berichten\"],\"4vsHmf\":[\"Tijd (min)\"],\"4x/Axu\":[\"Je bouncer heeft nog geen netwerken. Voeg er een toe om te beginnen.\"],\"5+INAX\":[\"Berichten markeren die jou vermelden\"],\"5R5Pv/\":[\"Oper-naam\"],\"678PKt\":[\"Netwerknaam\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Wachtwoord vereist om het kanaal te betreden. Laat leeg om de sleutel te verwijderen.\"],\"6HhMs3\":[\"Afsluitbericht\"],\"6V3Ea3\":[\"Gekopieerd\"],\"6lGV3K\":[\"Minder weergeven\"],\"6yFOEi\":[\"Voer oper-wachtwoord in...\"],\"7+IHTZ\":[\"Geen bestand gekozen\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!gebruiker@host (bijv. spam*!*@*, *!*@slechtehost.com)\"],\"7QkKyN\":[\"Privébericht sturen\"],\"7U1W7c\":[\"Zeer ontspannen\"],\"7Y1YQj\":[\"Echte naam:\"],\"7YHArF\":[\"— openen in viewer\"],\"7fjnVl\":[\"Gebruikers zoeken...\"],\"7jL88x\":[\"Dit bericht verwijderen? Dit kan niet ongedaan worden gemaakt.\"],\"7nGhhM\":[\"Waar denk je aan?\"],\"7sEpu1\":[\"Leden — \",[\"0\"]],\"7sNhEz\":[\"Gebruikersnaam\"],\"8H0Q+x\":[\"Meer informatie over profielen →\"],\"8Phu0A\":[\"Weergeven wanneer gebruikers hun nickname wijzigen\"],\"8XTG9e\":[\"Oper-wachtwoord invoeren\"],\"8XsV2J\":[\"Opnieuw verzenden\"],\"8ZsakT\":[\"Wachtwoord\"],\"8kR84m\":[\"Je staat op het punt een externe link te openen:\"],\"8lCgih\":[\"Regel verwijderen\"],\"8o3dPc\":[\"Bestanden hier neerzetten om te uploaden\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"deed mee\"],\"other\":[\"deed \",[\"joinCount\"],\" keer mee\"]}]],\"9BMLnJ\":[\"Opnieuw verbinden met server\"],\"9OEgyT\":[\"Reactie toevoegen\"],\"9PQ8m2\":[\"G-Line (globale ban)\"],\"9Qs99X\":[\"E-mail:\"],\"9QupBP\":[\"Patroon verwijderen\"],\"9W7tl5\":[\"(ongewijzigd)\"],\"9bG48P\":[\"Bezig met verzenden\"],\"9f5f0u\":[\"Vragen over privacy? Neem contact met ons op:\"],\"9iweoP\":[\"Netwerken op \",[\"0\"]],\"9unqs3\":[\"Afwezig:\"],\"9v3hwv\":[\"Geen servers gevonden.\"],\"9zb2WA\":[\"Verbinding maken\"],\"A1taO8\":[\"Zoeken\"],\"A2adVi\":[\"Typemeldingen verzenden\"],\"A9Rhec\":[\"Kanaalnaam\"],\"AWOSPo\":[\"Inzoomen\"],\"AXSpEQ\":[\"Oper bij verbinding\"],\"AdKRCX\":[\"Je bent verbonden met <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Account\"],\"AhNP40\":[\"Spoelen\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Nickname gewijzigd\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Antwoord annuleren\"],\"ApSx0O\":[[\"0\"],\" berichten gevonden die overeenkomen met \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Geen resultaten gevonden\"],\"AyNqAB\":[\"Alle servergebeurtenissen in de chat weergeven\"],\"B/QqGw\":[\"Niet achter het toetsenbord\"],\"B0sB2k\":[\"Platte tekst\"],\"B8AaMI\":[\"Dit veld is vereist\"],\"BA2c49\":[\"Server ondersteunt geen geavanceerde LIST-filtering\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" en \",[\"3\"],\" anderen typen...\"],\"BGul2A\":[\"Je hebt niet-opgeslagen wijzigingen. Weet je zeker dat je wilt sluiten zonder op te slaan?\"],\"BIf9fi\":[\"Je statusbericht\"],\"BOJWfb\":[\"Verbinding met soju-bouncer verbreken?\"],\"BPm98R\":[\"Er is geen server geselecteerd. Kies eerst een server uit de zijbalk; uitnodigingslinks worden per server beheerd.\"],\"BZz3md\":[\"Je persoonlijke website\"],\"Bgm/H7\":[\"Meerdere tekstregels invoeren toestaan\"],\"BiQIl1\":[\"Dit privéberichtgesprek vastmaken\"],\"BlNZZ2\":[\"Klik om naar bericht te springen\"],\"Bowq3c\":[\"Alleen operators kunnen het kanaalonderwerp wijzigen\"],\"Btozzp\":[\"Deze afbeelding is verlopen\"],\"Bycfjm\":[\"Totaal: \",[\"0\"]],\"C6IBQc\":[\"Kopieer volledige JSON\"],\"C9L9wL\":[\"Gegevensverzameling\"],\"CDq4wC\":[\"Gebruiker modereren\"],\"CHVRxG\":[\"Bericht aan @\",[\"0\"],\" (Shift+Enter voor nieuwe regel)\"],\"CN9zdR\":[\"Oper-naam en wachtwoord zijn vereist\"],\"CW3sYa\":[\"Reactie toevoegen \",[\"emoji\"]],\"CaAkqd\":[\"Afmeldingen weergeven\"],\"CbvaYj\":[\"Bannen via nickname\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Selecteer een kanaal\"],\"CsekCi\":[\"Normaal\"],\"D+NlUC\":[\"Systeem\"],\"D28t6+\":[\"is toegetreden en vertrokken\"],\"DB8zMK\":[\"Toepassen\"],\"DBcWHr\":[\"Aangepast meldingsgeluidsbestand\"],\"DTy9Xw\":[\"Mediavoorbeeldweergaven\"],\"Dj4pSr\":[\"Kies een veilig wachtwoord\"],\"Du+zn+\":[\"Zoeken...\"],\"Du2T2f\":[\"Instelling niet gevonden\"],\"DwsSVQ\":[\"Filters toepassen en vernieuwen\"],\"E3W/zd\":[\"Standaard nickname\"],\"E6nRW7\":[\"URL kopiëren\"],\"E703RG\":[\"Modi:\"],\"EAeu1Z\":[\"Uitnodiging verzenden\"],\"EFKJQT\":[\"Instelling\"],\"EGPQBv\":[\"Aangepaste floodregels (+f)\"],\"ELik0r\":[\"Volledig privacybeleid bekijken\"],\"EPbeC2\":[\"Kanaalonderwerp bekijken of bewerken\"],\"EQCDNT\":[\"Voer oper-gebruikersnaam in...\"],\"EUvulZ\":[\"1 bericht gevonden dat overeenkomt met \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Volgende afbeelding\"],\"EdQY6l\":[\"Geen\"],\"EnqLYU\":[\"Servers zoeken...\"],\"F0OKMc\":[\"Server bewerken\"],\"F6Int2\":[\"Markeringen inschakelen\"],\"FDoLyE\":[\"Max. gebruikers\"],\"FUU/hZ\":[\"Bepaalt hoeveel externe media in de chat worden geladen.\"],\"Fdp03t\":[\"aan\"],\"FfPWR0\":[\"Venster\"],\"FjkaiT\":[\"Uitzoomen\"],\"FlqOE9\":[\"Wat dit betekent:\"],\"FolHNl\":[\"Je account en authenticatie beheren\"],\"Fp2Dif\":[\"De server verlaten\"],\"G5KmCc\":[\"GZ-Line (globale Z-Line)\"],\"GDs0lz\":[\"<0>Risico:0> Gevoelige informatie (berichten, privégesprekken, authenticatiegegevens) kan worden blootgesteld aan netwerkbeheerders of aanvallers tussen IRC-servers.\"],\"GR+2I3\":[\"Uitnodigingsmasker toevoegen (bijv. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Losgekoppelde serverberichten sluiten\"],\"GdhD7H\":[\"Klik nogmaals om te bevestigen\"],\"GjRZex\":[\"Netwerkverbinding verbreken?\"],\"GlHnXw\":[\"Nickname wijziging mislukt: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Voorbeeld:\"],\"GtmO8/\":[\"van\"],\"GtuHUQ\":[\"Dit kanaal op de server hernoemen. Alle gebruikers zien de nieuwe naam.\"],\"GuGfFX\":[\"Zoeken aan/uit\"],\"GxkJXS\":[\"Uploaden...\"],\"GzbwnK\":[\"Het kanaal betreden\"],\"GzsUDB\":[\"Uitgebreid profiel\"],\"H/PnT8\":[\"Emoji invoegen\"],\"H6Izzl\":[\"Je voorkeurkleurcode\"],\"H9jIv+\":[\"Aanmeldingen/vertrekken weergeven\"],\"HAKBY9\":[\"Bestanden uploaden\"],\"HdE1If\":[\"Kanaal\"],\"Hk4AW9\":[\"Je voorkeurweergavenaam\"],\"HmHDk7\":[\"Lid selecteren\"],\"HrQzPU\":[\"Kanalen op \",[\"networkName\"]],\"I2tXQ5\":[\"Bericht aan @\",[\"0\"],\" (Enter voor nieuwe regel, Shift+Enter om te verzenden)\"],\"I6bw/h\":[\"Gebruiker bannen\"],\"I92Z+b\":[\"Meldingen inschakelen\"],\"I9D72S\":[\"Weet je zeker dat je dit bericht wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.\"],\"IA+1wo\":[\"Weergeven wanneer gebruikers uit kanalen worden verwijderd\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Wijzigingen opslaan\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" en \",[\"2\"],\" zijn aan het typen...\"],\"IgrLD/\":[\"Pauzeren\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Beantwoorden\"],\"IoHMnl\":[\"Maximale waarde is \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Verbinding maken...\"],\"J5T9NW\":[\"Gebruikersinformatie\"],\"J8Y5+z\":[\"Oeps! Netwerksplitsing! ⚠️\"],\"JBHkBA\":[\"Het kanaal verlaten\"],\"JCwL0Q\":[\"Reden invoeren (optioneel)\"],\"JFciKP\":[\"Aan/uit\"],\"JXGkhG\":[\"Kanaalnaam wijzigen (alleen operators)\"],\"JcD7qf\":[\"Meer acties\"],\"JdkA+c\":[\"Geheim (+s)\"],\"Jmu12l\":[\"Serverkanalen\"],\"JoQY+E\":[\"Hiermee wordt het netwerk uit je soju-bouncer verwijderd. Om het opnieuw te gebruiken, moet je het opnieuw toevoegen.\"],\"JvQ++s\":[\"Markdown inschakelen\"],\"K2jwh/\":[\"Geen WHOIS-gegevens beschikbaar\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Bericht verwijderen\"],\"KKBlUU\":[\"Insluiten\"],\"KM0pLb\":[\"Welkom in het kanaal!\"],\"KR6W2h\":[\"Gebruiker niet meer negeren\"],\"KV+Bi1\":[\"Alleen op uitnodiging (+i)\"],\"KdCtwE\":[\"Hoeveel seconden floodactiviteit bewaken voordat tellers worden gereset\"],\"Kkezga\":[\"Serverwachtwoord\"],\"KsiQ/8\":[\"Gebruikers moeten worden uitgenodigd om het kanaal te betreden\"],\"L+gB/D\":[\"Kanaalinformatie\"],\"LC1a7n\":[\"De IRC-server heeft gemeld dat de server-naar-serververbindingen een laag beveiligingsniveau hebben. Dit betekent dat wanneer je berichten worden doorgegeven tussen IRC-servers in het netwerk, ze mogelijk niet correct worden versleuteld of dat de SSL/TLS-certificaten niet correct worden gevalideerd.\"],\"LEwpeL\":[\"soju-bouncer (besturing)\"],\"LNfLR5\":[\"Kicks weergeven\"],\"LP+1Z7\":[\"Netwerk toevoegen\"],\"LQb0W/\":[\"Alle gebeurtenissen weergeven\"],\"LU7/yA\":[\"Alternatieve naam voor weergave in de interface. Mag spaties, emoji en speciale tekens bevatten. De echte kanaalnaam (\",[\"channelName\"],\") wordt nog steeds gebruikt voor IRC-opdrachten.\"],\"LUb9O7\":[\"Een geldige serverpoort is vereist\"],\"LV4fT6\":[\"Beschrijving (optioneel, bijv. \\\"Bètatesters K3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Privacybeleid\"],\"LcuSDR\":[\"Je profielgegevens en metadata beheren\"],\"LqLS9B\":[\"Nicknamewijzigingen weergeven\"],\"LsDQt2\":[\"Kanaalinstellingen\"],\"LtI9AS\":[\"Eigenaar\"],\"LuNhhL\":[\"reageerde op dit bericht\"],\"M/AZNG\":[\"URL naar je avatarafbeelding\"],\"M/WIer\":[\"Bericht verzenden\"],\"M8er/5\":[\"Naam:\"],\"MHk+7g\":[\"Vorige afbeelding\"],\"MRorGe\":[\"Gebruiker een PM sturen\"],\"MVbSGP\":[\"Tijdvenster (seconden)\"],\"MkpcsT\":[\"Je berichten en instellingen worden lokaal op je apparaat opgeslagen\"],\"MzPdC2\":[\"Serverwachtwoord (PASS)\"],\"N/hDSy\":[\"Markeren als bot — gewoonlijk 'aan' of leeg\"],\"N6j2JH\":[[\"0\"],\" bewerken\"],\"N7TQbE\":[\"Gebruiker uitnodigen voor \",[\"channelName\"]],\"NCca/o\":[\"Voer standaard bijnaam in...\"],\"Nqs6B9\":[\"Toont alle externe media. Elke URL kan een verzoek naar een onbekende server veroorzaken.\"],\"Nt+9O7\":[\"WebSocket gebruiken in plaats van raw TCP\"],\"NxIHzc\":[\"Gebruiker verbreken\"],\"O+v/cL\":[\"Alle kanalen op de server bekijken\"],\"OCGpR4\":[\"(overerven)\"],\"ODwSCk\":[\"Een GIF verzenden\"],\"OGQ5kK\":[\"Meldingsgeluiden en markeringen instellen\"],\"OIPt1Z\":[\"Zijbalk met ledenlijst weergeven of verbergen\"],\"OKSNq/\":[\"Zeer streng\"],\"ONWvwQ\":[\"Uploaden\"],\"OVKoQO\":[\"Je accountwachtwoord voor authenticatie\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - IRC naar de toekomst brengen\"],\"OhCpra\":[\"Een onderwerp instellen…\"],\"OkltoQ\":[[\"username\"],\" bannen via nickname (voorkomt dat ze opnieuw deelnemen met dezelfde nick)\"],\"P+t/Te\":[\"Geen aanvullende gegevens\"],\"P42Wcc\":[\"Veilig\"],\"PD38l0\":[\"Kanaalavatar voorbeeldweergave\"],\"PD9mEt\":[\"Typ een bericht...\"],\"PPqfdA\":[\"Kanaelconfiguratie-instellingen openen\"],\"PSCjfZ\":[\"Het onderwerp dat voor dit kanaal wordt weergegeven. Alle gebruikers kunnen het onderwerp zien.\"],\"PZCecv\":[\"PDF-voorbeeld\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 keer\"],\"other\":[[\"c\"],\" keer\"]}]],\"PguS2C\":[\"Uitzonderingsmasker toevoegen (bijv. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[[\"displayedChannelsCount\"],\" van \",[\"0\"],\" kanalen weergegeven\"],\"PqhVlJ\":[\"Gebruiker bannen (via hostmasker)\"],\"Q+chwU\":[\"Gebruikersnaam:\"],\"Q2QY4/\":[\"Deze uitnodiging verwijderen\"],\"Q3v9Wc\":[\"Ja, verwijderen\"],\"Q6hhn8\":[\"Voorkeuren\"],\"QF4a34\":[\"Voer een gebruikersnaam in\"],\"QGqSZ2\":[\"Kleur en opmaak\"],\"QJQd1J\":[\"Profiel bewerken\"],\"QSzGDE\":[\"Inactief\"],\"QUlny5\":[\"Welkom bij \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Meer lezen\"],\"QuSkCF\":[\"Kanalen filteren...\"],\"QwUrDZ\":[\"heeft het onderwerp gewijzigd naar: \",[\"topic\"]],\"R0UH07\":[\"Afbeelding \",[\"0\"],\" van \",[\"1\"]],\"R7SsBE\":[\"Dempen\"],\"R8rf1X\":[\"Klik om onderwerp in te stellen\"],\"RArB3D\":[\"werd gekickt uit \",[\"channelName\"],\" door \",[\"username\"]],\"RI3cWd\":[\"Ontdek de wereld van IRC met ObsidianIRC\"],\"RIfHS5\":[\"Een nieuwe uitnodigingslink aanmaken\"],\"RMMaN5\":[\"Gemodereerd (+m)\"],\"RWw9Lg\":[\"Venster sluiten\"],\"RZ2BuZ\":[\"Accountregistratie voor \",[\"account\"],\" vereist verificatie: \",[\"message\"]],\"RySp6q\":[\"Reacties verbergen\"],\"S5Togi\":[\"Netwerken laden van je bouncer…\"],\"SPKQTd\":[\"Nickname is vereist\"],\"SPVjfj\":[\"Standaard 'geen reden' als leeggelaten\"],\"SQKPvQ\":[\"Gebruiker uitnodigen\"],\"STmlpb\":[\"Terug naar netwerklijst\"],\"SkZcl+\":[\"Kies een vooraf ingesteld floodbeveililingsprofiel. Deze profielen bieden evenwichtige beveiligingsinstellingen voor verschillende toepassingen.\"],\"Slr+3C\":[\"Min. gebruikers\"],\"Spnlre\":[\"Je hebt \",[\"target\"],\" uitgenodigd om deel te nemen aan \",[\"channel\"]],\"T/ckN5\":[\"Openen in viewer\"],\"T91vKp\":[\"Afspelen\"],\"TV2Wdu\":[\"Lees hoe we met je gegevens omgaan en je privacy beschermen.\"],\"TgFpwD\":[\"Toepassen...\"],\"TkzSFB\":[\"Geen wijzigingen\"],\"TtserG\":[\"Echte naam invoeren\"],\"Ttz9J1\":[\"Voer wachtwoord in...\"],\"Tz0i8g\":[\"Instellingen\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reageren\"],\"UE4KO5\":[\"*kanaal*\"],\"UETAwW\":[\"Je hebt nog geen uitnodigingslinks aangemaakt. Gebruik het formulier hierboven om je eerste aan te maken.\"],\"UGT5vp\":[\"Instellingen opslaan\"],\"UV5hLB\":[\"Geen bannen gevonden\"],\"Uaj3Nd\":[\"Statusberichten\"],\"Ue3uny\":[\"Standaard (geen profiel)\"],\"UkARhe\":[\"Normaal — Standaardbeveiliging\"],\"Umn7Cj\":[\"Nog geen reacties. Wees de eerste!\"],\"UtUIRh\":[[\"0\"],\" oudere berichten\"],\"UwzP+U\":[\"Beveiligde verbinding\"],\"V0/A4O\":[\"Kanaaleigenaar\"],\"V0zZWc\":[\"Hiermee wordt ook het gebonden netwerk hieronder gesloten.\"],\"V4qgxE\":[\"Aangemaakt voor (min geleden)\"],\"V8yTm6\":[\"Zoekopdracht wissen\"],\"VJMMyz\":[\"ObsidianIRC — IRC de toekomst in\"],\"VJScHU\":[\"Reden\"],\"VLsmVV\":[\"Meldingen dempen\"],\"VbyRUy\":[\"Reacties\"],\"Vmx0mQ\":[\"Ingesteld door:\"],\"VqnIZz\":[\"Ons privacybeleid en gegevenspraktijken bekijken\"],\"VrMygG\":[\"Minimale lengte is \",[\"0\"]],\"VrnTui\":[\"Je voornaamwoorden, weergegeven in je profiel\"],\"W8E3qn\":[\"Geverifieerd account\"],\"WAakm9\":[\"Kanaal verwijderen\"],\"WFxTHC\":[\"Banmasker toevoegen (bijv. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Serverhost is vereist\"],\"WRYdXW\":[\"Audiopositie\"],\"WUOH5B\":[\"Gebruiker negeren\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Toon 1 item meer\"],\"other\":[\"Toon \",[\"1\"],\" items meer\"]}]],\"WYxRzo\":[\"Beheer en maak je uitnodigingslinks aan\"],\"Wd38W1\":[\"Laat het kanaal leeg voor een algemene netwerkuitnodiging. De beschrijving is alleen voor je eigen administratie â alleen voor jou zichtbaar in deze lijst.\"],\"Weq9zb\":[\"Algemeen\"],\"Wfj7Sk\":[\"Meldingsgeluiden dempen of activeren\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Gebruikersprofiel\"],\"X6S3lt\":[\"Instellingen, kanalen, servers zoeken...\"],\"XEHan5\":[\"Toch doorgaan\"],\"XI1+wb\":[\"Ongeldig formaat\"],\"XIXeuC\":[\"Bericht aan @\",[\"0\"]],\"XMS+k4\":[\"Privébericht starten\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Privégesprek losmaken\"],\"Xm/s+u\":[\"Weergave\"],\"Xp2n93\":[\"Toont media van de vertrouwde bestandshost van je server. Er worden geen verzoeken gedaan aan externe diensten.\"],\"XvjC4F\":[\"Opslaan...\"],\"Y/qryO\":[\"Geen gebruikers gevonden die overeenkomen met je zoekopdracht\"],\"YAqRpI\":[\"Accountregistratie voor \",[\"account\"],\" geslaagd: \",[\"message\"]],\"YEfzvP\":[\"Beveiligd onderwerp (+t)\"],\"YQOn6a\":[\"Ledenlijst inklappen\"],\"YRCoE9\":[\"Kanaaloperator\"],\"YURQaF\":[\"Profiel bekijken\"],\"YdBSvr\":[\"Mediaweergave en externe inhoud beheren\"],\"Yj6U3V\":[\"Geen centrale server:\"],\"YjvpGx\":[\"Voornaamwoorden\"],\"YqH4l4\":[\"Geen sleutel\"],\"YyUPpV\":[\"Account:\"],\"ZJSWfw\":[\"Bericht dat wordt weergegeven wanneer je de verbinding met de server verbreekt\"],\"ZR1dJ4\":[\"Uitnodigingen\"],\"ZdWg0V\":[\"Openen in browser\"],\"ZhRBbl\":[\"Berichten zoeken…\"],\"Zmcu3y\":[\"Geavanceerde filters\"],\"a0bHay\":[\"<0>\",[\"0\"],\"0> ontkoppelen?\"],\"a2/8e5\":[\"Onderwerp ingesteld na (min geleden)\"],\"aHKcKc\":[\"Vorige pagina\"],\"aJTbXX\":[\"Oper-wachtwoord\"],\"aQryQv\":[\"Patroon bestaat al\"],\"aW9pLN\":[\"Maximaal aantal toegestane gebruikers in het kanaal. Laat leeg voor geen limiet.\"],\"ah4fmZ\":[\"Toont ook voorbeeldweergaven van YouTube, Vimeo, SoundCloud en vergelijkbare bekende diensten.\"],\"aifXak\":[\"Geen media in dit kanaal\"],\"ap2zBz\":[\"Ontspannen\"],\"az8lvo\":[\"Uit\"],\"azXSNo\":[\"Ledenlijst uitvouwen\"],\"azdliB\":[\"Aanmelden bij een account\"],\"b26wlF\":[\"zij/haar\"],\"bD/+Ei\":[\"Streng\"],\"bQ6BJn\":[\"Stel gedetailleerde floodbeveiligingsregels in. Elke regel bepaalt welk type activiteit wordt bewaakt en welke actie wordt ondernomen als drempelwaarden worden overschreden.\"],\"beV7+y\":[\"De gebruiker ontvangt een uitnodiging om deel te nemen aan \",[\"channelName\"],\".\"],\"bk84cH\":[\"Afwezigheidsbericht\"],\"bkHdLj\":[\"IRC-server toevoegen\"],\"bmQLn5\":[\"Regel toevoegen\"],\"bv4cFj\":[\"Transport\"],\"bwRvnp\":[\"Actie\"],\"c8+EVZ\":[\"Geverifieerd account\"],\"cGYUlD\":[\"Er worden geen mediavoorbeeldweergaven geladen.\"],\"cLF98o\":[\"Reacties tonen (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Geen gebruikers beschikbaar\"],\"cSgpoS\":[\"Privégesprek vastmaken\"],\"cXeEKu\":[\"Hiermee worden ook de \",[\"0\"],\" gebonden netwerken hieronder gesloten.\"],\"cde3ce\":[\"Bericht aan <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Kopieer opgemaakte uitvoer\"],\"cl/A5J\":[\"Welkom bij \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Verwijderen\"],\"coPLXT\":[\"We slaan je IRC-communicatie niet op onze servers op\"],\"crYH/6\":[\"SoundCloud-speler\"],\"cv5DQb\":[\"geen host ingesteld\"],\"d3sis4\":[\"Server toevoegen\"],\"d9aN5k\":[[\"username\"],\" uit het kanaal verwijderen\"],\"dEgA5A\":[\"Annuleren\"],\"dGi1We\":[\"Dit privéberichtgesprek losmaken\"],\"dJVuyC\":[\"heeft \",[\"channelName\"],\" verlaten (\",[\"reason\"],\")\"],\"dMtLDE\":[\"aan\"],\"dXqxlh\":[\"<0>⚠️ Beveiligingsrisico!0> Deze verbinding kan kwetsbaar zijn voor onderschepping of man-in-the-middle-aanvallen.\"],\"da9Q/R\":[\"Kanaalmodi gewijzigd\"],\"dhJN3N\":[\"Reacties tonen\"],\"dj2xTE\":[\"Melding sluiten\"],\"dpCzmC\":[\"Floodbeveiligingsinstellingen\"],\"e9dQpT\":[\"Wil je deze link in een nieuw tabblad openen?\"],\"ePK91l\":[\"Bewerken\"],\"eYBDuB\":[\"Upload een afbeelding of geef een URL op met optionele \",[\"size\"],\"-vervanging voor dynamische grootte\"],\"edBbee\":[[\"username\"],\" bannen via hostmasker (voorkomt dat ze opnieuw deelnemen via hetzelfde IP/host)\"],\"ekfzWq\":[\"Gebruikersinstellingen\"],\"elPDWs\":[\"Pas je IRC-clientervaring aan\"],\"eu2osY\":[\"<0>💡 Aanbeveling:0> Ga alleen verder als je deze server vertrouwt en de risico's begrijpt. Deel geen gevoelige informatie of wachtwoorden via deze verbinding.\"],\"euEhbr\":[\"Klik om deel te nemen aan \",[\"channel\"]],\"ez3vLd\":[\"Meerdere regels invoer inschakelen\"],\"f0J5Ki\":[\"Server-naar-servercommunicatie kan niet-versleutelde verbindingen gebruiken\"],\"f9BHJk\":[\"Gebruiker waarschuwen\"],\"fDOLLd\":[\"Geen kanalen gevonden.\"],\"ffzDkB\":[\"Anonieme analyses:\"],\"fq1GF9\":[\"Weergeven wanneer gebruikers de verbinding met de server verbreken\"],\"gCldcN\":[\"Accentkleur wijzigen\"],\"gEF57C\":[\"Deze server ondersteunt slechts één verbindingstype\"],\"gJuLUI\":[\"Negeerlijst\"],\"gNzMrk\":[\"Huidige avatar\"],\"gjPWyO\":[\"Voer bijnaam in...\"],\"gz6UQ3\":[\"Maximaliseren\"],\"h6/IMX\":[\"Voeg je eerste netwerk toe\"],\"h6razj\":[\"Kanaalnaammasker uitsluiten\"],\"hG6jnw\":[\"Geen onderwerp ingesteld\"],\"hG89Ed\":[\"Afbeelding\"],\"hYgDIe\":[\"Aanmaken\"],\"hZ6znB\":[\"Poort\"],\"ha+Bz5\":[\"bijv. 100:1440\"],\"he3ygx\":[\"Kopiëren\"],\"hehnjM\":[\"Aantal\"],\"hzdLuQ\":[\"Alleen gebruikers met voice of hoger kunnen spreken\"],\"i0qMbr\":[\"Start\"],\"iDNBZe\":[\"Meldingen\"],\"iH8pgl\":[\"Terug\"],\"iL9SZg\":[\"Gebruiker bannen (via nickname)\"],\"iNt+3c\":[\"Terug naar afbeelding\"],\"iQvi+a\":[\"Niet meer waarschuwen over lage verbindingsbeveiliging voor deze server\"],\"iSLIjg\":[\"Verbinden\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Serverhost\"],\"idD8Ev\":[\"Opgeslagen\"],\"iivqkW\":[\"Aangemeld op\"],\"ij+Elv\":[\"Afbeeldingsvoorbeeldweergave\"],\"ilIWp7\":[\"Meldingen aan/uit\"],\"iuaqvB\":[\"Gebruik * voor jokertekens. Voorbeelden: slechtegebruiker!*@*, *!*@spammer.com, trol*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Bannen via hostmasker\"],\"jA4uoI\":[\"Onderwerp:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Reden (optioneel)\"],\"jUV7CU\":[\"Avatar uploaden\"],\"jW5Uwh\":[\"Bepaal hoeveel externe media worden geladen. Uit / Veilig / Vertrouwde bronnen / Alle inhoud.\"],\"jXzms5\":[\"Bijlageopties\"],\"jZlrte\":[\"Kleur\"],\"jfC/xh\":[\"Contact\"],\"jywMpv\":[\"#nieuwe-kanaalnaam\"],\"k112DD\":[\"Oudere berichten laden\"],\"k3ID0F\":[\"Leden filteren…\"],\"k65gsE\":[\"Diepgaande analyse\"],\"k7Zgob\":[\"Verbinding annuleren\"],\"kAVx5h\":[\"Geen uitnodigingen gevonden\"],\"kCLEPU\":[\"Verbonden met\"],\"kF5LKb\":[\"Genegeerde patronen:\"],\"kGeOx/\":[\"Deelnemen aan \",[\"0\"]],\"kITKr8\":[\"Kanaalmodi laden...\"],\"kPpPsw\":[\"Je bent een IRC Operator\"],\"kWJmRL\":[\"Jij\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Kopieer JSON\"],\"krViRy\":[\"Klik om te kopiëren als JSON\"],\"ks71ra\":[\"Uitzonderingen\"],\"kw4lRv\":[\"Kanaal half-operator\"],\"kxgIRq\":[\"Selecteer of voeg een kanaal toe om te beginnen.\"],\"ky6dWe\":[\"Avatarvoorbeeldweergave\"],\"l+GxCv\":[\"Kanalen laden...\"],\"l+IUVW\":[\"Accountverificatie voor \",[\"account\"],\" geslaagd: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"opnieuw verbonden\"],\"other\":[[\"reconnectCount\"],\" keer opnieuw verbonden\"]}]],\"l1l8sj\":[[\"0\"],\"d geleden\"],\"l5NhnV\":[\"#kanaal (optioneel)\"],\"l5jmzx\":[[\"0\"],\" en \",[\"1\"],\" zijn aan het typen...\"],\"lCF0wC\":[\"Vernieuwen\"],\"lHy8N5\":[\"Meer kanalen laden...\"],\"lasgrr\":[\"gebruikt\"],\"lbpf14\":[\"Deelnemen aan \",[\"value\"]],\"lfFsZ4\":[\"Kanalen\"],\"lkNdiH\":[\"Accountnaam\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Afbeelding uploaden\"],\"loQxaJ\":[\"Ik ben terug\"],\"lvfaxv\":[\"START\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Toevoegen\"],\"m8flAk\":[\"Voorbeeld (nog niet geüpload)\"],\"mEPxTp\":[\"<0>⚠️ Wees voorzichtig!0> Open alleen links van vertrouwde bronnen. Kwaadaardige links kunnen je beveiliging of privacy in gevaar brengen.\"],\"mHGdhG\":[\"Serverinformatie\"],\"mHS8lb\":[\"Bericht in #\",[\"0\"]],\"mMYBD9\":[\"Breed — Ruimere beveiligingsscope\"],\"mTGsPd\":[\"Kanaalonderwerp\"],\"mU8j6O\":[\"Geen externe berichten (+n)\"],\"mZp8FL\":[\"Automatisch terugvallen op één regel\"],\"mdQu8G\":[\"JouwNickname\"],\"miSSBQ\":[\"Reacties (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Gebruiker is geverifieerd\"],\"mwtcGl\":[\"Reacties sluiten\"],\"myL0MR\":[\"Dit netwerk verwijderen?\"],\"mzI/c+\":[\"Downloaden\"],\"n3fGRk\":[\"ingesteld door \",[\"0\"]],\"n5+j9l\":[\"bijv. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Ontspannen — Minder agressieve beveiliging\"],\"nNflMD\":[\"Kanaal verlaten\"],\"nPXkBi\":[\"WHOIS-gegevens laden...\"],\"nQnxxF\":[\"Bericht in #\",[\"0\"],\" (Shift+Enter voor nieuwe regel)\"],\"nWMRxa\":[\"Losmaken\"],\"nkC032\":[\"Geen floodprofiel\"],\"o69z4d\":[\"Een waarschuwingsbericht sturen naar \",[\"username\"]],\"o9ylQi\":[\"Zoek naar GIF's om te beginnen\"],\"oFGkER\":[\"Serverberichten\"],\"oOi11l\":[\"Naar beneden scrollen\"],\"oPYIL5\":[\"netwerk\"],\"oQEzQR\":[\"Nieuw DM\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Man-in-the-middle-aanvallen op serververbindingen zijn mogelijk\"],\"oeqmmJ\":[\"Vertrouwde bronnen\"],\"optX0N\":[[\"0\"],\"u geleden\"],\"ovBPCi\":[\"Standaard\"],\"p0Z69r\":[\"Patroon kan niet leeg zijn\"],\"p1KgtK\":[\"Audio laden mislukt\"],\"p59pEv\":[\"Extra details\"],\"p7sRI6\":[\"Laat anderen weten wanneer je typt\"],\"pBm1od\":[\"Geheim kanaal\"],\"pNmiXx\":[\"Je standaard nickname voor alle servers\"],\"pUUo9G\":[\"Hostnaam:\"],\"pVGPmz\":[\"Accountwachtwoord\"],\"peNE68\":[\"Permanent\"],\"plhHQt\":[\"Geen gegevens\"],\"pm6+q5\":[\"Beveiligingswaarschuwing\"],\"pn5qSs\":[\"Aanvullende informatie\"],\"q0cR4S\":[\"is nu bekend als **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanaal verschijnt niet in LIST- of NAMES-opdrachten\"],\"qLpTm/\":[\"Reactie \",[\"emoji\"],\" verwijderen\"],\"qVkGWK\":[\"Vastmaken\"],\"qY8wNa\":[\"Startpagina\"],\"qb0xJ7\":[\"Gebruik jokertekens: * komt overeen met een reeks, ? met één teken. Voorbeelden: nick!*@*, *!*@host.com, *!*gebruiker@*\"],\"qhzpRq\":[\"Kanaalsleutel (+k)\"],\"qtoOYG\":[\"Geen limiet\"],\"r1W2AS\":[\"Afbeelding van bestandshost\"],\"rIPR2O\":[\"Onderwerp ingesteld voor (min geleden)\"],\"rMMSYo\":[\"Maximale lengte is \",[\"0\"]],\"rWtzQe\":[\"Het netwerk splitste en herverbond. ✅\"],\"rYG2u6\":[\"Even wachten...\"],\"rdUucN\":[\"Voorbeeld\"],\"rjGI/Q\":[\"Privacy\"],\"rk8iDX\":[\"GIF's laden...\"],\"rn6SBY\":[\"Dempen opheffen\"],\"s/UKqq\":[\"Uit het kanaal verwijderd\"],\"s7oqXR\":[\"Selecteer een netwerk\"],\"s8cATI\":[\"heeft \",[\"channelName\"],\" betreden\"],\"sCO9ue\":[\"De verbinding met <0>\",[\"serverName\"],\"0> heeft de volgende beveiligingsproblemen:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"is nu bekend als **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" heeft je uitgenodigd om deel te nemen aan \",[\"channel\"]],\"sUBSbK\":[\"Nog geen upstream-netwerken.\"],\"sby+1/\":[\"Klik om te kopiëren\"],\"sfN25C\":[\"Je echte of volledige naam\"],\"sliuzR\":[\"Link openen\"],\"sqrO9R\":[\"Aangepaste vermeldingen\"],\"sr6RdJ\":[\"Meerdere regels met Shift+Enter\"],\"swrCpB\":[\"Het kanaal is hernoemd van \",[\"oldName\"],\" naar \",[\"newName\"],\" door \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Geavanceerd\"],\"t/YqKh\":[\"Verwijderen\"],\"t47eHD\":[\"Je unieke identificatie op deze server\"],\"tAkAh0\":[\"URL met optionele \",[\"size\"],\"-vervanging voor dynamische grootte. Voorbeeld: https://voorbeeld.com/avatar/\",[\"size\"],\"/kanaal.jpg\"],\"tXLJS3\":[\"Zijbalk met kanaallijst weergeven of verbergen\"],\"tfDRzk\":[\"Opslaan\"],\"tiBsJk\":[\"heeft \",[\"channelName\"],\" verlaten\"],\"tt4/UD\":[\"verliet de server (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Nickname {nick} is al in gebruik, probeer opnieuw met {newNick}\"],\"u0a8B4\":[\"Authenticeren als IRC Operator voor beheerderstoegang\"],\"u0rWFU\":[\"Aangemaakt na (min geleden)\"],\"u72w3t\":[\"Gebruikers en patronen om te negeren\"],\"u7jc2L\":[\"verliet de server\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Opslaan mislukt: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC-servers:\"],\"ukyW4o\":[\"Jouw uitnodigingslinks\"],\"usSSr/\":[\"Zoomniveau\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Gebruik Shift+Enter voor nieuwe regels (Enter verstuurt)\"],\"vERlcd\":[\"Profiel\"],\"vK0RL8\":[\"Geen onderwerp\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Taal\"],\"vaHYxN\":[\"Echte naam\"],\"vhjbKr\":[\"Afwezig\"],\"w/nogd\":[[\"0\"],\" netwerk\",[\"1\"],\" — kies er een om mee te doen\"],\"w4NYox\":[[\"title\"],\" client\"],\"w8xQRx\":[\"Ongeldige waarde\"],\"wFjjxZ\":[\"werd gekickt uit \",[\"channelName\"],\" door \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Geen uitzonderingen op bannen gevonden\"],\"wPrGnM\":[\"Kanaalbeheerder\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Weergeven wanneer gebruikers kanalen betreden of verlaten\"],\"whqZ9r\":[\"Extra woorden of zinnen om te markeren\"],\"wm7RV4\":[\"Meldingsgeluid\"],\"wz/Yoq\":[\"Je berichten kunnen worden onderschept wanneer ze tussen servers worden doorgegeven\"],\"x3+y8b\":[\"Zoveel mensen hebben zich via deze link geregistreerd\"],\"xCJdfg\":[\"Wissen\"],\"xOTzt5\":[\"zojuist\"],\"xUHRTR\":[\"Automatisch als operator authenticeren bij verbinden\"],\"xWHwwQ\":[\"Bannen\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"Deze server ondersteunt geen uitnodigingslinks (de<0>obby.world/invitation0>-capability wordt niet aangekondigd). Je kunt nog gewoon chatten; dit paneel is voor netwerken die op obbyircd draaien.\"],\"xceQrO\":[\"Alleen beveiligde WebSockets worden ondersteund\"],\"xdtXa+\":[\"kanaalnaam\"],\"xfXC7q\":[\"Tekstkanalen\"],\"xlCYOE\":[\"Meer berichten ophalen...\"],\"xlhswE\":[\"Minimale waarde is \",[\"0\"]],\"xq97Ci\":[\"Voeg een woord of zin toe...\"],\"xuRqRq\":[\"Clientlimiet (+l)\"],\"xwF+7J\":[[\"0\"],\" typt...\"],\"y1eoq1\":[\"Link kopiëren\"],\"yJztBY\":[\"Netwerk verwijderen\"],\"yNeucF\":[\"Deze server ondersteunt geen uitgebreide profielmetadata (IRCv3 METADATA-extensie). Extra velden zoals avatar, weergavenaam en status zijn niet beschikbaar.\"],\"yPlrca\":[\"Kanaalavatar\"],\"yQE2r9\":[\"Laden\"],\"ySU+JY\":[\"jouw@email.com\"],\"yTX1Rt\":[\"Oper-gebruikersnaam\"],\"yYOzWD\":[\"logboeken\"],\"yfx9Re\":[\"IRC operator-wachtwoord\"],\"ygCKqB\":[\"Stoppen\"],\"ymDxJx\":[\"IRC operator-gebruikersnaam\"],\"yrpRsQ\":[\"Sorteren op naam\"],\"yz7wBu\":[\"Sluiten\"],\"zJw+jA\":[\"stelt modus in: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"m geleden\"],\"zebeLu\":[\"Oper-gebruikersnaam invoeren\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/nl/messages.po b/src/locales/nl/messages.po
index 2539f54d..accaeafa 100644
--- a/src/locales/nl/messages.po
+++ b/src/locales/nl/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - IRC naar de toekomst brengen"
msgid "— open in viewer"
msgstr "— openen in viewer"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(overerven)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(ongewijzigd)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} en {1} zijn aan het typen..."
msgid "{0} is typing..."
msgstr "{0} typt..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} netwerk{1} — kies er een om mee te doen"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -205,6 +221,12 @@ msgstr "Uitnodigingsmasker toevoegen (bijv. nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "IRC-server toevoegen"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Netwerk toevoegen"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Regel toevoegen"
msgid "Add Server"
msgstr "Server toevoegen"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Voeg je eerste netwerk toe"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Extra details"
@@ -377,6 +403,10 @@ msgstr "Terug"
msgid "Back to image"
msgstr "Terug naar afbeelding"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Terug naar netwerklijst"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "{username} bannen via hostmasker (voorkomt dat ze opnieuw deelnemen via hetzelfde IP/host)"
@@ -424,6 +454,10 @@ msgstr "Alle kanalen op de server bekijken"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Verbinding annuleren"
msgid "Cancel reply"
msgstr "Antwoord annuleren"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Accentkleur wijzigen"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Kanaalnaam wijzigen (alleen operators)"
@@ -606,6 +645,8 @@ msgstr "Clientlimiet (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -669,6 +710,7 @@ msgid "Confirm?"
msgstr "Bevestigen?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Verbinden"
@@ -711,11 +753,11 @@ msgstr "Gekopieerd"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy"
-msgstr "Kopiëren"
+msgstr "Kopiëren"
#: src/components/ui/RawLogViewer.tsx
msgid "Copy all"
-msgstr "Alles kopiëren"
+msgstr "Alles kopiëren"
#: src/components/message/JsonLogMessage.tsx
msgid "Copy entire JSON"
@@ -731,7 +773,7 @@ msgstr "Kopieer JSON"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy link"
-msgstr "Link kopiëren"
+msgstr "Link kopiëren"
#: src/components/ui/ExternalLinkWarningModal.tsx
msgid "Copy URL"
@@ -814,6 +856,10 @@ msgstr "Kanaal verwijderen"
msgid "Delete message"
msgstr "Bericht verwijderen"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Netwerk verwijderen"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Privégesprek verwijderen"
@@ -826,15 +872,36 @@ msgstr "Deze uitnodiging verwijderen"
msgid "Delete this message? This cannot be undone."
msgstr "Dit bericht verwijderen? Dit kan niet ongedaan worden gemaakt."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Dit netwerk verwijderen?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "Beschrijving (optioneel, bijv. \"Bètatesters K3\")"
+msgstr "Beschrijving (optioneel, bijv. \"Bètatesters K3\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Verbreken"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "<0>{0}0> ontkoppelen?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Verbinding met soju-bouncer verbreken?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Netwerkverbinding verbreken?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Ontdekken"
@@ -893,18 +960,29 @@ msgstr "Downloaden"
msgid "Drop files to upload"
msgstr "Bestanden hier neerzetten om te uploaden"
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "bijv. <0>wss://host:port/socket0>"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "bijv. 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Bewerken"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "{0} bewerken"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Profiel bewerken"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "START"
msgid "Homepage"
msgstr "Startpagina"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Host"
@@ -1323,7 +1402,7 @@ msgstr "Kanaal verlaten"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Laat het kanaal leeg voor een algemene netwerkuitnodiging. De beschrijving is alleen voor je eigen administratie — alleen voor jou zichtbaar in deze lijst."
+msgstr "Laat het kanaal leeg voor een algemene netwerkuitnodiging. De beschrijving is alleen voor je eigen administratie â alleen voor jou zichtbaar in deze lijst."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "Het kanaal verlaten"
msgid "Let others know when you are typing"
msgstr "Laat anderen weten wanneer je typt"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Linkvoorbeeldweergave"
@@ -1375,6 +1458,10 @@ msgstr "GIF's laden..."
msgid "Loading more channels..."
msgstr "Meer kanalen laden..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Netwerken laden van je bouncer…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "WHOIS-gegevens laden..."
@@ -1565,10 +1652,21 @@ msgstr "Naam:"
msgid "network"
msgstr "netwerk"
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Netwerk gebonden via soju-bouncer"
+
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Netwerknaam"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Netwerken op {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Nieuw DM"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!gebruiker@host (bijv. spam*!*@*, *!*@slechtehost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Geen bestand gekozen"
msgid "No flood profile"
msgstr "Geen floodprofiel"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "geen host ingesteld"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Geen uitnodigingen gevonden"
@@ -1698,6 +1801,10 @@ msgstr "Geen onderwerp ingesteld"
msgid "No unread mentions or messages"
msgstr "Geen ongelezen vermeldingen of berichten"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Nog geen upstream-netwerken."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Geen gebruikers beschikbaar"
@@ -1784,6 +1891,10 @@ msgstr "Oeps! Netwerksplitsing! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Openen"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Kanaelconfiguratie-instellingen openen"
@@ -1887,6 +1998,10 @@ msgstr "Privégesprek vastmaken"
msgid "Pin this private message conversation"
msgstr "Dit privéberichtgesprek vastmaken"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Platte tekst"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "Gebruiker een PM sturen"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Poort"
@@ -2006,6 +2122,7 @@ msgstr "reageerde op dit bericht"
msgid "Read more"
msgstr "Meer lezen"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Reden"
msgid "Reason (optional)"
msgstr "Reden (optioneel)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Opnieuw verbinden met server"
@@ -2095,6 +2213,7 @@ msgstr "Regels"
msgid "Safe"
msgstr "Veilig"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Spoelen"
msgid "Select a channel"
msgstr "Selecteer een kanaal"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Selecteer een netwerk"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Lid selecteren"
@@ -2276,6 +2399,10 @@ msgstr "Serveroperators op het netwerk kunnen mogelijk je berichten lezen"
msgid "Server Password"
msgstr "Serverwachtwoord"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Serverwachtwoord (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Server-naar-servercommunicatie kan niet-versleutelde verbindingen gebruiken"
@@ -2380,6 +2507,12 @@ msgstr "Aangemeld op"
msgid "Software:"
msgstr "Software:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "soju-bouncer (besturing)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Sorteren op naam"
@@ -2459,6 +2592,10 @@ msgstr "Deze afbeelding is verlopen"
msgid "This many people registered through this link"
msgstr "Zoveel mensen hebben zich via deze link geregistreerd"
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Hiermee wordt het netwerk uit je soju-bouncer verwijderd. Om het opnieuw te gebruiken, moet je het opnieuw toevoegen."
+
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
msgstr "Deze server ondersteunt geen uitgebreide profielmetadata (IRCv3 METADATA-extensie). Extra velden zoals avatar, weergavenaam en status zijn niet beschikbaar."
@@ -2471,6 +2608,15 @@ msgstr "Deze server ondersteunt geen uitnodigingslinks (de<0>obby.world/invitati
msgid "This server only supports one connection type"
msgstr "Deze server ondersteunt slechts één verbindingstype"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Hiermee worden ook de {0} gebonden netwerken hieronder gesloten."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Hiermee wordt ook het gebonden netwerk hieronder gesloten."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Tijd (min)"
@@ -2479,6 +2625,10 @@ msgstr "Tijd (min)"
msgid "Time Window (seconds)"
msgstr "Tijdvenster (seconden)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Onderwerp:"
msgid "Total: {0}"
msgstr "Totaal: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Transport"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Vertrouwde bronnen"
@@ -2641,6 +2795,7 @@ msgstr "Gebruikersprofiel"
msgid "User Settings"
msgstr "Gebruikersinstellingen"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Breed — Ruimere beveiligingsscope"
msgid "Will default to 'no reason' if left empty"
msgstr "Standaard 'geen reden' als leeggelaten"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Ja, verwijderen"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2814,6 +2973,11 @@ msgstr "Je hebt nog geen uitnodigingslinks aangemaakt. Gebruik het formulier hie
msgid "You invited {target} to join {channel}"
msgstr "Je hebt {target} uitgenodigd om deel te nemen aan {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Je bent verbonden met <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Je accountwachtwoord voor authenticatie"
@@ -2822,6 +2986,10 @@ msgstr "Je accountwachtwoord voor authenticatie"
msgid "Your account username for authentication"
msgstr "Je accountgebruikersnaam voor authenticatie"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Je bouncer heeft nog geen netwerken. Voeg er een toe om te beginnen."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Je standaard nickname voor alle servers"
diff --git a/src/locales/pl/messages.mjs b/src/locales/pl/messages.mjs
index b4c322a9..6e9d17ac 100644
--- a/src/locales/pl/messages.mjs
+++ b/src/locales/pl/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Nieprawidłowy format wzorca. Użyj formatu nick!user@host (dozwolone symbole wieloznaczne *)\"],\"+6NQQA\":[\"Ogólny kanał wsparcia\"],\"+6NyRG\":[\"Klient\"],\"+K0AvT\":[\"Rozłącz\"],\"+cyFdH\":[\"Domyślna wiadomość przy ustawianiu statusu nieobecności\"],\"+mVPqU\":[\"Renderuj formatowanie Markdown w wiadomościach\"],\"+vqCJH\":[\"Nazwa użytkownika Twojego konta do uwierzytelniania\"],\"+yPBXI\":[\"Wybierz plik\"],\"+zy2Nq\":[\"Typ\"],\"/09cao\":[\"Niskie bezpieczeństwo połączenia (poziom \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Użytkownicy spoza kanału nie mogą wysyłać do niego wiadomości\"],\"/4C8U0\":[\"Kopiuj wszystko\"],\"/6BzZF\":[\"Przełącz listę członków\"],\"/AkXyp\":[\"Potwierdzić?\"],\"/TNOPk\":[\"Użytkownik jest nieobecny\"],\"/XQgft\":[\"Odkryj\"],\"/cF7Rs\":[\"Głośność\"],\"/dqduX\":[\"Następna strona\"],\"/fc3q4\":[\"Wszystkie treści\"],\"/kISDh\":[\"Włącz dźwięki powiadomień\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Odtwarzaj dźwięki dla wzmianek i wiadomości\"],\"0/0ZGA\":[\"Maska nazwy kanału\"],\"0D6j7U\":[\"Dowiedz się więcej o niestandardowych regułach →\"],\"0XsHcR\":[\"Wyrzuć użytkownika\"],\"0ZpE//\":[\"Sortuj według użytkowników\"],\"0bEPwz\":[\"Ustaw nieobecność\"],\"0dGkPt\":[\"Rozwiń listę kanałów\"],\"0gS7M5\":[\"Wyświetlana nazwa\"],\"0kS+M8\":[\"PrzykładSIEĆ\"],\"0rgoY7\":[\"Łącz się tylko z serwerami, które wybierzesz\"],\"0wdd7X\":[\"Dołącz\"],\"0wkVYx\":[\"Wiadomości prywatne\"],\"111uHX\":[\"Podgląd linku\"],\"196EG4\":[\"Usuń prywatną rozmowę\"],\"1DSr1i\":[\"Zarejestruj konto\"],\"1O/24y\":[\"Przełącz listę kanałów\"],\"1VPJJ2\":[\"Ostrzeżenie o zewnętrznym linku\"],\"1ZC/dv\":[\"Brak nieprzeczytanych wzmianek lub wiadomości\"],\"1pO1zi\":[\"Nazwa serwera jest wymagana\"],\"1uwfzQ\":[\"Zobacz temat kanału\"],\"268g7c\":[\"Wpisz wyświetlaną nazwę\"],\"2F9+AZ\":[\"Nie przechwycono jeszcze surowego ruchu IRC. Spróbuj się połączyć lub wysłać wiadomość.\"],\"2FOFq1\":[\"Operatorzy serwerów w sieci mogą potencjalnie odczytywać Twoje wiadomości\"],\"2FYpfJ\":[\"Więcej\"],\"2HF1Y2\":[[\"inviter\"],\" zaprosił \",[\"target\"],\" do dołączenia do \",[\"channel\"]],\"2I70QL\":[\"Zobacz informacje o profilu użytkownika\"],\"2QYdmE\":[\"Użytkownicy:\"],\"2QpEjG\":[\"wyszedł\"],\"2YE223\":[\"Wiadomość na #\",[\"0\"],\" (Enter – nowa linia, Shift+Enter – wyślij)\"],\"2bimFY\":[\"Użyj hasła serwera\"],\"2iTmdZ\":[\"Pamięć lokalna:\"],\"2odkwe\":[\"Rygorystyczny – bardziej agresywna ochrona\"],\"2uDhbA\":[\"Wpisz nazwę użytkownika do zaproszenia\"],\"2ygf/L\":[\"← Wróć\"],\"2zEgxj\":[\"Szukaj GIFów...\"],\"3RdPhl\":[\"Zmień nazwę kanału\"],\"3THokf\":[\"Użytkownik z głosem\"],\"3TSz9S\":[\"Minimalizuj\"],\"3jBDvM\":[\"Wyświetlana nazwa kanału\"],\"3ryuFU\":[\"Opcjonalne raporty o błędach w celu ulepszenia aplikacji\"],\"3uBF/8\":[\"Zamknij przeglądarkę\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Wprowadź nazwę konta...\"],\"4/Rr0R\":[\"Zaproś użytkownika do bieżącego kanału\"],\"4EZrJN\":[\"Reguły\"],\"4JJtW9\":[\"#przepełnienie\"],\"4NqeT4\":[\"Profil floodu (+F)\"],\"4RZQRK\":[\"Co teraz robisz?\"],\"4hfTrB\":[\"Nick\"],\"4n99LO\":[\"Już w \",[\"0\"]],\"4t6vMV\":[\"Automatycznie przełącz na jedną linię dla krótkich wiadomości\"],\"4vsHmf\":[\"Czas (min)\"],\"5+INAX\":[\"Podświetlaj wiadomości, w których jesteś wzmiankowany\"],\"5R5Pv/\":[\"Nazwa operatora\"],\"678PKt\":[\"Nazwa sieci\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Hasło wymagane do dołączenia do kanału. Pozostaw puste, aby usunąć klucz.\"],\"6HhMs3\":[\"Wiadomość pożegnalna\"],\"6V3Ea3\":[\"Skopiowano\"],\"6lGV3K\":[\"Pokaż mniej\"],\"6yFOEi\":[\"Wprowadź hasło opera...\"],\"7+IHTZ\":[\"Nie wybrano pliku\"],\"73hrRi\":[\"nick!user@host (np. spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Wyślij wiadomość prywatną\"],\"7U1W7c\":[\"Bardzo łagodny\"],\"7Y1YQj\":[\"Imię i nazwisko:\"],\"7YHArF\":[\"— otwórz w przeglądarce\"],\"7fjnVl\":[\"Szukaj użytkowników...\"],\"7jL88x\":[\"Usunąć tę wiadomość? Tej operacji nie można cofnąć.\"],\"7nGhhM\":[\"Co masz na myśli?\"],\"7sEpu1\":[\"Członkowie — \",[\"0\"]],\"7sNhEz\":[\"Nazwa użytkownika\"],\"8H0Q+x\":[\"Dowiedz się więcej o profilach →\"],\"8Phu0A\":[\"Wyświetlaj gdy użytkownicy zmieniają nick\"],\"8XTG9e\":[\"Wpisz hasło operatora\"],\"8XsV2J\":[\"Spróbuj wysłać ponownie\"],\"8ZsakT\":[\"Hasło\"],\"8kR84m\":[\"Zamierzasz otworzyć zewnętrzny link:\"],\"8lCgih\":[\"Usuń regułę\"],\"8o3dPc\":[\"Upuść pliki, aby przesłać\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"dołączył\"],\"few\":[\"dołączył \",[\"joinCount\"],\" razy\"],\"many\":[\"dołączył \",[\"joinCount\"],\" razy\"],\"other\":[\"dołączył \",[\"joinCount\"],\" razy\"]}]],\"9BMLnJ\":[\"Ponownie połącz z serwerem\"],\"9OEgyT\":[\"Dodaj reakcję\"],\"9PQ8m2\":[\"G-Line (globalny ban)\"],\"9Qs99X\":[\"E-mail:\"],\"9QupBP\":[\"Usuń wzorzec\"],\"9bG48P\":[\"Wysyłanie\"],\"9f5f0u\":[\"Pytania dotyczące prywatności? Skontaktuj się z nami:\"],\"9unqs3\":[\"Nieobecny:\"],\"9v3hwv\":[\"Nie znaleziono serwerów.\"],\"9zb2WA\":[\"Łączenie\"],\"A1taO8\":[\"Szukaj\"],\"A2adVi\":[\"Wysyłaj powiadomienia o pisaniu\"],\"A9Rhec\":[\"Nazwa kanału\"],\"AWOSPo\":[\"Powiększ\"],\"AXSpEQ\":[\"Operator przy połączeniu\"],\"AeXO77\":[\"Konto\"],\"AhNP40\":[\"Przewijaj\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Zmieniono nick\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Anuluj odpowiedź\"],\"ApSx0O\":[\"Znaleziono \",[\"0\"],\" wiadomości pasujących do \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Nie znaleziono wyników\"],\"AyNqAB\":[\"Wyświetl wszystkie zdarzenia serwera w czacie\"],\"B/QqGw\":[\"Z dala od klawiatury\"],\"B8AaMI\":[\"To pole jest wymagane\"],\"BA2c49\":[\"Serwer nie obsługuje zaawansowanego filtrowania LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" i \",[\"3\"],\" innych pisze...\"],\"BGul2A\":[\"Masz niezapisane zmiany. Czy na pewno chcesz zamknąć bez zapisywania?\"],\"BIf9fi\":[\"Twoja wiadomość statusu\"],\"BPm98R\":[\"Nie wybrano serwera. Wybierz najpierw serwer z paska bocznego; linki zapraszające są zarządzane osobno dla każdego serwera.\"],\"BZz3md\":[\"Twoja strona internetowa\"],\"Bgm/H7\":[\"Zezwól na wpisywanie wielu linii tekstu\"],\"BiQIl1\":[\"Przypnij tę prywatną rozmowę\"],\"BlNZZ2\":[\"Kliknij, aby przejść do wiadomości\"],\"Bowq3c\":[\"Tylko operatorzy mogą zmieniać temat kanału\"],\"Btozzp\":[\"Ten obraz wygasł\"],\"Bycfjm\":[\"Łącznie: \",[\"0\"]],\"C6IBQc\":[\"Kopiuj cały JSON\"],\"C9L9wL\":[\"Zbieranie danych\"],\"CDq4wC\":[\"Moderuj użytkownika\"],\"CHVRxG\":[\"Wiadomość do @\",[\"0\"],\" (Shift+Enter – nowa linia)\"],\"CN9zdR\":[\"Nazwa operatora i hasło są wymagane\"],\"CW3sYa\":[\"Dodaj reakcję \",[\"emoji\"]],\"CaAkqd\":[\"Pokaż rozłączenia\"],\"CbvaYj\":[\"Zablokuj po nicku\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Wybierz kanał\"],\"CsekCi\":[\"Normalny\"],\"D+NlUC\":[\"System\"],\"D28t6+\":[\"dołączył i wyszedł\"],\"DB8zMK\":[\"Zastosuj\"],\"DBcWHr\":[\"Niestandardowy plik dźwięku powiadomień\"],\"DTy9Xw\":[\"Podglądy mediów\"],\"Dj4pSr\":[\"Wybierz bezpieczne hasło\"],\"Du+zn+\":[\"Wyszukiwanie...\"],\"Du2T2f\":[\"Nie znaleziono ustawienia\"],\"DwsSVQ\":[\"Zastosuj filtry i odśwież\"],\"E3W/zd\":[\"Domyślny nick\"],\"E6nRW7\":[\"Kopiuj URL\"],\"E703RG\":[\"Tryby:\"],\"EAeu1Z\":[\"Wyślij zaproszenie\"],\"EFKJQT\":[\"Ustawienie\"],\"EGPQBv\":[\"Niestandardowe reguły floodu (+f)\"],\"ELik0r\":[\"Zobacz pełną politykę prywatności\"],\"EPbeC2\":[\"Zobacz lub edytuj temat kanału\"],\"EQCDNT\":[\"Wprowadź nazwę użytkownika opera...\"],\"EUvulZ\":[\"Znaleziono 1 wiadomość pasującą do \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Następny obraz\"],\"EdQY6l\":[\"Brak\"],\"EnqLYU\":[\"Szukaj serwerów...\"],\"F0OKMc\":[\"Edytuj serwer\"],\"F6Int2\":[\"Włącz podświetlenia\"],\"FDoLyE\":[\"Maks. użytkowników\"],\"FUU/hZ\":[\"Kontroluje ilość zewnętrznych mediów ładowanych na czacie.\"],\"Fdp03t\":[\"wł\"],\"FfPWR0\":[\"Okno\"],\"FjkaiT\":[\"Pomniejsz\"],\"FlqOE9\":[\"Co to oznacza:\"],\"FolHNl\":[\"Zarządzaj swoim kontem i uwierzytelnianiem\"],\"Fp2Dif\":[\"Opuścił serwer\"],\"G5KmCc\":[\"GZ-Line (globalna Z-Line)\"],\"GDs0lz\":[\"<0>Ryzyko:0> Poufne informacje (wiadomości, prywatne rozmowy, dane uwierzytelniające) mogą być widoczne dla administratorów sieci lub atakujących znajdujących się między serwerami IRC.\"],\"GR+2I3\":[\"Dodaj maskę zaproszenia (np. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Zamknij wyskakujące powiadomienia serwera\"],\"GdhD7H\":[\"Kliknij ponownie, aby potwierdzić\"],\"GlHnXw\":[\"Zmiana nicku nie powiodła się: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Podgląd:\"],\"GtmO8/\":[\"od\"],\"GtuHUQ\":[\"Zmień nazwę tego kanału na serwerze. Wszyscy użytkownicy zobaczą nową nazwę.\"],\"GuGfFX\":[\"Przełącz wyszukiwanie\"],\"GxkJXS\":[\"Przesyłanie...\"],\"GzbwnK\":[\"Dołączył do kanału\"],\"GzsUDB\":[\"Rozszerzony profil\"],\"H/PnT8\":[\"Wstaw emoji\"],\"H6Izzl\":[\"Twój preferowany kod koloru\"],\"H9jIv+\":[\"Pokaż dołączenia/odejścia\"],\"HAKBY9\":[\"Prześlij pliki\"],\"HdE1If\":[\"Kanał\"],\"Hk4AW9\":[\"Twoja preferowana wyświetlana nazwa\"],\"HmHDk7\":[\"Wybierz członka\"],\"HrQzPU\":[\"Kanały na \",[\"networkName\"]],\"I2tXQ5\":[\"Wiadomość do @\",[\"0\"],\" (Enter – nowa linia, Shift+Enter – wyślij)\"],\"I6bw/h\":[\"Zablokuj użytkownika\"],\"I92Z+b\":[\"Włącz powiadomienia\"],\"I9D72S\":[\"Czy na pewno chcesz usunąć tę wiadomość? Tej operacji nie można cofnąć.\"],\"IA+1wo\":[\"Wyświetlaj gdy użytkownicy są wyrzucani z kanałów\"],\"IDwkJx\":[\"Operator IRC\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Zapisz zmiany\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" i \",[\"2\"],\" piszą...\"],\"IgrLD/\":[\"Pauza\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Odpowiedz\"],\"IoHMnl\":[\"Maksymalna wartość wynosi \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Łączenie...\"],\"J5T9NW\":[\"Informacje o użytkowniku\"],\"J8Y5+z\":[\"Ups! Podział sieci! ⚠️\"],\"JBHkBA\":[\"Opuścił kanał\"],\"JCwL0Q\":[\"Wpisz powód (opcjonalnie)\"],\"JFciKP\":[\"Przełącz\"],\"JXGkhG\":[\"Zmień nazwę kanału (tylko dla operatorów)\"],\"JcD7qf\":[\"Więcej akcji\"],\"JdkA+c\":[\"Tajny (+s)\"],\"Jmu12l\":[\"Kanały serwera\"],\"JvQ++s\":[\"Włącz Markdown\"],\"K2jwh/\":[\"Brak dostępnych danych WHOIS\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Usuń wiadomość\"],\"KKBlUU\":[\"Osadź\"],\"KM0pLb\":[\"Witamy na kanale!\"],\"KR6W2h\":[\"Przestań ignorować użytkownika\"],\"KV+Bi1\":[\"Tylko na zaproszenie (+i)\"],\"KdCtwE\":[\"Ile sekund monitorować aktywność floodowania przed zresetowaniem liczników\"],\"Kkezga\":[\"Hasło serwera\"],\"KsiQ/8\":[\"Użytkownicy muszą być zaproszeni, aby dołączyć do kanału\"],\"L+gB/D\":[\"Informacje o kanale\"],\"LC1a7n\":[\"Serwer IRC zgłosił, że jego połączenia między serwerami mają niski poziom bezpieczeństwa. Oznacza to, że gdy Twoje wiadomości są przekazywane między serwerami IRC w sieci, mogą nie być właściwie szyfrowane lub certyfikaty SSL/TLS mogą nie być poprawnie weryfikowane.\"],\"LNfLR5\":[\"Pokaż wyrzucenia\"],\"LQb0W/\":[\"Pokaż wszystkie zdarzenia\"],\"LU7/yA\":[\"Alternatywna nazwa wyświetlana w interfejsie. Może zawierać spacje, emoji i znaki specjalne. Prawdziwa nazwa kanału (\",[\"channelName\"],\") nadal będzie używana w poleceniach IRC.\"],\"LUb9O7\":[\"Wymagany jest prawidłowy port serwera\"],\"LV4fT6\":[\"Opis (opcjonalnie, np. \\\"Testerzy beta Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Polityka prywatności\"],\"LcuSDR\":[\"Zarządzaj informacjami w profilu i metadanymi\"],\"LqLS9B\":[\"Pokaż zmiany nicku\"],\"LsDQt2\":[\"Ustawienia kanału\"],\"LtI9AS\":[\"Właściciel\"],\"LuNhhL\":[\"zareagował na tę wiadomość\"],\"M/AZNG\":[\"URL do obrazu Twojego awatara\"],\"M/WIer\":[\"Wyślij wiadomość\"],\"M8er/5\":[\"Nazwa:\"],\"MHk+7g\":[\"Poprzedni obraz\"],\"MRorGe\":[\"Wyślij wiadomość prywatną\"],\"MVbSGP\":[\"Okno czasowe (sekundy)\"],\"MkpcsT\":[\"Twoje wiadomości i ustawienia są przechowywane lokalnie na Twoim urządzeniu\"],\"N/hDSy\":[\"Oznacz jako bota – zazwyczaj 'on' lub puste\"],\"N7TQbE\":[\"Zaproś użytkownika do \",[\"channelName\"]],\"NCca/o\":[\"Wprowadź domyślny pseudonim...\"],\"Nqs6B9\":[\"Wyświetla wszystkie zewnętrzne media. Każdy URL może spowodować żądanie do nieznanego serwera.\"],\"Nt+9O7\":[\"Użyj WebSocket zamiast surowego TCP\"],\"NxIHzc\":[\"Rozłącz użytkownika\"],\"O+v/cL\":[\"Przeglądaj wszystkie kanały na serwerze\"],\"ODwSCk\":[\"Wyślij GIF\"],\"OGQ5kK\":[\"Konfiguruj dźwięki powiadomień i podświetlenia\"],\"OIPt1Z\":[\"Pokaż lub ukryj panel listy członków\"],\"OKSNq/\":[\"Bardzo rygorystyczny\"],\"ONWvwQ\":[\"Prześlij\"],\"OVKoQO\":[\"Hasło Twojego konta do uwierzytelniania\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Przenosimy IRC w przyszłość\"],\"OhCpra\":[\"Ustaw temat…\"],\"OkltoQ\":[\"Zablokuj \",[\"username\"],\" po nicku (uniemożliwia ponowne dołączenie z tym samym nickiem)\"],\"P+t/Te\":[\"Brak dodatkowych danych\"],\"P42Wcc\":[\"Bezpieczne\"],\"PD38l0\":[\"Podgląd awatara kanału\"],\"PD9mEt\":[\"Wpisz wiadomość...\"],\"PPqfdA\":[\"Otwórz ustawienia konfiguracji kanału\"],\"PSCjfZ\":[\"Temat, który będzie wyświetlany dla tego kanału. Wszyscy użytkownicy mogą zobaczyć temat.\"],\"PZCecv\":[\"Podgląd PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 raz\"],\"few\":[[\"c\"],\" razy\"],\"many\":[[\"c\"],\" razy\"],\"other\":[[\"c\"],\" razy\"]}]],\"PguS2C\":[\"Dodaj maskę wyjątku (np. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Wyświetlanie \",[\"displayedChannelsCount\"],\" z \",[\"0\"],\" kanałów\"],\"PqhVlJ\":[\"Zablokuj użytkownika (po hostmasce)\"],\"Q+chwU\":[\"Nazwa użytkownika:\"],\"Q2QY4/\":[\"Usuń to zaproszenie\"],\"Q6hhn8\":[\"Preferencje\"],\"QF4a34\":[\"Proszę podać nazwę użytkownika\"],\"QGqSZ2\":[\"Kolor i formatowanie\"],\"QJQd1J\":[\"Edytuj profil\"],\"QSzGDE\":[\"Bezczynny\"],\"QUlny5\":[\"Witamy na \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Czytaj więcej\"],\"QuSkCF\":[\"Filtruj kanały...\"],\"QwUrDZ\":[\"zmienił temat na: \",[\"topic\"]],\"R0UH07\":[\"Obraz \",[\"0\"],\" z \",[\"1\"]],\"R7SsBE\":[\"Wycisz\"],\"R8rf1X\":[\"Kliknij, aby ustawić temat\"],\"RArB3D\":[\"został wyrzucony z \",[\"channelName\"],\" przez \",[\"username\"]],\"RI3cWd\":[\"Odkryj świat IRC z ObsidianIRC\"],\"RIfHS5\":[\"Utwórz nowy link zapraszający\"],\"RMMaN5\":[\"Moderowany (+m)\"],\"RWw9Lg\":[\"Zamknij okno\"],\"RZ2BuZ\":[\"Rejestracja konta \",[\"account\"],\" wymaga weryfikacji: \",[\"message\"]],\"RySp6q\":[\"Ukryj komentarze\"],\"SPKQTd\":[\"Nick jest wymagany\"],\"SPVjfj\":[\"Domyślnie 'brak powodu', jeśli pozostawione puste\"],\"SQKPvQ\":[\"Zaproś użytkownika\"],\"SkZcl+\":[\"Wybierz wstępnie zdefiniowany profil ochrony przed floodem. Profile te oferują zrównoważone ustawienia ochrony dla różnych przypadków użycia.\"],\"Slr+3C\":[\"Min. użytkowników\"],\"Spnlre\":[\"Zaprosiłeś \",[\"target\"],\" do dołączenia do \",[\"channel\"]],\"T/ckN5\":[\"Otwórz w przeglądarce mediów\"],\"T91vKp\":[\"Odtwórz\"],\"TV2Wdu\":[\"Dowiedz się, jak przetwarzamy Twoje dane i chronimy Twoją prywatność.\"],\"TgFpwD\":[\"Stosowanie...\"],\"TkzSFB\":[\"Brak zmian\"],\"TtserG\":[\"Wpisz prawdziwe imię i nazwisko\"],\"Ttz9J1\":[\"Wprowadź hasło...\"],\"Tz0i8g\":[\"Ustawienia\"],\"U3pytU\":[\"Administrator\"],\"UDb2YD\":[\"Zareaguj\"],\"UE4KO5\":[\"*kanał*\"],\"UETAwW\":[\"Nie utworzyłeś jeszcze żadnych linków zapraszających. Użyj formularza powyżej, aby utworzyć swój pierwszy.\"],\"UGT5vp\":[\"Zapisz ustawienia\"],\"UV5hLB\":[\"Nie znaleziono żadnych banów\"],\"Uaj3Nd\":[\"Wiadomości statusu\"],\"Ue3uny\":[\"Domyślne (brak profilu)\"],\"UkARhe\":[\"Normalny – standardowa ochrona\"],\"Umn7Cj\":[\"Brak komentarzy. Bądź pierwszy!\"],\"UtUIRh\":[[\"0\"],\" starszych wiadomości\"],\"UwzP+U\":[\"Bezpieczne połączenie\"],\"V0/A4O\":[\"Właściciel kanału\"],\"V4qgxE\":[\"Utworzone przed (min temu)\"],\"V8yTm6\":[\"Wyczyść wyszukiwanie\"],\"VJMMyz\":[\"ObsidianIRC – IRC w nowoczesnym wydaniu\"],\"VJScHU\":[\"Powód\"],\"VLsmVV\":[\"Wycisz powiadomienia\"],\"VbyRUy\":[\"Komentarze\"],\"Vmx0mQ\":[\"Ustawione przez:\"],\"VqnIZz\":[\"Zobacz naszą politykę prywatności i zasady przetwarzania danych\"],\"VrMygG\":[\"Minimalna długość wynosi \",[\"0\"]],\"VrnTui\":[\"Twoje zaimki, widoczne w profilu\"],\"W8E3qn\":[\"Uwierzytelnione konto\"],\"WAakm9\":[\"Usuń kanał\"],\"WFxTHC\":[\"Dodaj maskę bana (np. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Host serwera jest wymagany\"],\"WRYdXW\":[\"Pozycja audio\"],\"WUOH5B\":[\"Ignoruj użytkownika\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Pokaż 1 więcej element\"],\"few\":[\"Pokaż \",[\"1\"],\" więcej elementów\"],\"many\":[\"Pokaż \",[\"1\"],\" więcej elementów\"],\"other\":[\"Pokaż \",[\"1\"],\" więcej elementów\"]}]],\"WYxRzo\":[\"Twórz i zarządzaj swoimi linkami zapraszającymi\"],\"Wd38W1\":[\"Pozostaw kanał pusty, aby utworzyć ogólne zaproszenie do sieci. Opis służy wyłącznie do Twoich notatek — widoczny jest tylko dla Ciebie na tej liście.\"],\"Weq9zb\":[\"Ogólne\"],\"Wfj7Sk\":[\"Wycisz lub odcisz dźwięki powiadomień\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Profil użytkownika\"],\"X6S3lt\":[\"Szukaj ustawień, kanałów, serwerów...\"],\"XEHan5\":[\"Kontynuuj mimo to\"],\"XI1+wb\":[\"Nieprawidłowy format\"],\"XIXeuC\":[\"Wiadomość do @\",[\"0\"]],\"XMS+k4\":[\"Rozpocznij prywatną rozmowę\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Odepnij prywatną rozmowę\"],\"Xm/s+u\":[\"Wyświetlanie\"],\"Xp2n93\":[\"Wyświetla media z zaufanego hosta plików Twojego serwera. Żadne żądania nie są wysyłane do zewnętrznych serwisów.\"],\"XvjC4F\":[\"Zapisywanie...\"],\"Y/qryO\":[\"Nie znaleziono użytkowników pasujących do wyszukiwania\"],\"YAqRpI\":[\"Rejestracja konta \",[\"account\"],\" powiodła się: \",[\"message\"]],\"YEfzvP\":[\"Chroniony temat (+t)\"],\"YQOn6a\":[\"Zwiń listę członków\"],\"YRCoE9\":[\"Operator kanału\"],\"YURQaF\":[\"Zobacz profil\"],\"YdBSvr\":[\"Kontroluj wyświetlanie mediów i zewnętrznych treści\"],\"Yj6U3V\":[\"Brak centralnego serwera:\"],\"YjvpGx\":[\"Zaimki\"],\"YqH4l4\":[\"Brak klucza\"],\"YyUPpV\":[\"Konto:\"],\"ZJSWfw\":[\"Wiadomość wyświetlana przy rozłączeniu z serwera\"],\"ZR1dJ4\":[\"Zaproszenia\"],\"ZdWg0V\":[\"Otwórz w przeglądarce\"],\"ZhRBbl\":[\"Szukaj wiadomości…\"],\"Zmcu3y\":[\"Zaawansowane filtry\"],\"a2/8e5\":[\"Temat ustawiony po (min temu)\"],\"aHKcKc\":[\"Poprzednia strona\"],\"aJTbXX\":[\"Hasło operatora\"],\"aQryQv\":[\"Wzorzec już istnieje\"],\"aW9pLN\":[\"Maksymalna liczba użytkowników dozwolona na kanale. Pozostaw puste, aby nie było limitu.\"],\"ah4fmZ\":[\"Wyświetla również podglądy z YouTube, Vimeo, SoundCloud i podobnych znanych serwisów.\"],\"aifXak\":[\"Brak mediów na tym kanale\"],\"ap2zBz\":[\"Łagodny\"],\"az8lvo\":[\"Wyłączone\"],\"azXSNo\":[\"Rozwiń listę członków\"],\"azdliB\":[\"Zaloguj się na konto\"],\"b26wlF\":[\"ona/jej\"],\"bD/+Ei\":[\"Rygorystyczny\"],\"bQ6BJn\":[\"Konfiguruj szczegółowe reguły ochrony przed floodem. Każda reguła określa, jaki rodzaj aktywności monitorować i jakie działanie podjąć po przekroczeniu progów.\"],\"beV7+y\":[\"Użytkownik otrzyma zaproszenie do dołączenia do \",[\"channelName\"],\".\"],\"bk84cH\":[\"Wiadomość o nieobecności\"],\"bkHdLj\":[\"Dodaj serwer IRC\"],\"bmQLn5\":[\"Dodaj regułę\"],\"bwRvnp\":[\"Akcja\"],\"c8+EVZ\":[\"Zweryfikowane konto\"],\"cGYUlD\":[\"Nie wczytano żadnych podglądów mediów.\"],\"cLF98o\":[\"Pokaż komentarze (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Brak dostępnych użytkowników\"],\"cSgpoS\":[\"Przypnij prywatną rozmowę\"],\"cde3ce\":[\"Wiadomość do <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Kopiuj sformatowane wyjście\"],\"cl/A5J\":[\"Witamy na \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Usuń\"],\"coPLXT\":[\"Nie przechowujemy Twoich komunikatów IRC na naszych serwerach\"],\"crYH/6\":[\"Odtwarzacz SoundCloud\"],\"d3sis4\":[\"Dodaj serwer\"],\"d9aN5k\":[\"Usuń \",[\"username\"],\" z kanału\"],\"dEgA5A\":[\"Anuluj\"],\"dGi1We\":[\"Odepnij tę prywatną rozmowę\"],\"dJVuyC\":[\"opuścił \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"do\"],\"dXqxlh\":[\"<0>⚠️ Zagrożenie bezpieczeństwa!0> To połączenie może być podatne na przechwycenie lub ataki typu man-in-the-middle.\"],\"da9Q/R\":[\"Zmieniono tryby kanału\"],\"dhJN3N\":[\"Pokaż komentarze\"],\"dj2xTE\":[\"Odrzuć powiadomienie\"],\"dpCzmC\":[\"Ustawienia ochrony przed floodem\"],\"e9dQpT\":[\"Czy chcesz otworzyć ten link w nowej karcie?\"],\"ePK91l\":[\"Edytuj\"],\"eYBDuB\":[\"Prześlij obraz lub podaj URL z opcjonalnym podstawieniem \",[\"size\"],\" dla dynamicznego rozmiaru\"],\"edBbee\":[\"Zablokuj \",[\"username\"],\" po hostmasce (uniemożliwia ponowne dołączenie z tego samego IP/hosta)\"],\"ekfzWq\":[\"Ustawienia użytkownika\"],\"elPDWs\":[\"Dostosuj swoje doświadczenie z klientem IRC\"],\"eu2osY\":[\"<0>💡 Zalecenie:0> Kontynuuj tylko jeśli ufasz temu serwerowi i rozumiesz ryzyko. Unikaj udostępniania poufnych informacji lub haseł przez to połączenie.\"],\"euEhbr\":[\"Kliknij, aby dołączyć do \",[\"channel\"]],\"ez3vLd\":[\"Włącz wieloliniowe wprowadzanie\"],\"f0J5Ki\":[\"Komunikacja między serwerami może używać nieszyfrowanych połączeń\"],\"f9BHJk\":[\"Ostrzeż użytkownika\"],\"fDOLLd\":[\"Nie znaleziono kanałów.\"],\"ffzDkB\":[\"Anonimowa analityka:\"],\"fq1GF9\":[\"Wyświetlaj gdy użytkownicy rozłączają się z serwera\"],\"gEF57C\":[\"Ten serwer obsługuje tylko jeden typ połączenia\"],\"gJuLUI\":[\"Lista ignorowanych\"],\"gNzMrk\":[\"Bieżący awatar\"],\"gjPWyO\":[\"Wprowadź pseudonim...\"],\"gz6UQ3\":[\"Maksymalizuj\"],\"h6razj\":[\"Wyklucz maskę nazwy kanału\"],\"hG6jnw\":[\"Nie ustawiono tematu\"],\"hG89Ed\":[\"Obraz\"],\"hYgDIe\":[\"Utwórz\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"np. 100:1440\"],\"he3ygx\":[\"Kopiuj\"],\"hehnjM\":[\"Ilość\"],\"hzdLuQ\":[\"Tylko użytkownicy z głosem lub wyżej mogą mówić\"],\"i0qMbr\":[\"Strona główna\"],\"iDNBZe\":[\"Powiadomienia\"],\"iH8pgl\":[\"Wróć\"],\"iL9SZg\":[\"Zablokuj użytkownika (po nicku)\"],\"iNt+3c\":[\"Wróć do obrazu\"],\"iQvi+a\":[\"Nie ostrzegaj mnie o niskim poziomie bezpieczeństwa połączeń dla tego serwera\"],\"iSLIjg\":[\"Połącz\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Host serwera\"],\"idD8Ev\":[\"Zapisano\"],\"iivqkW\":[\"Zalogowany od\"],\"ij+Elv\":[\"Podgląd obrazu\"],\"ilIWp7\":[\"Przełącz powiadomienia\"],\"iuaqvB\":[\"Użyj * jako symbolu wieloznacznego. Przykłady: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Zablokuj po hostmasce\"],\"jA4uoI\":[\"Temat:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Powód (opcjonalnie)\"],\"jUV7CU\":[\"Prześlij awatar\"],\"jW5Uwh\":[\"Kontroluj ilość wczytywanego zewnętrznego medium. Wyłączone / Bezpieczne / Zaufane źródła / Wszystkie treści.\"],\"jXzms5\":[\"Opcje załącznika\"],\"jZlrte\":[\"Kolor\"],\"jfC/xh\":[\"Kontakt\"],\"jywMpv\":[\"#nowa-nazwa-kanału\"],\"k112DD\":[\"Wczytaj starsze wiadomości\"],\"k3ID0F\":[\"Filtruj członków…\"],\"k65gsE\":[\"Szczegóły\"],\"k7Zgob\":[\"Anuluj połączenie\"],\"kAVx5h\":[\"Nie znaleziono zaproszeń\"],\"kCLEPU\":[\"Połączony z\"],\"kF5LKb\":[\"Ignorowane wzorce:\"],\"kGeOx/\":[\"Dołącz do \",[\"0\"]],\"kITKr8\":[\"Wczytywanie trybów kanału...\"],\"kPpPsw\":[\"Jesteś operatorem IRC\"],\"kWJmRL\":[\"Ty\"],\"kfcRb0\":[\"Awatar\"],\"kjMqSj\":[\"Kopiuj JSON\"],\"krViRy\":[\"Kliknij, aby skopiować jako JSON\"],\"ks71ra\":[\"Wyjątki\"],\"kw4lRv\":[\"Pół-operator kanału\"],\"kxgIRq\":[\"Wybierz lub dodaj kanał, aby zacząć.\"],\"ky6dWe\":[\"Podgląd awatara\"],\"l+GxCv\":[\"Wczytywanie kanałów...\"],\"l+IUVW\":[\"Weryfikacja konta \",[\"account\"],\" powiodła się: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"ponownie połączył\"],\"few\":[\"ponownie połączył \",[\"reconnectCount\"],\" razy\"],\"many\":[\"ponownie połączył \",[\"reconnectCount\"],\" razy\"],\"other\":[\"ponownie połączył \",[\"reconnectCount\"],\" razy\"]}]],\"l1l8sj\":[[\"0\"],\" dni temu\"],\"l5NhnV\":[\"#kanał (opcjonalnie)\"],\"l5jmzx\":[[\"0\"],\" i \",[\"1\"],\" piszą...\"],\"lCF0wC\":[\"Odśwież\"],\"lHy8N5\":[\"Wczytywanie kolejnych kanałów...\"],\"lasgrr\":[\"użyte\"],\"lbpf14\":[\"Dołącz do \",[\"value\"]],\"lfFsZ4\":[\"Kanały\"],\"lkNdiH\":[\"Nazwa konta\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Prześlij obraz\"],\"loQxaJ\":[\"Wróciłem\"],\"lvfaxv\":[\"STRONA GŁÓWNA\"],\"m16xKo\":[\"Dodaj\"],\"m8flAk\":[\"Podgląd (jeszcze nie przesłany)\"],\"mEPxTp\":[\"<0>⚠️ Uwaga!0> Otwieraj tylko linki z zaufanych źródeł. Złośliwe linki mogą narazić Twoje bezpieczeństwo lub prywatność.\"],\"mHGdhG\":[\"Informacje o serwerze\"],\"mHS8lb\":[\"Wiadomość na #\",[\"0\"]],\"mMYBD9\":[\"Szeroki – szerszy zakres ochrony\"],\"mTGsPd\":[\"Temat kanału\"],\"mU8j6O\":[\"Brak zewnętrznych wiadomości (+n)\"],\"mZp8FL\":[\"Automatyczny powrót do jednej linii\"],\"mdQu8G\":[\"TwójNick\"],\"miSSBQ\":[\"Komentarze (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Użytkownik jest uwierzytelniony\"],\"mwtcGl\":[\"Zamknij komentarze\"],\"mzI/c+\":[\"Pobierz\"],\"n3fGRk\":[\"ustawione przez \",[\"0\"]],\"nE9jsU\":[\"Łagodny – mniej agresywna ochrona\"],\"nNflMD\":[\"Opuść kanał\"],\"nPXkBi\":[\"Wczytywanie danych WHOIS...\"],\"nQnxxF\":[\"Wiadomość na #\",[\"0\"],\" (Shift+Enter – nowa linia)\"],\"nWMRxa\":[\"Odepnij\"],\"nkC032\":[\"Brak profilu floodu\"],\"o69z4d\":[\"Wyślij wiadomość ostrzegawczą do \",[\"username\"]],\"o9ylQi\":[\"Wyszukaj GIFy, aby rozpocząć\"],\"oFGkER\":[\"Powiadomienia serwera\"],\"oOi11l\":[\"Przewiń na dół\"],\"oPYIL5\":[\"sieć\"],\"oQEzQR\":[\"Nowy DM\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Ataki man-in-the-middle na połączenia serwera są możliwe\"],\"oeqmmJ\":[\"Zaufane źródła\"],\"optX0N\":[[\"0\"],\" godz. temu\"],\"ovBPCi\":[\"Domyślne\"],\"p0Z69r\":[\"Wzorzec nie może być pusty\"],\"p1KgtK\":[\"Nie udało się załadować audio\"],\"p59pEv\":[\"Dodatkowe szczegóły\"],\"p7sRI6\":[\"Informuj innych, gdy piszesz\"],\"pBm1od\":[\"Tajny kanał\"],\"pNmiXx\":[\"Twój domyślny nick dla wszystkich serwerów\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Hasło konta\"],\"peNE68\":[\"Stały\"],\"plhHQt\":[\"Brak danych\"],\"pm6+q5\":[\"Ostrzeżenie o bezpieczeństwie\"],\"pn5qSs\":[\"Dodatkowe informacje\"],\"q0cR4S\":[\"jest teraz znany jako **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanał nie będzie widoczny w poleceniach LIST ani NAMES\"],\"qLpTm/\":[\"Usuń reakcję \",[\"emoji\"]],\"qVkGWK\":[\"Przypnij\"],\"qY8wNa\":[\"Strona internetowa\"],\"qb0xJ7\":[\"Użyj symboli wieloznacznych: * pasuje do dowolnej sekwencji, ? pasuje do dowolnego pojedynczego znaku. Przykłady: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Klucz kanału (+k)\"],\"qtoOYG\":[\"Brak limitu\"],\"r1W2AS\":[\"Obraz z serwera plików\"],\"rIPR2O\":[\"Temat ustawiony przed (min temu)\"],\"rMMSYo\":[\"Maksymalna długość wynosi \",[\"0\"]],\"rWtzQe\":[\"Sieć rozdzieliła się i ponownie połączyła. ✅\"],\"rYG2u6\":[\"Proszę czekać...\"],\"rdUucN\":[\"Podgląd\"],\"rjGI/Q\":[\"Prywatność\"],\"rk8iDX\":[\"Wczytywanie GIFów...\"],\"rn6SBY\":[\"Odcisz\"],\"s/UKqq\":[\"Został wyrzucony z kanału\"],\"s8cATI\":[\"dołączył do \",[\"channelName\"]],\"sCO9ue\":[\"Połączenie z <0>\",[\"serverName\"],\"0> ma następujące problemy z bezpieczeństwem:\"],\"sGH11W\":[\"Serwer\"],\"sHI1H+\":[\"jest teraz znany jako **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" zaprosił cię do dołączenia do \",[\"channel\"]],\"sby+1/\":[\"Kliknij, aby skopiować\"],\"sfN25C\":[\"Twoje prawdziwe imię i nazwisko\"],\"sliuzR\":[\"Otwórz link\"],\"sqrO9R\":[\"Niestandardowe wzmianki\"],\"sr6RdJ\":[\"Wieloliniowy przy Shift+Enter\"],\"swrCpB\":[\"Kanał został przemianowany z \",[\"oldName\"],\" na \",[\"newName\"],\" przez \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Zaawansowane\"],\"t/YqKh\":[\"Usuń\"],\"t47eHD\":[\"Twój unikalny identyfikator na tym serwerze\"],\"tAkAh0\":[\"URL z opcjonalnym podstawieniem \",[\"size\"],\" dla dynamicznego rozmiaru. Przykład: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Pokaż lub ukryj panel listy kanałów\"],\"tfDRzk\":[\"Zapisz\"],\"tiBsJk\":[\"opuścił \",[\"channelName\"]],\"tt4/UD\":[\"wyszedł (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Nick {nick} jest już używany, ponawiam z {newNick}\"],\"u0a8B4\":[\"Uwierzytelnij się jako operator IRC, aby uzyskać dostęp administracyjny\"],\"u0rWFU\":[\"Utworzone po (min temu)\"],\"u72w3t\":[\"Użytkownicy i wzorce do ignorowania\"],\"u7jc2L\":[\"wyszedł\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Zapis nieudany: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"Serwery IRC:\"],\"ukyW4o\":[\"Twoje linki zapraszające\"],\"usSSr/\":[\"Poziom powiększenia\"],\"v7uvcf\":[\"Oprogramowanie:\"],\"vE8kb+\":[\"Użyj Shift+Enter dla nowych linii (Enter wysyła)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Brak tematu\"],\"vSJd18\":[\"Wideo\"],\"vXIe7J\":[\"Język\"],\"vaHYxN\":[\"Prawdziwe imię i nazwisko\"],\"vhjbKr\":[\"Nieobecny\"],\"w4NYox\":[\"klient \",[\"title\"]],\"w8xQRx\":[\"Nieprawidłowa wartość\"],\"wFjjxZ\":[\"został wyrzucony z \",[\"channelName\"],\" przez \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nie znaleziono wyjątków od bana\"],\"wPrGnM\":[\"Administrator kanału\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Wyświetlaj gdy użytkownicy dołączają lub opuszczają kanały\"],\"whqZ9r\":[\"Dodatkowe słowa lub frazy do podświetlenia\"],\"wm7RV4\":[\"Dźwięk powiadomienia\"],\"wz/Yoq\":[\"Twoje wiadomości mogą zostać przechwycone podczas przekazywania między serwerami\"],\"x3+y8b\":[\"Tylu osób zarejestrowało się przez ten link\"],\"xCJdfg\":[\"Wyczyść\"],\"xOTzt5\":[\"przed chwilą\"],\"xUHRTR\":[\"Automatycznie uwierzytelniaj jako operator przy połączeniu\"],\"xWHwwQ\":[\"Blokady\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"Ten serwer nie obsługuje linków zapraszających (możliwość<0>obby.world/invitation0>nie jest ogłaszana). Nadal możesz normalnie czatować; ten panel jest przeznaczony dla sieci opartych na obbyircd.\"],\"xceQrO\":[\"Obsługiwane są tylko bezpieczne WebSocket\"],\"xdtXa+\":[\"nazwa-kanału\"],\"xfXC7q\":[\"Kanały tekstowe\"],\"xlCYOE\":[\"Pobieranie kolejnych wiadomości...\"],\"xlhswE\":[\"Minimalna wartość wynosi \",[\"0\"]],\"xq97Ci\":[\"Dodaj słowo lub frazę...\"],\"xuRqRq\":[\"Limit klientów (+l)\"],\"xwF+7J\":[[\"0\"],\" pisze...\"],\"y1eoq1\":[\"Kopiuj link\"],\"yNeucF\":[\"Ten serwer nie obsługuje rozszerzonych metadanych profilu (rozszerzenie IRCv3 METADATA). Dodatkowe pola, takie jak awatar, wyświetlana nazwa i status, nie są dostępne.\"],\"yPlrca\":[\"Awatar kanału\"],\"yQE2r9\":[\"Ładowanie\"],\"ySU+JY\":[\"twoj@email.com\"],\"yTX1Rt\":[\"Nazwa użytkownika operatora\"],\"yYOzWD\":[\"logi\"],\"yfx9Re\":[\"Hasło operatora IRC\"],\"ygCKqB\":[\"Zatrzymaj\"],\"ymDxJx\":[\"Nazwa użytkownika operatora IRC\"],\"yrpRsQ\":[\"Sortuj według nazwy\"],\"yz7wBu\":[\"Zamknij\"],\"zJw+jA\":[\"ustawia tryb: \",[\"0\"]],\"zbymaY\":[[\"0\"],\" min temu\"],\"zebeLu\":[\"Wpisz nazwę użytkownika operatora\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Nieprawidłowy format wzorca. Użyj formatu nick!user@host (dozwolone symbole wieloznaczne *)\"],\"+6NQQA\":[\"Ogólny kanał wsparcia\"],\"+6NyRG\":[\"Klient\"],\"+K0AvT\":[\"Rozłącz\"],\"+cyFdH\":[\"Domyślna wiadomość przy ustawianiu statusu nieobecności\"],\"+mVPqU\":[\"Renderuj formatowanie Markdown w wiadomościach\"],\"+vqCJH\":[\"Nazwa użytkownika Twojego konta do uwierzytelniania\"],\"+yPBXI\":[\"Wybierz plik\"],\"+zy2Nq\":[\"Typ\"],\"/09cao\":[\"Niskie bezpieczeństwo połączenia (poziom \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Użytkownicy spoza kanału nie mogą wysyłać do niego wiadomości\"],\"/4C8U0\":[\"Kopiuj wszystko\"],\"/6BzZF\":[\"Przełącz listę członków\"],\"/AkXyp\":[\"PotwierdziÄ?\"],\"/TNOPk\":[\"Użytkownik jest nieobecny\"],\"/XQgft\":[\"Odkryj\"],\"/cF7Rs\":[\"Głośność\"],\"/dqduX\":[\"Następna strona\"],\"/fc3q4\":[\"Wszystkie treści\"],\"/kISDh\":[\"Włącz dźwięki powiadomień\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Odtwarzaj dźwięki dla wzmianek i wiadomości\"],\"0/0ZGA\":[\"Maska nazwy kanału\"],\"0D6j7U\":[\"Dowiedz się więcej o niestandardowych regułach →\"],\"0XsHcR\":[\"Wyrzuć użytkownika\"],\"0ZpE//\":[\"Sortuj według użytkowników\"],\"0bEPwz\":[\"Ustaw nieobecność\"],\"0dGkPt\":[\"Rozwiń listę kanałów\"],\"0gS7M5\":[\"Wyświetlana nazwa\"],\"0kS+M8\":[\"PrzykładSIEĆ\"],\"0rgoY7\":[\"Łącz się tylko z serwerami, które wybierzesz\"],\"0wdd7X\":[\"Dołącz\"],\"0wkVYx\":[\"Wiadomości prywatne\"],\"111uHX\":[\"Podgląd linku\"],\"196EG4\":[\"Usuń prywatną rozmowę\"],\"1DSr1i\":[\"Zarejestruj konto\"],\"1O/24y\":[\"Przełącz listę kanałów\"],\"1TNIig\":[\"Otwórz\"],\"1VPJJ2\":[\"Ostrzeżenie o zewnętrznym linku\"],\"1ZC/dv\":[\"Brak nieprzeczytanych wzmianek lub wiadomości\"],\"1pO1zi\":[\"Nazwa serwera jest wymagana\"],\"1uwfzQ\":[\"Zobacz temat kanału\"],\"268g7c\":[\"Wpisz wyświetlaną nazwę\"],\"2CEOW6\":[\"Sieć powiązana przez bouncera soju\"],\"2F9+AZ\":[\"Nie przechwycono jeszcze surowego ruchu IRC. Spróbuj siÄ poÅÄ
czyÄ lub wysÅaÄ wiadomoÅÄ.\"],\"2FOFq1\":[\"Operatorzy serwerów w sieci mogą potencjalnie odczytywać Twoje wiadomości\"],\"2FYpfJ\":[\"Więcej\"],\"2HF1Y2\":[[\"inviter\"],\" zaprosił \",[\"target\"],\" do dołączenia do \",[\"channel\"]],\"2I70QL\":[\"Zobacz informacje o profilu użytkownika\"],\"2QYdmE\":[\"Użytkownicy:\"],\"2QpEjG\":[\"wyszedł\"],\"2YE223\":[\"Wiadomość na #\",[\"0\"],\" (Enter – nowa linia, Shift+Enter – wyślij)\"],\"2bimFY\":[\"Użyj hasła serwera\"],\"2iTmdZ\":[\"Pamięć lokalna:\"],\"2odkwe\":[\"Rygorystyczny – bardziej agresywna ochrona\"],\"2uDhbA\":[\"Wpisz nazwę użytkownika do zaproszenia\"],\"2ygf/L\":[\"← Wróć\"],\"2zEgxj\":[\"Szukaj GIFów...\"],\"3RdPhl\":[\"Zmień nazwę kanału\"],\"3THokf\":[\"Użytkownik z głosem\"],\"3TSz9S\":[\"Minimalizuj\"],\"3jBDvM\":[\"Wyświetlana nazwa kanału\"],\"3ryuFU\":[\"Opcjonalne raporty o błędach w celu ulepszenia aplikacji\"],\"3uBF/8\":[\"Zamknij przeglądarkę\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Wprowadź nazwę konta...\"],\"4/Rr0R\":[\"Zaproś użytkownika do bieżącego kanału\"],\"4EZrJN\":[\"Reguły\"],\"4JJtW9\":[\"#przepełnienie\"],\"4NqeT4\":[\"Profil floodu (+F)\"],\"4RZQRK\":[\"Co teraz robisz?\"],\"4hfTrB\":[\"Nick\"],\"4n99LO\":[\"Już w \",[\"0\"]],\"4t6vMV\":[\"Automatycznie przełącz na jedną linię dla krótkich wiadomości\"],\"4vsHmf\":[\"Czas (min)\"],\"4x/Axu\":[\"Twój bouncer nie ma jeszcze żadnych sieci. Dodaj jedną, aby zacząć.\"],\"5+INAX\":[\"Podświetlaj wiadomości, w których jesteś wzmiankowany\"],\"5R5Pv/\":[\"Nazwa operatora\"],\"678PKt\":[\"Nazwa sieci\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Hasło wymagane do dołączenia do kanału. Pozostaw puste, aby usunąć klucz.\"],\"6HhMs3\":[\"Wiadomość pożegnalna\"],\"6V3Ea3\":[\"Skopiowano\"],\"6lGV3K\":[\"Pokaż mniej\"],\"6yFOEi\":[\"Wprowadź hasło opera...\"],\"7+IHTZ\":[\"Nie wybrano pliku\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (np. spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Wyślij wiadomość prywatną\"],\"7U1W7c\":[\"Bardzo łagodny\"],\"7Y1YQj\":[\"Imię i nazwisko:\"],\"7YHArF\":[\"— otwórz w przeglądarce\"],\"7fjnVl\":[\"Szukaj użytkowników...\"],\"7jL88x\":[\"Usunąć tę wiadomość? Tej operacji nie można cofnąć.\"],\"7nGhhM\":[\"Co masz na myśli?\"],\"7sEpu1\":[\"Członkowie — \",[\"0\"]],\"7sNhEz\":[\"Nazwa użytkownika\"],\"8H0Q+x\":[\"Dowiedz się więcej o profilach →\"],\"8Phu0A\":[\"Wyświetlaj gdy użytkownicy zmieniają nick\"],\"8XTG9e\":[\"Wpisz hasło operatora\"],\"8XsV2J\":[\"Spróbuj wysłać ponownie\"],\"8ZsakT\":[\"Hasło\"],\"8kR84m\":[\"Zamierzasz otworzyć zewnętrzny link:\"],\"8lCgih\":[\"Usuń regułę\"],\"8o3dPc\":[\"UpuÅÄ pliki, aby przesÅaÄ\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"dołączył\"],\"few\":[\"dołączył \",[\"joinCount\"],\" razy\"],\"many\":[\"dołączył \",[\"joinCount\"],\" razy\"],\"other\":[\"dołączył \",[\"joinCount\"],\" razy\"]}]],\"9BMLnJ\":[\"Ponownie połącz z serwerem\"],\"9OEgyT\":[\"Dodaj reakcję\"],\"9PQ8m2\":[\"G-Line (globalny ban)\"],\"9Qs99X\":[\"E-mail:\"],\"9QupBP\":[\"Usuń wzorzec\"],\"9W7tl5\":[\"(bez zmian)\"],\"9bG48P\":[\"Wysyłanie\"],\"9f5f0u\":[\"Pytania dotyczące prywatności? Skontaktuj się z nami:\"],\"9iweoP\":[\"Sieci na \",[\"0\"]],\"9unqs3\":[\"Nieobecny:\"],\"9v3hwv\":[\"Nie znaleziono serwerów.\"],\"9zb2WA\":[\"Łączenie\"],\"A1taO8\":[\"Szukaj\"],\"A2adVi\":[\"Wysyłaj powiadomienia o pisaniu\"],\"A9Rhec\":[\"Nazwa kanału\"],\"AWOSPo\":[\"Powiększ\"],\"AXSpEQ\":[\"Operator przy połączeniu\"],\"AdKRCX\":[\"Połączono z <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Konto\"],\"AhNP40\":[\"Przewijaj\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Zmieniono nick\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Anuluj odpowiedź\"],\"ApSx0O\":[\"Znaleziono \",[\"0\"],\" wiadomości pasujących do \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Nie znaleziono wyników\"],\"AyNqAB\":[\"Wyświetl wszystkie zdarzenia serwera w czacie\"],\"B/QqGw\":[\"Z dala od klawiatury\"],\"B0sB2k\":[\"Tekst jawny\"],\"B8AaMI\":[\"To pole jest wymagane\"],\"BA2c49\":[\"Serwer nie obsługuje zaawansowanego filtrowania LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" i \",[\"3\"],\" innych pisze...\"],\"BGul2A\":[\"Masz niezapisane zmiany. Czy na pewno chcesz zamknąć bez zapisywania?\"],\"BIf9fi\":[\"Twoja wiadomość statusu\"],\"BOJWfb\":[\"Rozłączyć z bouncerem soju?\"],\"BPm98R\":[\"Nie wybrano serwera. Wybierz najpierw serwer z paska bocznego; linki zapraszajÄ
ce sÄ
zarzÄ
dzane osobno dla każdego serwera.\"],\"BZz3md\":[\"Twoja strona internetowa\"],\"Bgm/H7\":[\"Zezwól na wpisywanie wielu linii tekstu\"],\"BiQIl1\":[\"Przypnij tę prywatną rozmowę\"],\"BlNZZ2\":[\"Kliknij, aby przejść do wiadomości\"],\"Bowq3c\":[\"Tylko operatorzy mogą zmieniać temat kanału\"],\"Btozzp\":[\"Ten obraz wygasł\"],\"Bycfjm\":[\"Łącznie: \",[\"0\"]],\"C6IBQc\":[\"Kopiuj cały JSON\"],\"C9L9wL\":[\"Zbieranie danych\"],\"CDq4wC\":[\"Moderuj użytkownika\"],\"CHVRxG\":[\"Wiadomość do @\",[\"0\"],\" (Shift+Enter – nowa linia)\"],\"CN9zdR\":[\"Nazwa operatora i hasło są wymagane\"],\"CW3sYa\":[\"Dodaj reakcję \",[\"emoji\"]],\"CaAkqd\":[\"Pokaż rozłączenia\"],\"CbvaYj\":[\"Zablokuj po nicku\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Wybierz kanał\"],\"CsekCi\":[\"Normalny\"],\"D+NlUC\":[\"System\"],\"D28t6+\":[\"dołączył i wyszedł\"],\"DB8zMK\":[\"Zastosuj\"],\"DBcWHr\":[\"Niestandardowy plik dźwięku powiadomień\"],\"DTy9Xw\":[\"Podglądy mediów\"],\"Dj4pSr\":[\"Wybierz bezpieczne hasło\"],\"Du+zn+\":[\"Wyszukiwanie...\"],\"Du2T2f\":[\"Nie znaleziono ustawienia\"],\"DwsSVQ\":[\"Zastosuj filtry i odśwież\"],\"E3W/zd\":[\"Domyślny nick\"],\"E6nRW7\":[\"Kopiuj URL\"],\"E703RG\":[\"Tryby:\"],\"EAeu1Z\":[\"Wyślij zaproszenie\"],\"EFKJQT\":[\"Ustawienie\"],\"EGPQBv\":[\"Niestandardowe reguły floodu (+f)\"],\"ELik0r\":[\"Zobacz pełną politykę prywatności\"],\"EPbeC2\":[\"Zobacz lub edytuj temat kanału\"],\"EQCDNT\":[\"Wprowadź nazwę użytkownika opera...\"],\"EUvulZ\":[\"Znaleziono 1 wiadomość pasującą do \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Następny obraz\"],\"EdQY6l\":[\"Brak\"],\"EnqLYU\":[\"Szukaj serwerów...\"],\"F0OKMc\":[\"Edytuj serwer\"],\"F6Int2\":[\"Włącz podświetlenia\"],\"FDoLyE\":[\"Maks. użytkowników\"],\"FUU/hZ\":[\"Kontroluje ilość zewnętrznych mediów ładowanych na czacie.\"],\"Fdp03t\":[\"wł\"],\"FfPWR0\":[\"Okno\"],\"FjkaiT\":[\"Pomniejsz\"],\"FlqOE9\":[\"Co to oznacza:\"],\"FolHNl\":[\"Zarządzaj swoim kontem i uwierzytelnianiem\"],\"Fp2Dif\":[\"Opuścił serwer\"],\"G5KmCc\":[\"GZ-Line (globalna Z-Line)\"],\"GDs0lz\":[\"<0>Ryzyko:0> Poufne informacje (wiadomości, prywatne rozmowy, dane uwierzytelniające) mogą być widoczne dla administratorów sieci lub atakujących znajdujących się między serwerami IRC.\"],\"GR+2I3\":[\"Dodaj maskę zaproszenia (np. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Zamknij wyskakujące powiadomienia serwera\"],\"GdhD7H\":[\"Kliknij ponownie, aby potwierdziÄ\"],\"GjRZex\":[\"Rozłączyć sieć?\"],\"GlHnXw\":[\"Zmiana nicku nie powiodła się: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Podgląd:\"],\"GtmO8/\":[\"od\"],\"GtuHUQ\":[\"Zmień nazwę tego kanału na serwerze. Wszyscy użytkownicy zobaczą nową nazwę.\"],\"GuGfFX\":[\"Przełącz wyszukiwanie\"],\"GxkJXS\":[\"Przesyłanie...\"],\"GzbwnK\":[\"Dołączył do kanału\"],\"GzsUDB\":[\"Rozszerzony profil\"],\"H/PnT8\":[\"Wstaw emoji\"],\"H6Izzl\":[\"Twój preferowany kod koloru\"],\"H9jIv+\":[\"Pokaż dołączenia/odejścia\"],\"HAKBY9\":[\"Prześlij pliki\"],\"HdE1If\":[\"Kanał\"],\"Hk4AW9\":[\"Twoja preferowana wyświetlana nazwa\"],\"HmHDk7\":[\"Wybierz członka\"],\"HrQzPU\":[\"Kanały na \",[\"networkName\"]],\"I2tXQ5\":[\"Wiadomość do @\",[\"0\"],\" (Enter – nowa linia, Shift+Enter – wyślij)\"],\"I6bw/h\":[\"Zablokuj użytkownika\"],\"I92Z+b\":[\"Włącz powiadomienia\"],\"I9D72S\":[\"Czy na pewno chcesz usunąć tę wiadomość? Tej operacji nie można cofnąć.\"],\"IA+1wo\":[\"Wyświetlaj gdy użytkownicy są wyrzucani z kanałów\"],\"IDwkJx\":[\"Operator IRC\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Zapisz zmiany\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" i \",[\"2\"],\" piszą...\"],\"IgrLD/\":[\"Pauza\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Odpowiedz\"],\"IoHMnl\":[\"Maksymalna wartość wynosi \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Łączenie...\"],\"J5T9NW\":[\"Informacje o użytkowniku\"],\"J8Y5+z\":[\"Ups! Podział sieci! ⚠️\"],\"JBHkBA\":[\"Opuścił kanał\"],\"JCwL0Q\":[\"Wpisz powód (opcjonalnie)\"],\"JFciKP\":[\"Przełącz\"],\"JXGkhG\":[\"Zmień nazwę kanału (tylko dla operatorów)\"],\"JcD7qf\":[\"Więcej akcji\"],\"JdkA+c\":[\"Tajny (+s)\"],\"Jmu12l\":[\"Kanały serwera\"],\"JoQY+E\":[\"To usunie sieć z twojego bouncera soju. Aby użyć jej ponownie, musisz dodać ją z powrotem.\"],\"JvQ++s\":[\"Włącz Markdown\"],\"K2jwh/\":[\"Brak dostępnych danych WHOIS\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Usuń wiadomość\"],\"KKBlUU\":[\"Osadź\"],\"KM0pLb\":[\"Witamy na kanale!\"],\"KR6W2h\":[\"Przestań ignorować użytkownika\"],\"KV+Bi1\":[\"Tylko na zaproszenie (+i)\"],\"KdCtwE\":[\"Ile sekund monitorować aktywność floodowania przed zresetowaniem liczników\"],\"Kkezga\":[\"Hasło serwera\"],\"KsiQ/8\":[\"Użytkownicy muszą być zaproszeni, aby dołączyć do kanału\"],\"L+gB/D\":[\"Informacje o kanale\"],\"LC1a7n\":[\"Serwer IRC zgłosił, że jego połączenia między serwerami mają niski poziom bezpieczeństwa. Oznacza to, że gdy Twoje wiadomości są przekazywane między serwerami IRC w sieci, mogą nie być właściwie szyfrowane lub certyfikaty SSL/TLS mogą nie być poprawnie weryfikowane.\"],\"LEwpeL\":[\"bouncer soju (sterowanie)\"],\"LNfLR5\":[\"Pokaż wyrzucenia\"],\"LP+1Z7\":[\"Dodaj sieć\"],\"LQb0W/\":[\"Pokaż wszystkie zdarzenia\"],\"LU7/yA\":[\"Alternatywna nazwa wyświetlana w interfejsie. Może zawierać spacje, emoji i znaki specjalne. Prawdziwa nazwa kanału (\",[\"channelName\"],\") nadal będzie używana w poleceniach IRC.\"],\"LUb9O7\":[\"Wymagany jest prawidłowy port serwera\"],\"LV4fT6\":[\"Opis (opcjonalnie, np. \\\"Testerzy beta Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Polityka prywatności\"],\"LcuSDR\":[\"Zarządzaj informacjami w profilu i metadanymi\"],\"LqLS9B\":[\"Pokaż zmiany nicku\"],\"LsDQt2\":[\"Ustawienia kanału\"],\"LtI9AS\":[\"Właściciel\"],\"LuNhhL\":[\"zareagował na tę wiadomość\"],\"M/AZNG\":[\"URL do obrazu Twojego awatara\"],\"M/WIer\":[\"Wyślij wiadomość\"],\"M8er/5\":[\"Nazwa:\"],\"MHk+7g\":[\"Poprzedni obraz\"],\"MRorGe\":[\"Wyślij wiadomość prywatną\"],\"MVbSGP\":[\"Okno czasowe (sekundy)\"],\"MkpcsT\":[\"Twoje wiadomości i ustawienia są przechowywane lokalnie na Twoim urządzeniu\"],\"MzPdC2\":[\"Hasło serwera (PASS)\"],\"N/hDSy\":[\"Oznacz jako bota – zazwyczaj 'on' lub puste\"],\"N6j2JH\":[\"Edytuj \",[\"0\"]],\"N7TQbE\":[\"Zaproś użytkownika do \",[\"channelName\"]],\"NCca/o\":[\"Wprowadź domyślny pseudonim...\"],\"Nqs6B9\":[\"Wyświetla wszystkie zewnętrzne media. Każdy URL może spowodować żądanie do nieznanego serwera.\"],\"Nt+9O7\":[\"Użyj WebSocket zamiast surowego TCP\"],\"NxIHzc\":[\"Rozłącz użytkownika\"],\"O+v/cL\":[\"Przeglądaj wszystkie kanały na serwerze\"],\"OCGpR4\":[\"(dziedzicz)\"],\"ODwSCk\":[\"Wyślij GIF\"],\"OGQ5kK\":[\"Konfiguruj dźwięki powiadomień i podświetlenia\"],\"OIPt1Z\":[\"Pokaż lub ukryj panel listy członków\"],\"OKSNq/\":[\"Bardzo rygorystyczny\"],\"ONWvwQ\":[\"Prześlij\"],\"OVKoQO\":[\"Hasło Twojego konta do uwierzytelniania\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Przenosimy IRC w przyszłość\"],\"OhCpra\":[\"Ustaw temat…\"],\"OkltoQ\":[\"Zablokuj \",[\"username\"],\" po nicku (uniemożliwia ponowne dołączenie z tym samym nickiem)\"],\"P+t/Te\":[\"Brak dodatkowych danych\"],\"P42Wcc\":[\"Bezpieczne\"],\"PD38l0\":[\"Podgląd awatara kanału\"],\"PD9mEt\":[\"Wpisz wiadomość...\"],\"PPqfdA\":[\"Otwórz ustawienia konfiguracji kanału\"],\"PSCjfZ\":[\"Temat, który będzie wyświetlany dla tego kanału. Wszyscy użytkownicy mogą zobaczyć temat.\"],\"PZCecv\":[\"Podgląd PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 raz\"],\"few\":[[\"c\"],\" razy\"],\"many\":[[\"c\"],\" razy\"],\"other\":[[\"c\"],\" razy\"]}]],\"PguS2C\":[\"Dodaj maskę wyjątku (np. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Wyświetlanie \",[\"displayedChannelsCount\"],\" z \",[\"0\"],\" kanałów\"],\"PqhVlJ\":[\"Zablokuj użytkownika (po hostmasce)\"],\"Q+chwU\":[\"Nazwa użytkownika:\"],\"Q2QY4/\":[\"UsuÅ to zaproszenie\"],\"Q3v9Wc\":[\"Tak, usuń\"],\"Q6hhn8\":[\"Preferencje\"],\"QF4a34\":[\"Proszę podać nazwę użytkownika\"],\"QGqSZ2\":[\"Kolor i formatowanie\"],\"QJQd1J\":[\"Edytuj profil\"],\"QSzGDE\":[\"Bezczynny\"],\"QUlny5\":[\"Witamy na \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Czytaj więcej\"],\"QuSkCF\":[\"Filtruj kanały...\"],\"QwUrDZ\":[\"zmienił temat na: \",[\"topic\"]],\"R0UH07\":[\"Obraz \",[\"0\"],\" z \",[\"1\"]],\"R7SsBE\":[\"Wycisz\"],\"R8rf1X\":[\"Kliknij, aby ustawić temat\"],\"RArB3D\":[\"został wyrzucony z \",[\"channelName\"],\" przez \",[\"username\"]],\"RI3cWd\":[\"Odkryj świat IRC z ObsidianIRC\"],\"RIfHS5\":[\"Utwórz nowy link zapraszajÄ
cy\"],\"RMMaN5\":[\"Moderowany (+m)\"],\"RWw9Lg\":[\"Zamknij okno\"],\"RZ2BuZ\":[\"Rejestracja konta \",[\"account\"],\" wymaga weryfikacji: \",[\"message\"]],\"RySp6q\":[\"Ukryj komentarze\"],\"S5Togi\":[\"Wczytywanie sieci z Twojego bouncera…\"],\"SPKQTd\":[\"Nick jest wymagany\"],\"SPVjfj\":[\"Domyślnie 'brak powodu', jeśli pozostawione puste\"],\"SQKPvQ\":[\"Zaproś użytkownika\"],\"STmlpb\":[\"Powrót do listy sieci\"],\"SkZcl+\":[\"Wybierz wstępnie zdefiniowany profil ochrony przed floodem. Profile te oferują zrównoważone ustawienia ochrony dla różnych przypadków użycia.\"],\"Slr+3C\":[\"Min. użytkowników\"],\"Spnlre\":[\"Zaprosiłeś \",[\"target\"],\" do dołączenia do \",[\"channel\"]],\"T/ckN5\":[\"Otwórz w przeglądarce mediów\"],\"T91vKp\":[\"Odtwórz\"],\"TV2Wdu\":[\"Dowiedz się, jak przetwarzamy Twoje dane i chronimy Twoją prywatność.\"],\"TgFpwD\":[\"Stosowanie...\"],\"TkzSFB\":[\"Brak zmian\"],\"TtserG\":[\"Wpisz prawdziwe imię i nazwisko\"],\"Ttz9J1\":[\"Wprowadź hasło...\"],\"Tz0i8g\":[\"Ustawienia\"],\"U3pytU\":[\"Administrator\"],\"UDb2YD\":[\"Zareaguj\"],\"UE4KO5\":[\"*kanał*\"],\"UETAwW\":[\"Nie utworzyÅeÅ jeszcze żadnych linków zapraszajÄ
cych. Użyj formularza powyżej, aby utworzyÄ swój pierwszy.\"],\"UGT5vp\":[\"Zapisz ustawienia\"],\"UV5hLB\":[\"Nie znaleziono żadnych banów\"],\"Uaj3Nd\":[\"Wiadomości statusu\"],\"Ue3uny\":[\"Domyślne (brak profilu)\"],\"UkARhe\":[\"Normalny – standardowa ochrona\"],\"Umn7Cj\":[\"Brak komentarzy. Bądź pierwszy!\"],\"UtUIRh\":[[\"0\"],\" starszych wiadomości\"],\"UwzP+U\":[\"Bezpieczne połączenie\"],\"V0/A4O\":[\"Właściciel kanału\"],\"V0zZWc\":[\"Spowoduje to także zamknięcie powiązanej sieci poniżej.\"],\"V4qgxE\":[\"Utworzone przed (min temu)\"],\"V8yTm6\":[\"Wyczyść wyszukiwanie\"],\"VJMMyz\":[\"ObsidianIRC – IRC w nowoczesnym wydaniu\"],\"VJScHU\":[\"Powód\"],\"VLsmVV\":[\"Wycisz powiadomienia\"],\"VbyRUy\":[\"Komentarze\"],\"Vmx0mQ\":[\"Ustawione przez:\"],\"VqnIZz\":[\"Zobacz naszą politykę prywatności i zasady przetwarzania danych\"],\"VrMygG\":[\"Minimalna długość wynosi \",[\"0\"]],\"VrnTui\":[\"Twoje zaimki, widoczne w profilu\"],\"W8E3qn\":[\"Uwierzytelnione konto\"],\"WAakm9\":[\"Usuń kanał\"],\"WFxTHC\":[\"Dodaj maskę bana (np. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Host serwera jest wymagany\"],\"WRYdXW\":[\"Pozycja audio\"],\"WUOH5B\":[\"Ignoruj użytkownika\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Pokaż 1 więcej element\"],\"few\":[\"Pokaż \",[\"1\"],\" więcej elementów\"],\"many\":[\"Pokaż \",[\"1\"],\" więcej elementów\"],\"other\":[\"Pokaż \",[\"1\"],\" więcej elementów\"]}]],\"WYxRzo\":[\"Twórz i zarzÄ
dzaj swoimi linkami zapraszajÄ
cymi\"],\"Wd38W1\":[\"Pozostaw kanaÅ pusty, aby utworzyÄ ogólne zaproszenie do sieci. Opis sÅuży wyÅÄ
cznie do Twoich notatek â widoczny jest tylko dla Ciebie na tej liÅcie.\"],\"Weq9zb\":[\"Ogólne\"],\"Wfj7Sk\":[\"Wycisz lub odcisz dźwięki powiadomień\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Profil użytkownika\"],\"X6S3lt\":[\"Szukaj ustawień, kanałów, serwerów...\"],\"XEHan5\":[\"Kontynuuj mimo to\"],\"XI1+wb\":[\"Nieprawidłowy format\"],\"XIXeuC\":[\"Wiadomość do @\",[\"0\"]],\"XMS+k4\":[\"Rozpocznij prywatną rozmowę\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Odepnij prywatną rozmowę\"],\"Xm/s+u\":[\"Wyświetlanie\"],\"Xp2n93\":[\"Wyświetla media z zaufanego hosta plików Twojego serwera. Żadne żądania nie są wysyłane do zewnętrznych serwisów.\"],\"XvjC4F\":[\"Zapisywanie...\"],\"Y/qryO\":[\"Nie znaleziono użytkowników pasujących do wyszukiwania\"],\"YAqRpI\":[\"Rejestracja konta \",[\"account\"],\" powiodła się: \",[\"message\"]],\"YEfzvP\":[\"Chroniony temat (+t)\"],\"YQOn6a\":[\"Zwiń listę członków\"],\"YRCoE9\":[\"Operator kanału\"],\"YURQaF\":[\"Zobacz profil\"],\"YdBSvr\":[\"Kontroluj wyświetlanie mediów i zewnętrznych treści\"],\"Yj6U3V\":[\"Brak centralnego serwera:\"],\"YjvpGx\":[\"Zaimki\"],\"YqH4l4\":[\"Brak klucza\"],\"YyUPpV\":[\"Konto:\"],\"ZJSWfw\":[\"Wiadomość wyświetlana przy rozłączeniu z serwera\"],\"ZR1dJ4\":[\"Zaproszenia\"],\"ZdWg0V\":[\"Otwórz w przeglądarce\"],\"ZhRBbl\":[\"Szukaj wiadomości…\"],\"Zmcu3y\":[\"Zaawansowane filtry\"],\"a0bHay\":[\"Rozłączyć <0>\",[\"0\"],\"0>?\"],\"a2/8e5\":[\"Temat ustawiony po (min temu)\"],\"aHKcKc\":[\"Poprzednia strona\"],\"aJTbXX\":[\"Hasło operatora\"],\"aQryQv\":[\"Wzorzec już istnieje\"],\"aW9pLN\":[\"Maksymalna liczba użytkowników dozwolona na kanale. Pozostaw puste, aby nie było limitu.\"],\"ah4fmZ\":[\"Wyświetla również podglądy z YouTube, Vimeo, SoundCloud i podobnych znanych serwisów.\"],\"aifXak\":[\"Brak mediów na tym kanale\"],\"ap2zBz\":[\"Łagodny\"],\"az8lvo\":[\"Wyłączone\"],\"azXSNo\":[\"Rozwiń listę członków\"],\"azdliB\":[\"Zaloguj się na konto\"],\"b26wlF\":[\"ona/jej\"],\"bD/+Ei\":[\"Rygorystyczny\"],\"bQ6BJn\":[\"Konfiguruj szczegółowe reguły ochrony przed floodem. Każda reguła określa, jaki rodzaj aktywności monitorować i jakie działanie podjąć po przekroczeniu progów.\"],\"beV7+y\":[\"Użytkownik otrzyma zaproszenie do dołączenia do \",[\"channelName\"],\".\"],\"bk84cH\":[\"Wiadomość o nieobecności\"],\"bkHdLj\":[\"Dodaj serwer IRC\"],\"bmQLn5\":[\"Dodaj regułę\"],\"bv4cFj\":[\"Transport\"],\"bwRvnp\":[\"Akcja\"],\"c8+EVZ\":[\"Zweryfikowane konto\"],\"cGYUlD\":[\"Nie wczytano żadnych podglądów mediów.\"],\"cLF98o\":[\"Pokaż komentarze (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Brak dostępnych użytkowników\"],\"cSgpoS\":[\"Przypnij prywatną rozmowę\"],\"cXeEKu\":[\"Spowoduje to także zamknięcie \",[\"0\"],\" powiązanych sieci poniżej.\"],\"cde3ce\":[\"Wiadomość do <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Kopiuj sformatowane wyjście\"],\"cl/A5J\":[\"Witamy na \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Usuń\"],\"coPLXT\":[\"Nie przechowujemy Twoich komunikatów IRC na naszych serwerach\"],\"crYH/6\":[\"Odtwarzacz SoundCloud\"],\"cv5DQb\":[\"nie ustawiono hosta\"],\"d3sis4\":[\"Dodaj serwer\"],\"d9aN5k\":[\"Usuń \",[\"username\"],\" z kanału\"],\"dEgA5A\":[\"Anuluj\"],\"dGi1We\":[\"Odepnij tę prywatną rozmowę\"],\"dJVuyC\":[\"opuścił \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"do\"],\"dXqxlh\":[\"<0>⚠️ Zagrożenie bezpieczeństwa!0> To połączenie może być podatne na przechwycenie lub ataki typu man-in-the-middle.\"],\"da9Q/R\":[\"Zmieniono tryby kanału\"],\"dhJN3N\":[\"Pokaż komentarze\"],\"dj2xTE\":[\"Odrzuć powiadomienie\"],\"dpCzmC\":[\"Ustawienia ochrony przed floodem\"],\"e9dQpT\":[\"Czy chcesz otworzyć ten link w nowej karcie?\"],\"ePK91l\":[\"Edytuj\"],\"eYBDuB\":[\"Prześlij obraz lub podaj URL z opcjonalnym podstawieniem \",[\"size\"],\" dla dynamicznego rozmiaru\"],\"edBbee\":[\"Zablokuj \",[\"username\"],\" po hostmasce (uniemożliwia ponowne dołączenie z tego samego IP/hosta)\"],\"ekfzWq\":[\"Ustawienia użytkownika\"],\"elPDWs\":[\"Dostosuj swoje doświadczenie z klientem IRC\"],\"eu2osY\":[\"<0>💡 Zalecenie:0> Kontynuuj tylko jeśli ufasz temu serwerowi i rozumiesz ryzyko. Unikaj udostępniania poufnych informacji lub haseł przez to połączenie.\"],\"euEhbr\":[\"Kliknij, aby dołączyć do \",[\"channel\"]],\"ez3vLd\":[\"Włącz wieloliniowe wprowadzanie\"],\"f0J5Ki\":[\"Komunikacja między serwerami może używać nieszyfrowanych połączeń\"],\"f9BHJk\":[\"Ostrzeż użytkownika\"],\"fDOLLd\":[\"Nie znaleziono kanałów.\"],\"ffzDkB\":[\"Anonimowa analityka:\"],\"fq1GF9\":[\"Wyświetlaj gdy użytkownicy rozłączają się z serwera\"],\"gCldcN\":[\"Zmień kolor akcentu\"],\"gEF57C\":[\"Ten serwer obsługuje tylko jeden typ połączenia\"],\"gJuLUI\":[\"Lista ignorowanych\"],\"gNzMrk\":[\"Bieżący awatar\"],\"gjPWyO\":[\"Wprowadź pseudonim...\"],\"gz6UQ3\":[\"Maksymalizuj\"],\"h6/IMX\":[\"Dodaj swoją pierwszą sieć\"],\"h6razj\":[\"Wyklucz maskę nazwy kanału\"],\"hG6jnw\":[\"Nie ustawiono tematu\"],\"hG89Ed\":[\"Obraz\"],\"hYgDIe\":[\"Utwórz\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"np. 100:1440\"],\"he3ygx\":[\"Kopiuj\"],\"hehnjM\":[\"Ilość\"],\"hzdLuQ\":[\"Tylko użytkownicy z głosem lub wyżej mogą mówić\"],\"i0qMbr\":[\"Strona główna\"],\"iDNBZe\":[\"Powiadomienia\"],\"iH8pgl\":[\"Wróć\"],\"iL9SZg\":[\"Zablokuj użytkownika (po nicku)\"],\"iNt+3c\":[\"Wróć do obrazu\"],\"iQvi+a\":[\"Nie ostrzegaj mnie o niskim poziomie bezpieczeństwa połączeń dla tego serwera\"],\"iSLIjg\":[\"Połącz\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Host serwera\"],\"idD8Ev\":[\"Zapisano\"],\"iivqkW\":[\"Zalogowany od\"],\"ij+Elv\":[\"Podgląd obrazu\"],\"ilIWp7\":[\"Przełącz powiadomienia\"],\"iuaqvB\":[\"Użyj * jako symbolu wieloznacznego. Przykłady: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Zablokuj po hostmasce\"],\"jA4uoI\":[\"Temat:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Powód (opcjonalnie)\"],\"jUV7CU\":[\"Prześlij awatar\"],\"jW5Uwh\":[\"Kontroluj ilość wczytywanego zewnętrznego medium. Wyłączone / Bezpieczne / Zaufane źródła / Wszystkie treści.\"],\"jXzms5\":[\"Opcje załącznika\"],\"jZlrte\":[\"Kolor\"],\"jfC/xh\":[\"Kontakt\"],\"jywMpv\":[\"#nowa-nazwa-kanału\"],\"k112DD\":[\"Wczytaj starsze wiadomości\"],\"k3ID0F\":[\"Filtruj członków…\"],\"k65gsE\":[\"Szczegóły\"],\"k7Zgob\":[\"Anuluj połączenie\"],\"kAVx5h\":[\"Nie znaleziono zaproszeń\"],\"kCLEPU\":[\"Połączony z\"],\"kF5LKb\":[\"Ignorowane wzorce:\"],\"kGeOx/\":[\"Dołącz do \",[\"0\"]],\"kITKr8\":[\"Wczytywanie trybów kanału...\"],\"kPpPsw\":[\"Jesteś operatorem IRC\"],\"kWJmRL\":[\"Ty\"],\"kfcRb0\":[\"Awatar\"],\"kjMqSj\":[\"Kopiuj JSON\"],\"krViRy\":[\"Kliknij, aby skopiować jako JSON\"],\"ks71ra\":[\"Wyjątki\"],\"kw4lRv\":[\"Pół-operator kanału\"],\"kxgIRq\":[\"Wybierz lub dodaj kanał, aby zacząć.\"],\"ky6dWe\":[\"Podgląd awatara\"],\"l+GxCv\":[\"Wczytywanie kanałów...\"],\"l+IUVW\":[\"Weryfikacja konta \",[\"account\"],\" powiodła się: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"ponownie połączył\"],\"few\":[\"ponownie połączył \",[\"reconnectCount\"],\" razy\"],\"many\":[\"ponownie połączył \",[\"reconnectCount\"],\" razy\"],\"other\":[\"ponownie połączył \",[\"reconnectCount\"],\" razy\"]}]],\"l1l8sj\":[[\"0\"],\" dni temu\"],\"l5NhnV\":[\"#kanaÅ (opcjonalnie)\"],\"l5jmzx\":[[\"0\"],\" i \",[\"1\"],\" piszą...\"],\"lCF0wC\":[\"OdÅwież\"],\"lHy8N5\":[\"Wczytywanie kolejnych kanałów...\"],\"lasgrr\":[\"użyte\"],\"lbpf14\":[\"Dołącz do \",[\"value\"]],\"lfFsZ4\":[\"Kanały\"],\"lkNdiH\":[\"Nazwa konta\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Prześlij obraz\"],\"loQxaJ\":[\"Wróciłem\"],\"lvfaxv\":[\"STRONA GŁÓWNA\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Dodaj\"],\"m8flAk\":[\"Podgląd (jeszcze nie przesłany)\"],\"mEPxTp\":[\"<0>⚠️ Uwaga!0> Otwieraj tylko linki z zaufanych źródeł. Złośliwe linki mogą narazić Twoje bezpieczeństwo lub prywatność.\"],\"mHGdhG\":[\"Informacje o serwerze\"],\"mHS8lb\":[\"Wiadomość na #\",[\"0\"]],\"mMYBD9\":[\"Szeroki – szerszy zakres ochrony\"],\"mTGsPd\":[\"Temat kanału\"],\"mU8j6O\":[\"Brak zewnętrznych wiadomości (+n)\"],\"mZp8FL\":[\"Automatyczny powrót do jednej linii\"],\"mdQu8G\":[\"TwójNick\"],\"miSSBQ\":[\"Komentarze (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Użytkownik jest uwierzytelniony\"],\"mwtcGl\":[\"Zamknij komentarze\"],\"myL0MR\":[\"Usunąć tę sieć?\"],\"mzI/c+\":[\"Pobierz\"],\"n3fGRk\":[\"ustawione przez \",[\"0\"]],\"n5+j9l\":[\"np. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Łagodny – mniej agresywna ochrona\"],\"nNflMD\":[\"Opuść kanał\"],\"nPXkBi\":[\"Wczytywanie danych WHOIS...\"],\"nQnxxF\":[\"Wiadomość na #\",[\"0\"],\" (Shift+Enter – nowa linia)\"],\"nWMRxa\":[\"Odepnij\"],\"nkC032\":[\"Brak profilu floodu\"],\"o69z4d\":[\"Wyślij wiadomość ostrzegawczą do \",[\"username\"]],\"o9ylQi\":[\"Wyszukaj GIFy, aby rozpocząć\"],\"oFGkER\":[\"Powiadomienia serwera\"],\"oOi11l\":[\"Przewiń na dół\"],\"oPYIL5\":[\"sieÄ\"],\"oQEzQR\":[\"Nowy DM\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Ataki man-in-the-middle na połączenia serwera są możliwe\"],\"oeqmmJ\":[\"Zaufane źródła\"],\"optX0N\":[[\"0\"],\" godz. temu\"],\"ovBPCi\":[\"Domyślne\"],\"p0Z69r\":[\"Wzorzec nie może być pusty\"],\"p1KgtK\":[\"Nie udało się załadować audio\"],\"p59pEv\":[\"Dodatkowe szczegóły\"],\"p7sRI6\":[\"Informuj innych, gdy piszesz\"],\"pBm1od\":[\"Tajny kanał\"],\"pNmiXx\":[\"Twój domyślny nick dla wszystkich serwerów\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Hasło konta\"],\"peNE68\":[\"Stały\"],\"plhHQt\":[\"Brak danych\"],\"pm6+q5\":[\"Ostrzeżenie o bezpieczeństwie\"],\"pn5qSs\":[\"Dodatkowe informacje\"],\"q0cR4S\":[\"jest teraz znany jako **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanał nie będzie widoczny w poleceniach LIST ani NAMES\"],\"qLpTm/\":[\"Usuń reakcję \",[\"emoji\"]],\"qVkGWK\":[\"Przypnij\"],\"qY8wNa\":[\"Strona internetowa\"],\"qb0xJ7\":[\"Użyj symboli wieloznacznych: * pasuje do dowolnej sekwencji, ? pasuje do dowolnego pojedynczego znaku. Przykłady: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Klucz kanału (+k)\"],\"qtoOYG\":[\"Brak limitu\"],\"r1W2AS\":[\"Obraz z serwera plików\"],\"rIPR2O\":[\"Temat ustawiony przed (min temu)\"],\"rMMSYo\":[\"Maksymalna długość wynosi \",[\"0\"]],\"rWtzQe\":[\"Sieć rozdzieliła się i ponownie połączyła. ✅\"],\"rYG2u6\":[\"Proszę czekać...\"],\"rdUucN\":[\"Podgląd\"],\"rjGI/Q\":[\"Prywatność\"],\"rk8iDX\":[\"Wczytywanie GIFów...\"],\"rn6SBY\":[\"Odcisz\"],\"s/UKqq\":[\"Został wyrzucony z kanału\"],\"s7oqXR\":[\"Wybierz sieć\"],\"s8cATI\":[\"dołączył do \",[\"channelName\"]],\"sCO9ue\":[\"Połączenie z <0>\",[\"serverName\"],\"0> ma następujące problemy z bezpieczeństwem:\"],\"sGH11W\":[\"Serwer\"],\"sHI1H+\":[\"jest teraz znany jako **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" zaprosił cię do dołączenia do \",[\"channel\"]],\"sUBSbK\":[\"Brak sieci nadrzędnych.\"],\"sby+1/\":[\"Kliknij, aby skopiować\"],\"sfN25C\":[\"Twoje prawdziwe imię i nazwisko\"],\"sliuzR\":[\"Otwórz link\"],\"sqrO9R\":[\"Niestandardowe wzmianki\"],\"sr6RdJ\":[\"Wieloliniowy przy Shift+Enter\"],\"swrCpB\":[\"Kanał został przemianowany z \",[\"oldName\"],\" na \",[\"newName\"],\" przez \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Zaawansowane\"],\"t/YqKh\":[\"Usuń\"],\"t47eHD\":[\"Twój unikalny identyfikator na tym serwerze\"],\"tAkAh0\":[\"URL z opcjonalnym podstawieniem \",[\"size\"],\" dla dynamicznego rozmiaru. Przykład: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Pokaż lub ukryj panel listy kanałów\"],\"tfDRzk\":[\"Zapisz\"],\"tiBsJk\":[\"opuścił \",[\"channelName\"]],\"tt4/UD\":[\"wyszedł (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Nick {nick} jest już używany, ponawiam z {newNick}\"],\"u0a8B4\":[\"Uwierzytelnij się jako operator IRC, aby uzyskać dostęp administracyjny\"],\"u0rWFU\":[\"Utworzone po (min temu)\"],\"u72w3t\":[\"Użytkownicy i wzorce do ignorowania\"],\"u7jc2L\":[\"wyszedł\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Zapis nieudany: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"Serwery IRC:\"],\"ukyW4o\":[\"Twoje linki zapraszajÄ
ce\"],\"usSSr/\":[\"Poziom powiększenia\"],\"v7uvcf\":[\"Oprogramowanie:\"],\"vE8kb+\":[\"Użyj Shift+Enter dla nowych linii (Enter wysyła)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Brak tematu\"],\"vSJd18\":[\"Wideo\"],\"vXIe7J\":[\"Język\"],\"vaHYxN\":[\"Prawdziwe imię i nazwisko\"],\"vhjbKr\":[\"Nieobecny\"],\"w/nogd\":[[\"0\"],\" sieć\",[\"1\"],\" — wybierz jedną, aby dołączyć\"],\"w4NYox\":[\"klient \",[\"title\"]],\"w8xQRx\":[\"Nieprawidłowa wartość\"],\"wFjjxZ\":[\"został wyrzucony z \",[\"channelName\"],\" przez \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nie znaleziono wyjątków od bana\"],\"wPrGnM\":[\"Administrator kanału\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Wyświetlaj gdy użytkownicy dołączają lub opuszczają kanały\"],\"whqZ9r\":[\"Dodatkowe słowa lub frazy do podświetlenia\"],\"wm7RV4\":[\"Dźwięk powiadomienia\"],\"wz/Yoq\":[\"Twoje wiadomości mogą zostać przechwycone podczas przekazywania między serwerami\"],\"x3+y8b\":[\"Tylu osób zarejestrowaÅo siÄ przez ten link\"],\"xCJdfg\":[\"Wyczyść\"],\"xOTzt5\":[\"przed chwilÄ
\"],\"xUHRTR\":[\"Automatycznie uwierzytelniaj jako operator przy połączeniu\"],\"xWHwwQ\":[\"Blokady\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"Ten serwer nie obsÅuguje linków zapraszajÄ
cych (możliwoÅÄ<0>obby.world/invitation0>nie jest ogÅaszana). Nadal możesz normalnie czatowaÄ; ten panel jest przeznaczony dla sieci opartych na obbyircd.\"],\"xceQrO\":[\"Obsługiwane są tylko bezpieczne WebSocket\"],\"xdtXa+\":[\"nazwa-kanału\"],\"xfXC7q\":[\"Kanały tekstowe\"],\"xlCYOE\":[\"Pobieranie kolejnych wiadomości...\"],\"xlhswE\":[\"Minimalna wartość wynosi \",[\"0\"]],\"xq97Ci\":[\"Dodaj słowo lub frazę...\"],\"xuRqRq\":[\"Limit klientów (+l)\"],\"xwF+7J\":[[\"0\"],\" pisze...\"],\"y1eoq1\":[\"Kopiuj link\"],\"yJztBY\":[\"Usuń sieć\"],\"yNeucF\":[\"Ten serwer nie obsługuje rozszerzonych metadanych profilu (rozszerzenie IRCv3 METADATA). Dodatkowe pola, takie jak awatar, wyświetlana nazwa i status, nie są dostępne.\"],\"yPlrca\":[\"Awatar kanału\"],\"yQE2r9\":[\"Ładowanie\"],\"ySU+JY\":[\"twoj@email.com\"],\"yTX1Rt\":[\"Nazwa użytkownika operatora\"],\"yYOzWD\":[\"logi\"],\"yfx9Re\":[\"Hasło operatora IRC\"],\"ygCKqB\":[\"Zatrzymaj\"],\"ymDxJx\":[\"Nazwa użytkownika operatora IRC\"],\"yrpRsQ\":[\"Sortuj według nazwy\"],\"yz7wBu\":[\"Zamknij\"],\"zJw+jA\":[\"ustawia tryb: \",[\"0\"]],\"zbymaY\":[[\"0\"],\" min temu\"],\"zebeLu\":[\"Wpisz nazwę użytkownika operatora\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/pl/messages.po b/src/locales/pl/messages.po
index 27544c31..13e7ce31 100644
--- a/src/locales/pl/messages.po
+++ b/src/locales/pl/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - Przenosimy IRC w przyszłość"
msgid "— open in viewer"
msgstr "— otwórz w przeglądarce"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(dziedzicz)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(bez zmian)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} i {1} piszą..."
msgid "{0} is typing..."
msgstr "{0} pisze..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} sieć{1} — wybierz jedną, aby dołączyć"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -115,7 +131,7 @@ msgstr "*spam*"
#: src/components/ui/InvitationsPanel.tsx
msgid "#channel (optional)"
-msgstr "#kanał (opcjonalnie)"
+msgstr "#kanaÅ (opcjonalnie)"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "#new-channel-name"
@@ -205,6 +221,12 @@ msgstr "Dodaj maskę zaproszenia (np. nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "Dodaj serwer IRC"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Dodaj sieć"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Dodaj regułę"
msgid "Add Server"
msgstr "Dodaj serwer"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Dodaj swoją pierwszą sieć"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Dodatkowe szczegóły"
@@ -377,6 +403,10 @@ msgstr "Wróć"
msgid "Back to image"
msgstr "Wróć do obrazu"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Powrót do listy sieci"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "Zablokuj {username} po hostmasce (uniemożliwia ponowne dołączenie z tego samego IP/hosta)"
@@ -424,6 +454,10 @@ msgstr "Przeglądaj wszystkie kanały na serwerze"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Anuluj połączenie"
msgid "Cancel reply"
msgstr "Anuluj odpowiedź"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Zmień kolor akcentu"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Zmień nazwę kanału (tylko dla operatorów)"
@@ -564,7 +603,7 @@ msgstr "Wyczyść wyszukiwanie"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "Kliknij ponownie, aby potwierdzić"
+msgstr "Kliknij ponownie, aby potwierdziÄ"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "Limit klientów (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -666,9 +707,10 @@ msgstr "Konfiguruj dźwięki powiadomień i podświetlenia"
#: src/components/ui/InvitationsPanel.tsx
msgid "Confirm?"
-msgstr "Potwierdzić?"
+msgstr "PotwierdziÄ?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Połącz"
@@ -739,15 +781,15 @@ msgstr "Kopiuj URL"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create"
-msgstr "Utwórz"
+msgstr "Utwórz"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "Utwórz nowy link zapraszający"
+msgstr "Utwórz nowy link zapraszajÄ
cy"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "Twórz i zarządzaj swoimi linkami zapraszającymi"
+msgstr "Twórz i zarzÄ
dzaj swoimi linkami zapraszajÄ
cymi"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,27 +856,52 @@ msgstr "Usuń kanał"
msgid "Delete message"
msgstr "Usuń wiadomość"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Usuń sieć"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Usuń prywatną rozmowę"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "Usuń to zaproszenie"
+msgstr "UsuÅ to zaproszenie"
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "Usunąć tę wiadomość? Tej operacji nie można cofnąć."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Usunąć tę sieć?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
msgstr "Opis (opcjonalnie, np. \"Testerzy beta Q3\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Rozłącz"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "Rozłączyć <0>{0}0>?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Rozłączyć z bouncerem soju?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Rozłączyć sieć?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Odkryj"
@@ -891,20 +958,31 @@ msgstr "Pobierz"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "Upuść pliki, aby przesłać"
+msgstr "UpuÅÄ pliki, aby przesÅaÄ"
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "np. <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "np. 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Edytuj"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "Edytuj {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Edytuj profil"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "STRONA GŁÓWNA"
msgid "Homepage"
msgstr "Strona internetowa"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Host"
@@ -1285,7 +1364,7 @@ msgstr "Dołączył do kanału"
#: src/components/ui/InvitationsPanel.tsx
msgid "just now"
-msgstr "przed chwilą"
+msgstr "przed chwilÄ
"
#: src/components/ui/ModerationModal.tsx
#: src/components/ui/UserContextMenu.tsx
@@ -1323,7 +1402,7 @@ msgstr "Opuść kanał"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Pozostaw kanał pusty, aby utworzyć ogólne zaproszenie do sieci. Opis służy wyłącznie do Twoich notatek — widoczny jest tylko dla Ciebie na tej liście."
+msgstr "Pozostaw kanaÅ pusty, aby utworzyÄ ogólne zaproszenie do sieci. Opis sÅuży wyÅÄ
cznie do Twoich notatek â widoczny jest tylko dla Ciebie na tej liÅcie."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "Opuścił kanał"
msgid "Let others know when you are typing"
msgstr "Informuj innych, gdy piszesz"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Podgląd linku"
@@ -1375,6 +1458,10 @@ msgstr "Wczytywanie GIFów..."
msgid "Loading more channels..."
msgstr "Wczytywanie kolejnych kanałów..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Wczytywanie sieci z Twojego bouncera…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "Wczytywanie danych WHOIS..."
@@ -1563,12 +1650,23 @@ msgstr "Nazwa:"
#: src/components/ui/InvitationsPanel.tsx
msgid "network"
-msgstr "sieć"
+msgstr "sieÄ"
+
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Sieć powiązana przez bouncera soju"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Nazwa sieci"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Sieci na {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Nowy DM"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (np. spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Nie wybrano pliku"
msgid "No flood profile"
msgstr "Brak profilu floodu"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "nie ustawiono hosta"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Nie znaleziono zaproszeń"
@@ -1672,7 +1775,7 @@ msgstr "Nie wczytano żadnych podglądów mediów."
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "Nie przechwycono jeszcze surowego ruchu IRC. Spróbuj się połączyć lub wysłać wiadomość."
+msgstr "Nie przechwycono jeszcze surowego ruchu IRC. Spróbuj siÄ poÅÄ
czyÄ lub wysÅaÄ wiadomoÅÄ."
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "Nie znaleziono wyników"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "Nie wybrano serwera. Wybierz najpierw serwer z paska bocznego; linki zapraszające są zarządzane osobno dla każdego serwera."
+msgstr "Nie wybrano serwera. Wybierz najpierw serwer z paska bocznego; linki zapraszajÄ
ce sÄ
zarzÄ
dzane osobno dla każdego serwera."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "Nie ustawiono tematu"
msgid "No unread mentions or messages"
msgstr "Brak nieprzeczytanych wzmianek lub wiadomości"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Brak sieci nadrzędnych."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Brak dostępnych użytkowników"
@@ -1784,6 +1891,10 @@ msgstr "Ups! Podział sieci! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Otwórz"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Otwórz ustawienia konfiguracji kanału"
@@ -1887,6 +1998,10 @@ msgstr "Przypnij prywatną rozmowę"
msgid "Pin this private message conversation"
msgstr "Przypnij tę prywatną rozmowę"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Tekst jawny"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "Wyślij wiadomość prywatną"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Port"
@@ -2006,6 +2122,7 @@ msgstr "zareagował na tę wiadomość"
msgid "Read more"
msgstr "Czytaj więcej"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Powód"
msgid "Reason (optional)"
msgstr "Powód (opcjonalnie)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Ponownie połącz z serwerem"
@@ -2031,7 +2149,7 @@ msgstr "Ponownie połącz z serwerem"
#: src/components/ui/InvitationsPanel.tsx
#: src/components/ui/InvitationsPanel.tsx
msgid "Refresh"
-msgstr "Odśwież"
+msgstr "OdÅwież"
#: src/components/ui/AddServerModal.tsx
msgid "Register for an account"
@@ -2095,6 +2213,7 @@ msgstr "Reguły"
msgid "Safe"
msgstr "Bezpieczne"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Przewijaj"
msgid "Select a channel"
msgstr "Wybierz kanał"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Wybierz sieć"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Wybierz członka"
@@ -2276,6 +2399,10 @@ msgstr "Operatorzy serwerów w sieci mogą potencjalnie odczytywać Twoje wiadom
msgid "Server Password"
msgstr "Hasło serwera"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Hasło serwera (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Komunikacja między serwerami może używać nieszyfrowanych połączeń"
@@ -2380,6 +2507,12 @@ msgstr "Zalogowany od"
msgid "Software:"
msgstr "Oprogramowanie:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "bouncer soju (sterowanie)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Sortuj według nazwy"
@@ -2457,7 +2590,11 @@ msgstr "Ten obraz wygasł"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "Tylu osób zarejestrowało się przez ten link"
+msgstr "Tylu osób zarejestrowaÅo siÄ przez ten link"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "To usunie sieć z twojego bouncera soju. Aby użyć jej ponownie, musisz dodać ją z powrotem."
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "Ten serwer nie obsługuje rozszerzonych metadanych profilu (rozszerzenie
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Ten serwer nie obsługuje linków zapraszających (możliwość<0>obby.world/invitation0>nie jest ogłaszana). Nadal możesz normalnie czatować; ten panel jest przeznaczony dla sieci opartych na obbyircd."
+msgstr "Ten serwer nie obsÅuguje linków zapraszajÄ
cych (możliwoÅÄ<0>obby.world/invitation0>nie jest ogÅaszana). Nadal możesz normalnie czatowaÄ; ten panel jest przeznaczony dla sieci opartych na obbyircd."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Ten serwer obsługuje tylko jeden typ połączenia"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Spowoduje to także zamknięcie {0} powiązanych sieci poniżej."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Spowoduje to także zamknięcie powiązanej sieci poniżej."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Czas (min)"
@@ -2479,6 +2625,10 @@ msgstr "Czas (min)"
msgid "Time Window (seconds)"
msgstr "Okno czasowe (sekundy)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Temat:"
msgid "Total: {0}"
msgstr "Łącznie: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Transport"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Zaufane źródła"
@@ -2615,7 +2769,7 @@ msgstr "Użyj symboli wieloznacznych: * pasuje do dowolnej sekwencji, ? pasuje d
#: src/components/ui/InvitationsPanel.tsx
msgid "used"
-msgstr "użyte"
+msgstr "użyte"
#: src/components/message/JsonLogMessage.tsx
#: src/components/ui/UserProfileModal.tsx
@@ -2641,6 +2795,7 @@ msgstr "Profil użytkownika"
msgid "User Settings"
msgstr "Ustawienia użytkownika"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Szeroki – szerszy zakres ochrony"
msgid "Will default to 'no reason' if left empty"
msgstr "Domyślnie 'brak powodu', jeśli pozostawione puste"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Tak, usuń"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "Masz niezapisane zmiany. Czy na pewno chcesz zamknąć bez zapisywania?"
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "Nie utworzyłeś jeszcze żadnych linków zapraszających. Użyj formularza powyżej, aby utworzyć swój pierwszy."
+msgstr "Nie utworzyÅeÅ jeszcze żadnych linków zapraszajÄ
cych. Użyj formularza powyżej, aby utworzyÄ swój pierwszy."
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "Zaprosiłeś {target} do dołączenia do {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Połączono z <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Hasło Twojego konta do uwierzytelniania"
@@ -2822,13 +2986,17 @@ msgstr "Hasło Twojego konta do uwierzytelniania"
msgid "Your account username for authentication"
msgstr "Nazwa użytkownika Twojego konta do uwierzytelniania"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Twój bouncer nie ma jeszcze żadnych sieci. Dodaj jedną, aby zacząć."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Twój domyślny nick dla wszystkich serwerów"
#: src/components/ui/InvitationsPanel.tsx
msgid "Your invite links"
-msgstr "Twoje linki zapraszające"
+msgstr "Twoje linki zapraszajÄ
ce"
#: src/components/ui/UserSettings.tsx
msgid "Your messages and settings are stored locally on your device"
diff --git a/src/locales/pt/messages.mjs b/src/locales/pt/messages.mjs
index 938ee647..ee53f8f8 100644
--- a/src/locales/pt/messages.mjs
+++ b/src/locales/pt/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Formato de padrão inválido. Use o formato nick!user@host (curingas * permitidos)\"],\"+6NQQA\":[\"Canal de Suporte Geral\"],\"+6NyRG\":[\"Cliente\"],\"+K0AvT\":[\"Desconectar\"],\"+cyFdH\":[\"Mensagem padrão ao marcar-se como ausente\"],\"+mVPqU\":[\"Renderizar formatação Markdown nas mensagens\"],\"+vqCJH\":[\"Seu nome de usuário de conta para autenticação\"],\"+yPBXI\":[\"Escolher arquivo\"],\"+zy2Nq\":[\"Tipo\"],\"/09cao\":[\"Baixa Segurança de Link (Nível \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Usuários fora do canal não podem enviar mensagens\"],\"/4C8U0\":[\"Copiar tudo\"],\"/6BzZF\":[\"Alternar Lista de Membros\"],\"/AkXyp\":[\"Confirmar?\"],\"/TNOPk\":[\"O usuário está ausente\"],\"/XQgft\":[\"Descobrir\"],\"/cF7Rs\":[\"Volume\"],\"/dqduX\":[\"Próxima página\"],\"/fc3q4\":[\"Todo o Conteúdo\"],\"/kISDh\":[\"Ativar sons de notificação\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Áudio\"],\"/rfkZe\":[\"Reproduzir sons para menções e mensagens\"],\"0/0ZGA\":[\"Máscara do nome do canal\"],\"0D6j7U\":[\"Saiba mais sobre regras personalizadas →\"],\"0XsHcR\":[\"Expulsar Usuário\"],\"0ZpE//\":[\"Ordenar por usuários\"],\"0bEPwz\":[\"Definir como Ausente\"],\"0dGkPt\":[\"Expandir lista de canais\"],\"0gS7M5\":[\"Nome de exibição\"],\"0kS+M8\":[\"ExemploREDE\"],\"0rgoY7\":[\"Conectar apenas a servidores que você escolher\"],\"0wdd7X\":[\"Entrar\"],\"0wkVYx\":[\"Mensagens privadas\"],\"111uHX\":[\"Visualização do link\"],\"196EG4\":[\"Excluir Conversa Privada\"],\"1DSr1i\":[\"Registrar uma conta\"],\"1O/24y\":[\"Alternar Lista de Canais\"],\"1VPJJ2\":[\"Aviso de Link Externo\"],\"1ZC/dv\":[\"Nenhuma menção ou mensagem não lida\"],\"1pO1zi\":[\"Nome do servidor é obrigatório\"],\"1uwfzQ\":[\"Ver Tópico do Canal\"],\"268g7c\":[\"Digite o nome de exibição\"],\"2F9+AZ\":[\"Nenhum tráfego IRC bruto capturado ainda. Tente conectar ou enviar uma mensagem.\"],\"2FOFq1\":[\"Operadores de servidor na rede podem potencialmente ler suas mensagens\"],\"2FYpfJ\":[\"Mais\"],\"2HF1Y2\":[[\"inviter\"],\" convidou \",[\"target\"],\" para entrar em \",[\"channel\"]],\"2I70QL\":[\"Ver informações do perfil do usuário\"],\"2QYdmE\":[\"Usuários:\"],\"2QpEjG\":[\"saiu\"],\"2YE223\":[\"Mensagem #\",[\"0\"],\" (Enter para nova linha, Shift+Enter para enviar)\"],\"2bimFY\":[\"Usar senha do servidor\"],\"2iTmdZ\":[\"Armazenamento local:\"],\"2odkwe\":[\"Estrito – Proteção mais agressiva\"],\"2uDhbA\":[\"Digite o nome de usuário para convidar\"],\"2ygf/L\":[\"← Voltar\"],\"2zEgxj\":[\"Pesquisar GIFs...\"],\"3RdPhl\":[\"Renomear Canal\"],\"3THokf\":[\"Usuário com voz\"],\"3TSz9S\":[\"Minimizar\"],\"3jBDvM\":[\"Nome de exibição do canal\"],\"3ryuFU\":[\"Relatórios de falha opcionais para melhorar o app\"],\"3uBF/8\":[\"Fechar visualizador\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Inserir nome da conta...\"],\"4/Rr0R\":[\"Convidar um usuário para o canal atual\"],\"4EZrJN\":[\"Regras\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Perfil de flood (+F)\"],\"4RZQRK\":[\"O que você está fazendo?\"],\"4hfTrB\":[\"Apelido\"],\"4n99LO\":[\"Já em \",[\"0\"]],\"4t6vMV\":[\"Mudar automaticamente para linha única em mensagens curtas\"],\"4vsHmf\":[\"Tempo (min)\"],\"5+INAX\":[\"Destacar mensagens que mencionam você\"],\"5R5Pv/\":[\"Nome Oper\"],\"678PKt\":[\"Nome da Rede\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Senha necessária para entrar no canal. Deixe vazio para remover a chave.\"],\"6HhMs3\":[\"Mensagem de saída\"],\"6V3Ea3\":[\"Copiado\"],\"6lGV3K\":[\"Mostrar menos\"],\"6yFOEi\":[\"Inserir senha do oper...\"],\"7+IHTZ\":[\"Nenhum arquivo escolhido\"],\"73hrRi\":[\"nick!user@host (ex.: spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Enviar mensagem privada\"],\"7U1W7c\":[\"Muito Relaxado\"],\"7Y1YQj\":[\"Nome real:\"],\"7YHArF\":[\"— abrir no visualizador\"],\"7fjnVl\":[\"Pesquisar usuários...\"],\"7jL88x\":[\"Excluir esta mensagem? Esta ação não pode ser desfeita.\"],\"7nGhhM\":[\"O que você está pensando?\"],\"7sEpu1\":[\"Membros — \",[\"0\"]],\"7sNhEz\":[\"Nome de usuário\"],\"8H0Q+x\":[\"Saiba mais sobre perfis →\"],\"8Phu0A\":[\"Exibir quando usuários mudam seu apelido\"],\"8XTG9e\":[\"Digite a senha oper\"],\"8XsV2J\":[\"Tentar enviar novamente\"],\"8ZsakT\":[\"Senha\"],\"8kR84m\":[\"Você está prestes a abrir um link externo:\"],\"8lCgih\":[\"Remover regra\"],\"8o3dPc\":[\"Solte os arquivos para enviar\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"entrou\"],\"other\":[\"entrou \",[\"joinCount\"],\" vezes\"]}]],\"9BMLnJ\":[\"Reconectar ao servidor\"],\"9OEgyT\":[\"Adicionar reação\"],\"9PQ8m2\":[\"G-Line (ban global)\"],\"9Qs99X\":[\"E-mail:\"],\"9QupBP\":[\"Remover padrão\"],\"9bG48P\":[\"Enviando\"],\"9f5f0u\":[\"Dúvidas sobre privacidade? Entre em contato:\"],\"9unqs3\":[\"Ausente:\"],\"9v3hwv\":[\"Nenhum servidor encontrado.\"],\"9zb2WA\":[\"Conectando\"],\"A1taO8\":[\"Pesquisar\"],\"A2adVi\":[\"Enviar notificações de digitação\"],\"A9Rhec\":[\"Nome do canal\"],\"AWOSPo\":[\"Ampliar\"],\"AXSpEQ\":[\"Oper ao conectar\"],\"AeXO77\":[\"Conta\"],\"AhNP40\":[\"Avançar\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Apelido alterado\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Cancelar resposta\"],\"ApSx0O\":[\"Encontradas \",[\"0\"],\" mensagens correspondentes a \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Nenhum resultado encontrado\"],\"AyNqAB\":[\"Exibir todos os eventos do servidor no chat\"],\"B/QqGw\":[\"Longe do teclado\"],\"B8AaMI\":[\"Este campo é obrigatório\"],\"BA2c49\":[\"O servidor não suporta filtragem avançada de LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" e mais \",[\"3\"],\" estão digitando...\"],\"BGul2A\":[\"Você tem alterações não salvas. Tem certeza que deseja fechar sem salvar?\"],\"BIf9fi\":[\"Sua mensagem de status\"],\"BPm98R\":[\"Nenhum servidor selecionado. Escolha primeiro um servidor na barra lateral; os links de convite são gerenciados por servidor.\"],\"BZz3md\":[\"Seu site pessoal\"],\"Bgm/H7\":[\"Permitir inserção de múltiplas linhas de texto\"],\"BiQIl1\":[\"Fixar esta conversa de mensagem privada\"],\"BlNZZ2\":[\"Clique para ir à mensagem\"],\"Bowq3c\":[\"Apenas operadores podem alterar o tópico\"],\"Btozzp\":[\"Esta imagem expirou\"],\"Bycfjm\":[\"Total: \",[\"0\"]],\"C6IBQc\":[\"Copiar JSON completo\"],\"C9L9wL\":[\"Coleta de dados\"],\"CDq4wC\":[\"Moderar Usuário\"],\"CHVRxG\":[\"Mensagem @\",[\"0\"],\" (Shift+Enter para nova linha)\"],\"CN9zdR\":[\"Nome oper e senha são obrigatórios\"],\"CW3sYa\":[\"Adicionar reação \",[\"emoji\"]],\"CaAkqd\":[\"Mostrar desconexões\"],\"CbvaYj\":[\"Banir por apelido\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Selecionar um canal\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"Sistema\"],\"D28t6+\":[\"entrou e saiu\"],\"DB8zMK\":[\"Aplicar\"],\"DBcWHr\":[\"Arquivo de som de notificação personalizado\"],\"DTy9Xw\":[\"Pré-visualizações de mídia\"],\"Dj4pSr\":[\"Escolha uma senha segura\"],\"Du+zn+\":[\"Pesquisando...\"],\"Du2T2f\":[\"Configuração não encontrada\"],\"DwsSVQ\":[\"Aplicar filtros e atualizar\"],\"E3W/zd\":[\"Apelido padrão\"],\"E6nRW7\":[\"Copiar URL\"],\"E703RG\":[\"Modos:\"],\"EAeu1Z\":[\"Enviar convite\"],\"EFKJQT\":[\"Configuração\"],\"EGPQBv\":[\"Regras de flood personalizadas (+f)\"],\"ELik0r\":[\"Ver política de privacidade completa\"],\"EPbeC2\":[\"Ver ou editar o tópico do canal\"],\"EQCDNT\":[\"Inserir nome de usuário oper...\"],\"EUvulZ\":[\"Encontrada 1 mensagem correspondente a \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Próxima imagem\"],\"EdQY6l\":[\"Nenhum\"],\"EnqLYU\":[\"Pesquisar servidores...\"],\"F0OKMc\":[\"Editar Servidor\"],\"F6Int2\":[\"Ativar destaques\"],\"FDoLyE\":[\"Usuários máx.\"],\"FUU/hZ\":[\"Controla quanto conteúdo de mídia externa é carregado no chat.\"],\"Fdp03t\":[\"ativo\"],\"FfPWR0\":[\"Modal\"],\"FjkaiT\":[\"Reduzir\"],\"FlqOE9\":[\"O que isso significa:\"],\"FolHNl\":[\"Gerenciar sua conta e autenticação\"],\"Fp2Dif\":[\"Saiu do servidor\"],\"G5KmCc\":[\"GZ-Line (Z-Line global)\"],\"GDs0lz\":[\"<0>Risco:0> Informações sensíveis (mensagens, conversas privadas, dados de autenticação) podem ser expostas a administradores de rede ou atacantes posicionados entre servidores IRC.\"],\"GR+2I3\":[\"Adicionar máscara de convite (ex.: nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Fechar avisos do servidor em destaque\"],\"GdhD7H\":[\"Clique novamente para confirmar\"],\"GlHnXw\":[\"Falha ao alterar apelido: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Prévia:\"],\"GtmO8/\":[\"de\"],\"GtuHUQ\":[\"Renomear este canal no servidor. Todos os usuários verão o novo nome.\"],\"GuGfFX\":[\"Alternar pesquisa\"],\"GxkJXS\":[\"Enviando...\"],\"GzbwnK\":[\"Entrou no canal\"],\"GzsUDB\":[\"Perfil estendido\"],\"H/PnT8\":[\"Inserir emoji\"],\"H6Izzl\":[\"Seu código de cor preferido\"],\"H9jIv+\":[\"Mostrar entradas/saídas\"],\"HAKBY9\":[\"Enviar ficheiros\"],\"HdE1If\":[\"Canal\"],\"Hk4AW9\":[\"Seu nome de exibição preferido\"],\"HmHDk7\":[\"Selecionar Membro\"],\"HrQzPU\":[\"Canais em \",[\"networkName\"]],\"I2tXQ5\":[\"Mensagem @\",[\"0\"],\" (Enter para nova linha, Shift+Enter para enviar)\"],\"I6bw/h\":[\"Banir usuário\"],\"I92Z+b\":[\"Ativar notificações\"],\"I9D72S\":[\"Tem certeza que deseja excluir esta mensagem? Esta ação não pode ser desfeita.\"],\"IA+1wo\":[\"Exibir quando usuários são expulsos de canais\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Salvar Alterações\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" e \",[\"2\"],\" estão digitando...\"],\"IgrLD/\":[\"Pausar\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Responder\"],\"IoHMnl\":[\"O valor máximo é \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Conectando...\"],\"J5T9NW\":[\"Informações do usuário\"],\"J8Y5+z\":[\"Ops! Divisão de rede! ⚠️\"],\"JBHkBA\":[\"Saiu do canal\"],\"JCwL0Q\":[\"Digite o motivo (opcional)\"],\"JFciKP\":[\"Alternar\"],\"JXGkhG\":[\"Alterar o nome do canal (apenas operadores)\"],\"JcD7qf\":[\"Mais ações\"],\"JdkA+c\":[\"Secreto (+s)\"],\"Jmu12l\":[\"Canais do Servidor\"],\"JvQ++s\":[\"Ativar Markdown\"],\"K2jwh/\":[\"Nenhum dado WHOIS disponível\"],\"KAXSwC\":[\"Voz\"],\"KDfTdX\":[\"Excluir mensagem\"],\"KKBlUU\":[\"Incorporar\"],\"KM0pLb\":[\"Bem-vindo ao canal!\"],\"KR6W2h\":[\"Deixar de ignorar usuário\"],\"KV+Bi1\":[\"Apenas por convite (+i)\"],\"KdCtwE\":[\"Quantos segundos monitorar a atividade de flood antes de redefinir os contadores\"],\"Kkezga\":[\"Senha do Servidor\"],\"KsiQ/8\":[\"Os usuários devem ser convidados para entrar no canal\"],\"L+gB/D\":[\"Informações do canal\"],\"LC1a7n\":[\"O servidor IRC relatou que seus links servidor a servidor têm um nível de segurança baixo. Isso significa que quando suas mensagens são retransmitidas entre servidores IRC na rede, elas podem não estar devidamente criptografadas ou os certificados SSL/TLS podem não ser validados corretamente.\"],\"LNfLR5\":[\"Mostrar expulsões\"],\"LQb0W/\":[\"Mostrar todos os eventos\"],\"LU7/yA\":[\"Nome alternativo para exibição. Pode conter espaços, emojis e caracteres especiais. O nome real (\",[\"channelName\"],\") ainda será usado para comandos IRC.\"],\"LUb9O7\":[\"Porta de servidor válida é obrigatória\"],\"LV4fT6\":[\"Descrição (opcional, ex.: \\\"Beta testers Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Política de privacidade\"],\"LcuSDR\":[\"Gerenciar suas informações de perfil e metadados\"],\"LqLS9B\":[\"Mostrar mudanças de apelido\"],\"LsDQt2\":[\"Configurações do Canal\"],\"LtI9AS\":[\"Dono\"],\"LuNhhL\":[\"reagiu a esta mensagem\"],\"M/AZNG\":[\"URL da sua imagem de avatar\"],\"M/WIer\":[\"Enviar mensagem\"],\"M8er/5\":[\"Nome:\"],\"MHk+7g\":[\"Imagem anterior\"],\"MRorGe\":[\"Mensagem Privada\"],\"MVbSGP\":[\"Janela de tempo (segundos)\"],\"MkpcsT\":[\"Suas mensagens e configurações são armazenadas localmente\"],\"N/hDSy\":[\"Marcar como bot, geralmente 'on' ou vazio\"],\"N7TQbE\":[\"Convidar usuário para \",[\"channelName\"]],\"NCca/o\":[\"Inserir apelido padrão...\"],\"Nqs6B9\":[\"Exibe toda a mídia externa. Qualquer URL pode causar uma requisição a um servidor desconhecido.\"],\"Nt+9O7\":[\"Usar WebSocket em vez de TCP bruto\"],\"NxIHzc\":[\"Expulsar usuário\"],\"O+v/cL\":[\"Navegar por todos os canais do servidor\"],\"ODwSCk\":[\"Enviar um GIF\"],\"OGQ5kK\":[\"Configurar sons de notificação e destaques\"],\"OIPt1Z\":[\"Mostrar ou ocultar a barra lateral de membros\"],\"OKSNq/\":[\"Muito Estrito\"],\"ONWvwQ\":[\"Carregar\"],\"OVKoQO\":[\"Sua senha de conta para autenticação\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Levando o IRC para o futuro\"],\"OhCpra\":[\"Definir um tópico…\"],\"OkltoQ\":[\"Banir \",[\"username\"],\" por apelido (impede que entre novamente com o mesmo nick)\"],\"P+t/Te\":[\"Sem dados adicionais\"],\"P42Wcc\":[\"Seguro\"],\"PD38l0\":[\"Visualização do avatar do canal\"],\"PD9mEt\":[\"Digite uma mensagem...\"],\"PPqfdA\":[\"Abrir configurações do canal\"],\"PSCjfZ\":[\"O tópico exibido para este canal. Todos os usuários podem ver.\"],\"PZCecv\":[\"Pré-visualização de PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 vez\"],\"other\":[[\"c\"],\" vezes\"]}]],\"PguS2C\":[\"Adicionar máscara de exceção (ex.: nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Mostrando \",[\"displayedChannelsCount\"],\" de \",[\"0\"],\" canais\"],\"PqhVlJ\":[\"Banir Usuário (por Hostmask)\"],\"Q+chwU\":[\"Nome de usuário:\"],\"Q2QY4/\":[\"Excluir este convite\"],\"Q6hhn8\":[\"Preferências\"],\"QF4a34\":[\"Por favor, insira um nome de usuário\"],\"QGqSZ2\":[\"Cor e Formatação\"],\"QJQd1J\":[\"Editar perfil\"],\"QSzGDE\":[\"Ocioso\"],\"QUlny5\":[\"Bem-vindo ao \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Ler mais\"],\"QuSkCF\":[\"Filtrar canais...\"],\"QwUrDZ\":[\"alterou o tópico para: \",[\"topic\"]],\"R0UH07\":[\"Imagem \",[\"0\"],\" de \",[\"1\"]],\"R7SsBE\":[\"Silenciar\"],\"R8rf1X\":[\"Clique para definir o tópico\"],\"RArB3D\":[\"foi expulso de \",[\"channelName\"],\" por \",[\"username\"]],\"RI3cWd\":[\"Descubra o mundo do IRC com o ObsidianIRC\"],\"RIfHS5\":[\"Criar um novo link de convite\"],\"RMMaN5\":[\"Moderado (+m)\"],\"RWw9Lg\":[\"Fechar janela\"],\"RZ2BuZ\":[\"O registro da conta \",[\"account\"],\" requer verificação: \",[\"message\"]],\"RySp6q\":[\"Ocultar comentários\"],\"SPKQTd\":[\"Apelido é obrigatório\"],\"SPVjfj\":[\"Será definido como 'sem motivo' se deixado em branco\"],\"SQKPvQ\":[\"Convidar Usuário\"],\"SkZcl+\":[\"Escolha um perfil de proteção contra flood predefinido. Estes perfis fornecem configurações de proteção equilibradas para diferentes casos de uso.\"],\"Slr+3C\":[\"Usuários mín.\"],\"Spnlre\":[\"Você convidou \",[\"target\"],\" para entrar em \",[\"channel\"]],\"T/ckN5\":[\"Abrir no visualizador\"],\"T91vKp\":[\"Reproduzir\"],\"TV2Wdu\":[\"Saiba como tratamos seus dados e protegemos sua privacidade.\"],\"TgFpwD\":[\"Aplicando...\"],\"TkzSFB\":[\"Sem Alterações\"],\"TtserG\":[\"Digite o nome real\"],\"Ttz9J1\":[\"Inserir senha...\"],\"Tz0i8g\":[\"Configurações\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reagir\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Você ainda não criou nenhum link de convite. Use o formulário acima para criar o primeiro.\"],\"UGT5vp\":[\"Salvar configurações\"],\"UV5hLB\":[\"Nenhum banimento encontrado\"],\"Uaj3Nd\":[\"Mensagens de Status\"],\"Ue3uny\":[\"Padrão (sem perfil)\"],\"UkARhe\":[\"Normal – Proteção padrão\"],\"Umn7Cj\":[\"Ainda sem comentários. Seja o primeiro!\"],\"UtUIRh\":[[\"0\"],\" mensagens antigas\"],\"UwzP+U\":[\"Conexão segura\"],\"V0/A4O\":[\"Proprietário do canal\"],\"V4qgxE\":[\"Criado antes (min atrás)\"],\"V8yTm6\":[\"Limpar pesquisa\"],\"VJMMyz\":[\"ObsidianIRC - Levando o IRC ao futuro\"],\"VJScHU\":[\"Motivo\"],\"VLsmVV\":[\"Silenciar notificações\"],\"VbyRUy\":[\"Comentários\"],\"Vmx0mQ\":[\"Definido por:\"],\"VqnIZz\":[\"Ver nossa política de privacidade e práticas de dados\"],\"VrMygG\":[\"O comprimento mínimo é \",[\"0\"]],\"VrnTui\":[\"Seus pronomes, exibidos no seu perfil\"],\"W8E3qn\":[\"Conta autenticada\"],\"WAakm9\":[\"Excluir Canal\"],\"WFxTHC\":[\"Adicionar máscara de banimento (ex.: nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Host do servidor é obrigatório\"],\"WRYdXW\":[\"Posição do áudio\"],\"WUOH5B\":[\"Ignorar usuário\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Mostrar 1 item a mais\"],\"other\":[\"Mostrar \",[\"1\"],\" itens a mais\"]}]],\"WYxRzo\":[\"Crie e gerencie seus links de convite\"],\"Wd38W1\":[\"Deixe o canal em branco para um convite genérico de rede. A descrição é apenas para seus registros — visível somente para você nesta lista.\"],\"Weq9zb\":[\"Geral\"],\"Wfj7Sk\":[\"Silenciar ou ativar sons de notificação\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Perfil do Usuário\"],\"X6S3lt\":[\"Pesquisar configurações, canais, servidores...\"],\"XEHan5\":[\"Continuar Assim Mesmo\"],\"XI1+wb\":[\"Formato inválido\"],\"XIXeuC\":[\"Mensagem @\",[\"0\"]],\"XMS+k4\":[\"Iniciar Mensagem Privada\"],\"XWgxXq\":[\"Álbum\"],\"Xd7+IT\":[\"Desafixar Conversa Privada\"],\"Xm/s+u\":[\"Exibição\"],\"Xp2n93\":[\"Exibe mídia do host de arquivos confiável do seu servidor. Nenhuma requisição é feita a serviços externos.\"],\"XvjC4F\":[\"Salvando...\"],\"Y/qryO\":[\"Nenhum usuário encontrado para sua pesquisa\"],\"YAqRpI\":[\"Registro da conta \",[\"account\"],\" bem-sucedido: \",[\"message\"]],\"YEfzvP\":[\"Tópico protegido (+t)\"],\"YQOn6a\":[\"Recolher lista de membros\"],\"YRCoE9\":[\"Operador do canal\"],\"YURQaF\":[\"Ver perfil\"],\"YdBSvr\":[\"Controlar exibição de mídia e conteúdo externo\"],\"Yj6U3V\":[\"Sem servidor central:\"],\"YjvpGx\":[\"Pronomes\"],\"YqH4l4\":[\"Sem chave\"],\"YyUPpV\":[\"Conta:\"],\"ZJSWfw\":[\"Mensagem exibida ao desconectar do servidor\"],\"ZR1dJ4\":[\"Convites\"],\"ZdWg0V\":[\"Abrir no navegador\"],\"ZhRBbl\":[\"Pesquisar mensagens…\"],\"Zmcu3y\":[\"Filtros avançados\"],\"a2/8e5\":[\"Tópico definido após (min atrás)\"],\"aHKcKc\":[\"Página anterior\"],\"aJTbXX\":[\"Senha Oper\"],\"aQryQv\":[\"O padrão já existe\"],\"aW9pLN\":[\"Número máximo de usuários permitidos. Deixe vazio para sem limite.\"],\"ah4fmZ\":[\"Também exibe visualizações do YouTube, Vimeo, SoundCloud e serviços conhecidos similares.\"],\"aifXak\":[\"Nenhuma mídia neste canal\"],\"ap2zBz\":[\"Relaxado\"],\"az8lvo\":[\"Desligado\"],\"azXSNo\":[\"Expandir lista de membros\"],\"azdliB\":[\"Entrar em uma conta\"],\"b26wlF\":[\"ela/dela\"],\"bD/+Ei\":[\"Estrito\"],\"bQ6BJn\":[\"Configure regras detalhadas de proteção contra flood. Cada regra especifica que tipo de atividade monitorar e que ação tomar quando os limites são excedidos.\"],\"beV7+y\":[\"O usuário receberá um convite para entrar em \",[\"channelName\"],\".\"],\"bk84cH\":[\"Mensagem de ausência\"],\"bkHdLj\":[\"Adicionar servidor IRC\"],\"bmQLn5\":[\"Adicionar regra\"],\"bwRvnp\":[\"Ação\"],\"c8+EVZ\":[\"Conta verificada\"],\"cGYUlD\":[\"Nenhuma visualização de mídia é carregada.\"],\"cLF98o\":[\"Mostrar comentários (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Nenhum usuário disponível\"],\"cSgpoS\":[\"Fixar Conversa Privada\"],\"cde3ce\":[\"Mensagem <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Copiar saída formatada\"],\"cl/A5J\":[\"Bem-vindo ao \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Excluir\"],\"coPLXT\":[\"Não armazenamos suas comunicações IRC em nossos servidores\"],\"crYH/6\":[\"Player do SoundCloud\"],\"d3sis4\":[\"Adicionar Servidor\"],\"d9aN5k\":[\"Remover \",[\"username\"],\" do canal\"],\"dEgA5A\":[\"Cancelar\"],\"dGi1We\":[\"Desafixar esta conversa de mensagem privada\"],\"dJVuyC\":[\"saiu de \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"para\"],\"dXqxlh\":[\"<0>⚠️ Risco de Segurança!0> Esta conexão pode ser vulnerável a interceptação ou ataques man-in-the-middle.\"],\"da9Q/R\":[\"Modos do canal alterados\"],\"dhJN3N\":[\"Mostrar comentários\"],\"dj2xTE\":[\"Dispensar notificação\"],\"dpCzmC\":[\"Configurações de proteção contra flood\"],\"e9dQpT\":[\"Deseja abrir este link em uma nova aba?\"],\"ePK91l\":[\"Editar\"],\"eYBDuB\":[\"Faça upload de uma imagem ou forneça uma URL com substituição opcional \",[\"size\"]],\"edBbee\":[\"Banir \",[\"username\"],\" por hostmask (impede que entre novamente pelo mesmo IP/host)\"],\"ekfzWq\":[\"Configurações do usuário\"],\"elPDWs\":[\"Personalize sua experiência no cliente IRC\"],\"eu2osY\":[\"<0>💡 Recomendação:0> Prossiga apenas se você confiar neste servidor e compreender os riscos. Evite compartilhar informações sensíveis ou senhas nesta conexão.\"],\"euEhbr\":[\"Clique para entrar em \",[\"channel\"]],\"ez3vLd\":[\"Ativar entrada multilinha\"],\"f0J5Ki\":[\"A comunicação servidor a servidor pode usar conexões não criptografadas\"],\"f9BHJk\":[\"Avisar Usuário\"],\"fDOLLd\":[\"Nenhum canal encontrado.\"],\"ffzDkB\":[\"Análises anônimas:\"],\"fq1GF9\":[\"Exibir quando usuários se desconectam do servidor\"],\"gEF57C\":[\"Este servidor suporta apenas um tipo de conexão\"],\"gJuLUI\":[\"Lista de ignorados\"],\"gNzMrk\":[\"Avatar atual\"],\"gjPWyO\":[\"Inserir apelido...\"],\"gz6UQ3\":[\"Maximizar\"],\"h6razj\":[\"Excluir máscara do nome do canal\"],\"hG6jnw\":[\"Nenhum tópico definido\"],\"hG89Ed\":[\"Imagem\"],\"hYgDIe\":[\"Criar\"],\"hZ6znB\":[\"Porta\"],\"ha+Bz5\":[\"ex.: 100:1440\"],\"he3ygx\":[\"Copiar\"],\"hehnjM\":[\"Quantidade\"],\"hzdLuQ\":[\"Apenas usuários com voz ou superior podem falar\"],\"i0qMbr\":[\"Início\"],\"iDNBZe\":[\"Notificações\"],\"iH8pgl\":[\"Voltar\"],\"iL9SZg\":[\"Banir Usuário (por Apelido)\"],\"iNt+3c\":[\"Voltar para a imagem\"],\"iQvi+a\":[\"Não me avisar sobre baixa segurança de link para este servidor\"],\"iSLIjg\":[\"Conectar\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Host do Servidor\"],\"idD8Ev\":[\"Salvo\"],\"iivqkW\":[\"Conectado em\"],\"ij+Elv\":[\"Visualização da imagem\"],\"ilIWp7\":[\"Alternar Notificações\"],\"iuaqvB\":[\"Use * para curingas. Exemplos: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Banir por máscara de host\"],\"jA4uoI\":[\"Tópico:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Motivo (opcional)\"],\"jUV7CU\":[\"Fazer upload do avatar\"],\"jW5Uwh\":[\"Controla quanto conteúdo de mídia externa é carregado. Desativado / Seguro / Fontes confiáveis / Todo conteúdo.\"],\"jXzms5\":[\"Opções de anexo\"],\"jZlrte\":[\"Cor\"],\"jfC/xh\":[\"Contato\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Carregar mensagens antigas\"],\"k3ID0F\":[\"Filtrar membros…\"],\"k65gsE\":[\"Mergulho profundo\"],\"k7Zgob\":[\"Cancelar Conexão\"],\"kAVx5h\":[\"Nenhum convite encontrado\"],\"kCLEPU\":[\"Conectado a\"],\"kF5LKb\":[\"Padrões ignorados:\"],\"kGeOx/\":[\"Entrar em \",[\"0\"]],\"kITKr8\":[\"Carregando modos do canal...\"],\"kPpPsw\":[\"Você é um IRC Operator\"],\"kWJmRL\":[\"Você\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Copiar JSON\"],\"krViRy\":[\"Clique para copiar como JSON\"],\"ks71ra\":[\"Exceções\"],\"kw4lRv\":[\"Meio operador do canal\"],\"kxgIRq\":[\"Selecione ou adicione um canal para começar.\"],\"ky6dWe\":[\"Visualização do avatar\"],\"l+GxCv\":[\"Carregando canais...\"],\"l+IUVW\":[\"Verificação da conta \",[\"account\"],\" bem-sucedida: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"reconectou\"],\"other\":[\"reconectou \",[\"reconnectCount\"],\" vezes\"]}]],\"l1l8sj\":[\"há \",[\"0\"],\"d\"],\"l5NhnV\":[\"#canal (opcional)\"],\"l5jmzx\":[[\"0\"],\" e \",[\"1\"],\" estão digitando...\"],\"lCF0wC\":[\"Atualizar\"],\"lHy8N5\":[\"Carregando mais canais...\"],\"lasgrr\":[\"usado\"],\"lbpf14\":[\"Entrar em \",[\"value\"]],\"lfFsZ4\":[\"Canais\"],\"lkNdiH\":[\"Nome da conta\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Enviar Imagem\"],\"loQxaJ\":[\"Estou de Volta\"],\"lvfaxv\":[\"INÍCIO\"],\"m16xKo\":[\"Adicionar\"],\"m8flAk\":[\"Prévia (ainda não enviado)\"],\"mEPxTp\":[\"<0>⚠️ Cuidado!0> Abra apenas links de fontes confiáveis. Links maliciosos podem comprometer sua segurança ou privacidade.\"],\"mHGdhG\":[\"Informações do servidor\"],\"mHS8lb\":[\"Mensagem #\",[\"0\"]],\"mMYBD9\":[\"Amplo – Escopo de proteção mais amplo\"],\"mTGsPd\":[\"Tópico do canal\"],\"mU8j6O\":[\"Sem mensagens externas (+n)\"],\"mZp8FL\":[\"Retorno automático para linha única\"],\"mdQu8G\":[\"SeuApelido\"],\"miSSBQ\":[\"Comentários (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Usuário autenticado\"],\"mwtcGl\":[\"Fechar comentários\"],\"mzI/c+\":[\"Baixar\"],\"n3fGRk\":[\"definido por \",[\"0\"]],\"nE9jsU\":[\"Relaxado – Proteção menos agressiva\"],\"nNflMD\":[\"Sair do canal\"],\"nPXkBi\":[\"Carregando dados WHOIS...\"],\"nQnxxF\":[\"Mensagem #\",[\"0\"],\" (Shift+Enter para nova linha)\"],\"nWMRxa\":[\"Desafixar\"],\"nkC032\":[\"Sem perfil de flood\"],\"o69z4d\":[\"Enviar uma mensagem de aviso para \",[\"username\"]],\"o9ylQi\":[\"Pesquise GIFs para começar\"],\"oFGkER\":[\"Avisos do Servidor\"],\"oOi11l\":[\"Rolar para o fim\"],\"oPYIL5\":[\"rede\"],\"oQEzQR\":[\"Nova mensagem direta\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Ataques man-in-the-middle em links de servidor são possíveis\"],\"oeqmmJ\":[\"Fontes Confiáveis\"],\"optX0N\":[\"há \",[\"0\"],\"h\"],\"ovBPCi\":[\"Padrão\"],\"p0Z69r\":[\"O padrão não pode estar vazio\"],\"p1KgtK\":[\"Falha ao carregar áudio\"],\"p59pEv\":[\"Detalhes adicionais\"],\"p7sRI6\":[\"Avisar outros quando você está digitando\"],\"pBm1od\":[\"Canal secreto\"],\"pNmiXx\":[\"Seu apelido padrão para todos os servidores\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Senha da conta\"],\"peNE68\":[\"Permanente\"],\"plhHQt\":[\"Sem dados\"],\"pm6+q5\":[\"Aviso de Segurança\"],\"pn5qSs\":[\"Informações adicionais\"],\"q0cR4S\":[\"agora é conhecido como **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"O canal não aparecerá nos comandos LIST ou NAMES\"],\"qLpTm/\":[\"Remover reação \",[\"emoji\"]],\"qVkGWK\":[\"Fixar\"],\"qY8wNa\":[\"Página inicial\"],\"qb0xJ7\":[\"Curingas: * corresponde a qualquer sequência, ? a um único caractere. Exemplos: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Chave do canal (+k)\"],\"qtoOYG\":[\"Sem limite\"],\"r1W2AS\":[\"Imagem do servidor de arquivos\"],\"rIPR2O\":[\"Tópico definido antes (min atrás)\"],\"rMMSYo\":[\"O comprimento máximo é \",[\"0\"]],\"rWtzQe\":[\"A rede se dividiu e reconectou. ✅\"],\"rYG2u6\":[\"Aguarde...\"],\"rdUucN\":[\"Visualização\"],\"rjGI/Q\":[\"Privacidade\"],\"rk8iDX\":[\"Carregando GIFs...\"],\"rn6SBY\":[\"Ativar som\"],\"s/UKqq\":[\"Foi expulso do canal\"],\"s8cATI\":[\"entrou em \",[\"channelName\"]],\"sCO9ue\":[\"A conexão com <0>\",[\"serverName\"],\"0> apresenta as seguintes preocupações de segurança:\"],\"sGH11W\":[\"Servidor\"],\"sHI1H+\":[\"agora é conhecido como **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" convidou você para entrar em \",[\"channel\"]],\"sby+1/\":[\"Clique para copiar\"],\"sfN25C\":[\"Seu nome real ou completo\"],\"sliuzR\":[\"Abrir Link\"],\"sqrO9R\":[\"Menções personalizadas\"],\"sr6RdJ\":[\"Multilinha com Shift+Enter\"],\"swrCpB\":[\"O canal foi renomeado de \",[\"oldName\"],\" para \",[\"newName\"],\" por \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Avançado\"],\"t/YqKh\":[\"Remover\"],\"t47eHD\":[\"Seu identificador único neste servidor\"],\"tAkAh0\":[\"URL com substituição opcional \",[\"size\"],\". Exemplo: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Mostrar ou ocultar a barra lateral de canais\"],\"tfDRzk\":[\"Salvar\"],\"tiBsJk\":[\"saiu de \",[\"channelName\"]],\"tt4/UD\":[\"saiu (\",[\"reason\"],\")\"],\"u0TcnO\":[\"O apelido {nick} já está em uso, tentando com {newNick}\"],\"u0a8B4\":[\"Autenticar como operador IRC para acesso administrativo\"],\"u0rWFU\":[\"Criado após (min atrás)\"],\"u72w3t\":[\"Usuários e padrões a ignorar\"],\"u7jc2L\":[\"saiu\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Falha ao salvar: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"Servidores IRC:\"],\"ukyW4o\":[\"Seus links de convite\"],\"usSSr/\":[\"Nível de zoom\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Shift+Enter para novas linhas (Enter envia)\"],\"vERlcd\":[\"Perfil\"],\"vK0RL8\":[\"Sem tópico\"],\"vSJd18\":[\"Vídeo\"],\"vXIe7J\":[\"Idioma\"],\"vaHYxN\":[\"Nome real\"],\"vhjbKr\":[\"Ausente\"],\"w4NYox\":[\"cliente \",[\"title\"]],\"w8xQRx\":[\"Valor inválido\"],\"wFjjxZ\":[\"foi expulso de \",[\"channelName\"],\" por \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nenhuma exceção de banimento encontrada\"],\"wPrGnM\":[\"Administrador do canal\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Exibir quando usuários entram ou saem de canais\"],\"whqZ9r\":[\"Palavras ou frases adicionais para destacar\"],\"wm7RV4\":[\"Som de notificação\"],\"wz/Yoq\":[\"Suas mensagens podem ser interceptadas ao serem retransmitidas entre servidores\"],\"x3+y8b\":[\"Esta é a quantidade de pessoas que se registraram por este link\"],\"xCJdfg\":[\"Limpar\"],\"xOTzt5\":[\"agora mesmo\"],\"xUHRTR\":[\"Autenticar automaticamente como operador ao conectar\"],\"xWHwwQ\":[\"Banimentos\"],\"xYilR2\":[\"Mídia\"],\"xbi8D6\":[\"Este servidor não suporta links de convite (a capability<0>obby.world/invitation0>não é anunciada). Você ainda pode conversar normalmente; este painel é para redes baseadas em obbyircd.\"],\"xceQrO\":[\"Apenas websockets seguros são suportados\"],\"xdtXa+\":[\"nome-do-canal\"],\"xfXC7q\":[\"Canais de texto\"],\"xlCYOE\":[\"Carregando mais mensagens...\"],\"xlhswE\":[\"O valor mínimo é \",[\"0\"]],\"xq97Ci\":[\"Adicionar uma palavra ou frase...\"],\"xuRqRq\":[\"Limite de clientes (+l)\"],\"xwF+7J\":[[\"0\"],\" está digitando...\"],\"y1eoq1\":[\"Copiar link\"],\"yNeucF\":[\"Este servidor não suporta metadados de perfil estendidos (extensão IRCv3 METADATA). Campos como avatar, nome de exibição e status não estão disponíveis.\"],\"yPlrca\":[\"Avatar do canal\"],\"yQE2r9\":[\"Carregando\"],\"ySU+JY\":[\"seu@email.com\"],\"yTX1Rt\":[\"Nome de usuário Oper\"],\"yYOzWD\":[\"logs\"],\"yfx9Re\":[\"Senha de operador IRC\"],\"ygCKqB\":[\"Parar\"],\"ymDxJx\":[\"Nome de usuário de operador IRC\"],\"yrpRsQ\":[\"Ordenar por nome\"],\"yz7wBu\":[\"Fechar\"],\"zJw+jA\":[\"define modo: \",[\"0\"]],\"zbymaY\":[\"há \",[\"0\"],\"min\"],\"zebeLu\":[\"Digite o nome de usuário oper\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Formato de padrão inválido. Use o formato nick!user@host (curingas * permitidos)\"],\"+6NQQA\":[\"Canal de Suporte Geral\"],\"+6NyRG\":[\"Cliente\"],\"+K0AvT\":[\"Desconectar\"],\"+cyFdH\":[\"Mensagem padrão ao marcar-se como ausente\"],\"+mVPqU\":[\"Renderizar formatação Markdown nas mensagens\"],\"+vqCJH\":[\"Seu nome de usuário de conta para autenticação\"],\"+yPBXI\":[\"Escolher arquivo\"],\"+zy2Nq\":[\"Tipo\"],\"/09cao\":[\"Baixa Segurança de Link (Nível \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Usuários fora do canal não podem enviar mensagens\"],\"/4C8U0\":[\"Copiar tudo\"],\"/6BzZF\":[\"Alternar Lista de Membros\"],\"/AkXyp\":[\"Confirmar?\"],\"/TNOPk\":[\"O usuário está ausente\"],\"/XQgft\":[\"Descobrir\"],\"/cF7Rs\":[\"Volume\"],\"/dqduX\":[\"Próxima página\"],\"/fc3q4\":[\"Todo o Conteúdo\"],\"/kISDh\":[\"Ativar sons de notificação\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Áudio\"],\"/rfkZe\":[\"Reproduzir sons para menções e mensagens\"],\"0/0ZGA\":[\"Máscara do nome do canal\"],\"0D6j7U\":[\"Saiba mais sobre regras personalizadas →\"],\"0XsHcR\":[\"Expulsar Usuário\"],\"0ZpE//\":[\"Ordenar por usuários\"],\"0bEPwz\":[\"Definir como Ausente\"],\"0dGkPt\":[\"Expandir lista de canais\"],\"0gS7M5\":[\"Nome de exibição\"],\"0kS+M8\":[\"ExemploREDE\"],\"0rgoY7\":[\"Conectar apenas a servidores que você escolher\"],\"0wdd7X\":[\"Entrar\"],\"0wkVYx\":[\"Mensagens privadas\"],\"111uHX\":[\"Visualização do link\"],\"196EG4\":[\"Excluir Conversa Privada\"],\"1DSr1i\":[\"Registrar uma conta\"],\"1O/24y\":[\"Alternar Lista de Canais\"],\"1TNIig\":[\"Abrir\"],\"1VPJJ2\":[\"Aviso de Link Externo\"],\"1ZC/dv\":[\"Nenhuma menção ou mensagem não lida\"],\"1pO1zi\":[\"Nome do servidor é obrigatório\"],\"1uwfzQ\":[\"Ver Tópico do Canal\"],\"268g7c\":[\"Digite o nome de exibição\"],\"2CEOW6\":[\"Rede vinculada via bouncer soju\"],\"2F9+AZ\":[\"Nenhum tráfego IRC bruto capturado ainda. Tente conectar ou enviar uma mensagem.\"],\"2FOFq1\":[\"Operadores de servidor na rede podem potencialmente ler suas mensagens\"],\"2FYpfJ\":[\"Mais\"],\"2HF1Y2\":[[\"inviter\"],\" convidou \",[\"target\"],\" para entrar em \",[\"channel\"]],\"2I70QL\":[\"Ver informações do perfil do usuário\"],\"2QYdmE\":[\"Usuários:\"],\"2QpEjG\":[\"saiu\"],\"2YE223\":[\"Mensagem #\",[\"0\"],\" (Enter para nova linha, Shift+Enter para enviar)\"],\"2bimFY\":[\"Usar senha do servidor\"],\"2iTmdZ\":[\"Armazenamento local:\"],\"2odkwe\":[\"Estrito – Proteção mais agressiva\"],\"2uDhbA\":[\"Digite o nome de usuário para convidar\"],\"2ygf/L\":[\"← Voltar\"],\"2zEgxj\":[\"Pesquisar GIFs...\"],\"3RdPhl\":[\"Renomear Canal\"],\"3THokf\":[\"Usuário com voz\"],\"3TSz9S\":[\"Minimizar\"],\"3jBDvM\":[\"Nome de exibição do canal\"],\"3ryuFU\":[\"Relatórios de falha opcionais para melhorar o app\"],\"3uBF/8\":[\"Fechar visualizador\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Inserir nome da conta...\"],\"4/Rr0R\":[\"Convidar um usuário para o canal atual\"],\"4EZrJN\":[\"Regras\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Perfil de flood (+F)\"],\"4RZQRK\":[\"O que você está fazendo?\"],\"4hfTrB\":[\"Apelido\"],\"4n99LO\":[\"Já em \",[\"0\"]],\"4t6vMV\":[\"Mudar automaticamente para linha única em mensagens curtas\"],\"4vsHmf\":[\"Tempo (min)\"],\"4x/Axu\":[\"Seu bouncer ainda não possui redes. Adicione uma para começar.\"],\"5+INAX\":[\"Destacar mensagens que mencionam você\"],\"5R5Pv/\":[\"Nome Oper\"],\"678PKt\":[\"Nome da Rede\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Senha necessária para entrar no canal. Deixe vazio para remover a chave.\"],\"6HhMs3\":[\"Mensagem de saída\"],\"6V3Ea3\":[\"Copiado\"],\"6lGV3K\":[\"Mostrar menos\"],\"6yFOEi\":[\"Inserir senha do oper...\"],\"7+IHTZ\":[\"Nenhum arquivo escolhido\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (ex.: spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Enviar mensagem privada\"],\"7U1W7c\":[\"Muito Relaxado\"],\"7Y1YQj\":[\"Nome real:\"],\"7YHArF\":[\"— abrir no visualizador\"],\"7fjnVl\":[\"Pesquisar usuários...\"],\"7jL88x\":[\"Excluir esta mensagem? Esta ação não pode ser desfeita.\"],\"7nGhhM\":[\"O que você está pensando?\"],\"7sEpu1\":[\"Membros — \",[\"0\"]],\"7sNhEz\":[\"Nome de usuário\"],\"8H0Q+x\":[\"Saiba mais sobre perfis →\"],\"8Phu0A\":[\"Exibir quando usuários mudam seu apelido\"],\"8XTG9e\":[\"Digite a senha oper\"],\"8XsV2J\":[\"Tentar enviar novamente\"],\"8ZsakT\":[\"Senha\"],\"8kR84m\":[\"Você está prestes a abrir um link externo:\"],\"8lCgih\":[\"Remover regra\"],\"8o3dPc\":[\"Solte os arquivos para enviar\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"entrou\"],\"other\":[\"entrou \",[\"joinCount\"],\" vezes\"]}]],\"9BMLnJ\":[\"Reconectar ao servidor\"],\"9OEgyT\":[\"Adicionar reação\"],\"9PQ8m2\":[\"G-Line (ban global)\"],\"9Qs99X\":[\"E-mail:\"],\"9QupBP\":[\"Remover padrão\"],\"9W7tl5\":[\"(inalterado)\"],\"9bG48P\":[\"Enviando\"],\"9f5f0u\":[\"Dúvidas sobre privacidade? Entre em contato:\"],\"9iweoP\":[\"Redes em \",[\"0\"]],\"9unqs3\":[\"Ausente:\"],\"9v3hwv\":[\"Nenhum servidor encontrado.\"],\"9zb2WA\":[\"Conectando\"],\"A1taO8\":[\"Pesquisar\"],\"A2adVi\":[\"Enviar notificações de digitação\"],\"A9Rhec\":[\"Nome do canal\"],\"AWOSPo\":[\"Ampliar\"],\"AXSpEQ\":[\"Oper ao conectar\"],\"AdKRCX\":[\"Você está conectado a <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Conta\"],\"AhNP40\":[\"Avançar\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Apelido alterado\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Cancelar resposta\"],\"ApSx0O\":[\"Encontradas \",[\"0\"],\" mensagens correspondentes a \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Nenhum resultado encontrado\"],\"AyNqAB\":[\"Exibir todos os eventos do servidor no chat\"],\"B/QqGw\":[\"Longe do teclado\"],\"B0sB2k\":[\"Texto puro\"],\"B8AaMI\":[\"Este campo é obrigatório\"],\"BA2c49\":[\"O servidor não suporta filtragem avançada de LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" e mais \",[\"3\"],\" estão digitando...\"],\"BGul2A\":[\"Você tem alterações não salvas. Tem certeza que deseja fechar sem salvar?\"],\"BIf9fi\":[\"Sua mensagem de status\"],\"BOJWfb\":[\"Desconectar do bouncer soju?\"],\"BPm98R\":[\"Nenhum servidor selecionado. Escolha primeiro um servidor na barra lateral; os links de convite são gerenciados por servidor.\"],\"BZz3md\":[\"Seu site pessoal\"],\"Bgm/H7\":[\"Permitir inserção de múltiplas linhas de texto\"],\"BiQIl1\":[\"Fixar esta conversa de mensagem privada\"],\"BlNZZ2\":[\"Clique para ir à mensagem\"],\"Bowq3c\":[\"Apenas operadores podem alterar o tópico\"],\"Btozzp\":[\"Esta imagem expirou\"],\"Bycfjm\":[\"Total: \",[\"0\"]],\"C6IBQc\":[\"Copiar JSON completo\"],\"C9L9wL\":[\"Coleta de dados\"],\"CDq4wC\":[\"Moderar Usuário\"],\"CHVRxG\":[\"Mensagem @\",[\"0\"],\" (Shift+Enter para nova linha)\"],\"CN9zdR\":[\"Nome oper e senha são obrigatórios\"],\"CW3sYa\":[\"Adicionar reação \",[\"emoji\"]],\"CaAkqd\":[\"Mostrar desconexões\"],\"CbvaYj\":[\"Banir por apelido\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Selecionar um canal\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"Sistema\"],\"D28t6+\":[\"entrou e saiu\"],\"DB8zMK\":[\"Aplicar\"],\"DBcWHr\":[\"Arquivo de som de notificação personalizado\"],\"DTy9Xw\":[\"Pré-visualizações de mídia\"],\"Dj4pSr\":[\"Escolha uma senha segura\"],\"Du+zn+\":[\"Pesquisando...\"],\"Du2T2f\":[\"Configuração não encontrada\"],\"DwsSVQ\":[\"Aplicar filtros e atualizar\"],\"E3W/zd\":[\"Apelido padrão\"],\"E6nRW7\":[\"Copiar URL\"],\"E703RG\":[\"Modos:\"],\"EAeu1Z\":[\"Enviar convite\"],\"EFKJQT\":[\"Configuração\"],\"EGPQBv\":[\"Regras de flood personalizadas (+f)\"],\"ELik0r\":[\"Ver política de privacidade completa\"],\"EPbeC2\":[\"Ver ou editar o tópico do canal\"],\"EQCDNT\":[\"Inserir nome de usuário oper...\"],\"EUvulZ\":[\"Encontrada 1 mensagem correspondente a \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Próxima imagem\"],\"EdQY6l\":[\"Nenhum\"],\"EnqLYU\":[\"Pesquisar servidores...\"],\"F0OKMc\":[\"Editar Servidor\"],\"F6Int2\":[\"Ativar destaques\"],\"FDoLyE\":[\"Usuários máx.\"],\"FUU/hZ\":[\"Controla quanto conteúdo de mídia externa é carregado no chat.\"],\"Fdp03t\":[\"ativo\"],\"FfPWR0\":[\"Modal\"],\"FjkaiT\":[\"Reduzir\"],\"FlqOE9\":[\"O que isso significa:\"],\"FolHNl\":[\"Gerenciar sua conta e autenticação\"],\"Fp2Dif\":[\"Saiu do servidor\"],\"G5KmCc\":[\"GZ-Line (Z-Line global)\"],\"GDs0lz\":[\"<0>Risco:0> Informações sensíveis (mensagens, conversas privadas, dados de autenticação) podem ser expostas a administradores de rede ou atacantes posicionados entre servidores IRC.\"],\"GR+2I3\":[\"Adicionar máscara de convite (ex.: nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Fechar avisos do servidor em destaque\"],\"GdhD7H\":[\"Clique novamente para confirmar\"],\"GjRZex\":[\"Desconectar rede?\"],\"GlHnXw\":[\"Falha ao alterar apelido: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Prévia:\"],\"GtmO8/\":[\"de\"],\"GtuHUQ\":[\"Renomear este canal no servidor. Todos os usuários verão o novo nome.\"],\"GuGfFX\":[\"Alternar pesquisa\"],\"GxkJXS\":[\"Enviando...\"],\"GzbwnK\":[\"Entrou no canal\"],\"GzsUDB\":[\"Perfil estendido\"],\"H/PnT8\":[\"Inserir emoji\"],\"H6Izzl\":[\"Seu código de cor preferido\"],\"H9jIv+\":[\"Mostrar entradas/saídas\"],\"HAKBY9\":[\"Enviar ficheiros\"],\"HdE1If\":[\"Canal\"],\"Hk4AW9\":[\"Seu nome de exibição preferido\"],\"HmHDk7\":[\"Selecionar Membro\"],\"HrQzPU\":[\"Canais em \",[\"networkName\"]],\"I2tXQ5\":[\"Mensagem @\",[\"0\"],\" (Enter para nova linha, Shift+Enter para enviar)\"],\"I6bw/h\":[\"Banir usuário\"],\"I92Z+b\":[\"Ativar notificações\"],\"I9D72S\":[\"Tem certeza que deseja excluir esta mensagem? Esta ação não pode ser desfeita.\"],\"IA+1wo\":[\"Exibir quando usuários são expulsos de canais\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Salvar Alterações\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" e \",[\"2\"],\" estão digitando...\"],\"IgrLD/\":[\"Pausar\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Responder\"],\"IoHMnl\":[\"O valor máximo é \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Conectando...\"],\"J5T9NW\":[\"Informações do usuário\"],\"J8Y5+z\":[\"Ops! Divisão de rede! ⚠️\"],\"JBHkBA\":[\"Saiu do canal\"],\"JCwL0Q\":[\"Digite o motivo (opcional)\"],\"JFciKP\":[\"Alternar\"],\"JXGkhG\":[\"Alterar o nome do canal (apenas operadores)\"],\"JcD7qf\":[\"Mais ações\"],\"JdkA+c\":[\"Secreto (+s)\"],\"Jmu12l\":[\"Canais do Servidor\"],\"JoQY+E\":[\"Isso remove a rede do seu bouncer soju. Para usá-la novamente, você precisará adicioná-la de volta.\"],\"JvQ++s\":[\"Ativar Markdown\"],\"K2jwh/\":[\"Nenhum dado WHOIS disponível\"],\"KAXSwC\":[\"Voz\"],\"KDfTdX\":[\"Excluir mensagem\"],\"KKBlUU\":[\"Incorporar\"],\"KM0pLb\":[\"Bem-vindo ao canal!\"],\"KR6W2h\":[\"Deixar de ignorar usuário\"],\"KV+Bi1\":[\"Apenas por convite (+i)\"],\"KdCtwE\":[\"Quantos segundos monitorar a atividade de flood antes de redefinir os contadores\"],\"Kkezga\":[\"Senha do Servidor\"],\"KsiQ/8\":[\"Os usuários devem ser convidados para entrar no canal\"],\"L+gB/D\":[\"Informações do canal\"],\"LC1a7n\":[\"O servidor IRC relatou que seus links servidor a servidor têm um nível de segurança baixo. Isso significa que quando suas mensagens são retransmitidas entre servidores IRC na rede, elas podem não estar devidamente criptografadas ou os certificados SSL/TLS podem não ser validados corretamente.\"],\"LEwpeL\":[\"bouncer soju (controle)\"],\"LNfLR5\":[\"Mostrar expulsões\"],\"LP+1Z7\":[\"Adicionar rede\"],\"LQb0W/\":[\"Mostrar todos os eventos\"],\"LU7/yA\":[\"Nome alternativo para exibição. Pode conter espaços, emojis e caracteres especiais. O nome real (\",[\"channelName\"],\") ainda será usado para comandos IRC.\"],\"LUb9O7\":[\"Porta de servidor válida é obrigatória\"],\"LV4fT6\":[\"Descrição (opcional, ex.: \\\"Beta testers Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Política de privacidade\"],\"LcuSDR\":[\"Gerenciar suas informações de perfil e metadados\"],\"LqLS9B\":[\"Mostrar mudanças de apelido\"],\"LsDQt2\":[\"Configurações do Canal\"],\"LtI9AS\":[\"Dono\"],\"LuNhhL\":[\"reagiu a esta mensagem\"],\"M/AZNG\":[\"URL da sua imagem de avatar\"],\"M/WIer\":[\"Enviar mensagem\"],\"M8er/5\":[\"Nome:\"],\"MHk+7g\":[\"Imagem anterior\"],\"MRorGe\":[\"Mensagem Privada\"],\"MVbSGP\":[\"Janela de tempo (segundos)\"],\"MkpcsT\":[\"Suas mensagens e configurações são armazenadas localmente\"],\"MzPdC2\":[\"Senha do servidor (PASS)\"],\"N/hDSy\":[\"Marcar como bot, geralmente 'on' ou vazio\"],\"N6j2JH\":[\"Editar \",[\"0\"]],\"N7TQbE\":[\"Convidar usuário para \",[\"channelName\"]],\"NCca/o\":[\"Inserir apelido padrão...\"],\"Nqs6B9\":[\"Exibe toda a mídia externa. Qualquer URL pode causar uma requisição a um servidor desconhecido.\"],\"Nt+9O7\":[\"Usar WebSocket em vez de TCP bruto\"],\"NxIHzc\":[\"Expulsar usuário\"],\"O+v/cL\":[\"Navegar por todos os canais do servidor\"],\"OCGpR4\":[\"(herdar)\"],\"ODwSCk\":[\"Enviar um GIF\"],\"OGQ5kK\":[\"Configurar sons de notificação e destaques\"],\"OIPt1Z\":[\"Mostrar ou ocultar a barra lateral de membros\"],\"OKSNq/\":[\"Muito Estrito\"],\"ONWvwQ\":[\"Carregar\"],\"OVKoQO\":[\"Sua senha de conta para autenticação\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Levando o IRC para o futuro\"],\"OhCpra\":[\"Definir um tópico…\"],\"OkltoQ\":[\"Banir \",[\"username\"],\" por apelido (impede que entre novamente com o mesmo nick)\"],\"P+t/Te\":[\"Sem dados adicionais\"],\"P42Wcc\":[\"Seguro\"],\"PD38l0\":[\"Visualização do avatar do canal\"],\"PD9mEt\":[\"Digite uma mensagem...\"],\"PPqfdA\":[\"Abrir configurações do canal\"],\"PSCjfZ\":[\"O tópico exibido para este canal. Todos os usuários podem ver.\"],\"PZCecv\":[\"Pré-visualização de PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 vez\"],\"other\":[[\"c\"],\" vezes\"]}]],\"PguS2C\":[\"Adicionar máscara de exceção (ex.: nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Mostrando \",[\"displayedChannelsCount\"],\" de \",[\"0\"],\" canais\"],\"PqhVlJ\":[\"Banir Usuário (por Hostmask)\"],\"Q+chwU\":[\"Nome de usuário:\"],\"Q2QY4/\":[\"Excluir este convite\"],\"Q3v9Wc\":[\"Sim, excluir\"],\"Q6hhn8\":[\"Preferências\"],\"QF4a34\":[\"Por favor, insira um nome de usuário\"],\"QGqSZ2\":[\"Cor e Formatação\"],\"QJQd1J\":[\"Editar perfil\"],\"QSzGDE\":[\"Ocioso\"],\"QUlny5\":[\"Bem-vindo ao \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Ler mais\"],\"QuSkCF\":[\"Filtrar canais...\"],\"QwUrDZ\":[\"alterou o tópico para: \",[\"topic\"]],\"R0UH07\":[\"Imagem \",[\"0\"],\" de \",[\"1\"]],\"R7SsBE\":[\"Silenciar\"],\"R8rf1X\":[\"Clique para definir o tópico\"],\"RArB3D\":[\"foi expulso de \",[\"channelName\"],\" por \",[\"username\"]],\"RI3cWd\":[\"Descubra o mundo do IRC com o ObsidianIRC\"],\"RIfHS5\":[\"Criar um novo link de convite\"],\"RMMaN5\":[\"Moderado (+m)\"],\"RWw9Lg\":[\"Fechar janela\"],\"RZ2BuZ\":[\"O registro da conta \",[\"account\"],\" requer verificação: \",[\"message\"]],\"RySp6q\":[\"Ocultar comentários\"],\"S5Togi\":[\"Carregando redes do seu bouncer…\"],\"SPKQTd\":[\"Apelido é obrigatório\"],\"SPVjfj\":[\"Será definido como 'sem motivo' se deixado em branco\"],\"SQKPvQ\":[\"Convidar Usuário\"],\"STmlpb\":[\"Voltar à lista de redes\"],\"SkZcl+\":[\"Escolha um perfil de proteção contra flood predefinido. Estes perfis fornecem configurações de proteção equilibradas para diferentes casos de uso.\"],\"Slr+3C\":[\"Usuários mín.\"],\"Spnlre\":[\"Você convidou \",[\"target\"],\" para entrar em \",[\"channel\"]],\"T/ckN5\":[\"Abrir no visualizador\"],\"T91vKp\":[\"Reproduzir\"],\"TV2Wdu\":[\"Saiba como tratamos seus dados e protegemos sua privacidade.\"],\"TgFpwD\":[\"Aplicando...\"],\"TkzSFB\":[\"Sem Alterações\"],\"TtserG\":[\"Digite o nome real\"],\"Ttz9J1\":[\"Inserir senha...\"],\"Tz0i8g\":[\"Configurações\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reagir\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Você ainda não criou nenhum link de convite. Use o formulário acima para criar o primeiro.\"],\"UGT5vp\":[\"Salvar configurações\"],\"UV5hLB\":[\"Nenhum banimento encontrado\"],\"Uaj3Nd\":[\"Mensagens de Status\"],\"Ue3uny\":[\"Padrão (sem perfil)\"],\"UkARhe\":[\"Normal – Proteção padrão\"],\"Umn7Cj\":[\"Ainda sem comentários. Seja o primeiro!\"],\"UtUIRh\":[[\"0\"],\" mensagens antigas\"],\"UwzP+U\":[\"Conexão segura\"],\"V0/A4O\":[\"Proprietário do canal\"],\"V0zZWc\":[\"Isso também fechará a rede vinculada abaixo.\"],\"V4qgxE\":[\"Criado antes (min atrás)\"],\"V8yTm6\":[\"Limpar pesquisa\"],\"VJMMyz\":[\"ObsidianIRC - Levando o IRC ao futuro\"],\"VJScHU\":[\"Motivo\"],\"VLsmVV\":[\"Silenciar notificações\"],\"VbyRUy\":[\"Comentários\"],\"Vmx0mQ\":[\"Definido por:\"],\"VqnIZz\":[\"Ver nossa política de privacidade e práticas de dados\"],\"VrMygG\":[\"O comprimento mínimo é \",[\"0\"]],\"VrnTui\":[\"Seus pronomes, exibidos no seu perfil\"],\"W8E3qn\":[\"Conta autenticada\"],\"WAakm9\":[\"Excluir Canal\"],\"WFxTHC\":[\"Adicionar máscara de banimento (ex.: nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Host do servidor é obrigatório\"],\"WRYdXW\":[\"Posição do áudio\"],\"WUOH5B\":[\"Ignorar usuário\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Mostrar 1 item a mais\"],\"other\":[\"Mostrar \",[\"1\"],\" itens a mais\"]}]],\"WYxRzo\":[\"Crie e gerencie seus links de convite\"],\"Wd38W1\":[\"Deixe o canal em branco para um convite genérico de rede. A descrição é apenas para seus registros â visÃvel somente para você nesta lista.\"],\"Weq9zb\":[\"Geral\"],\"Wfj7Sk\":[\"Silenciar ou ativar sons de notificação\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Perfil do Usuário\"],\"X6S3lt\":[\"Pesquisar configurações, canais, servidores...\"],\"XEHan5\":[\"Continuar Assim Mesmo\"],\"XI1+wb\":[\"Formato inválido\"],\"XIXeuC\":[\"Mensagem @\",[\"0\"]],\"XMS+k4\":[\"Iniciar Mensagem Privada\"],\"XWgxXq\":[\"Álbum\"],\"Xd7+IT\":[\"Desafixar Conversa Privada\"],\"Xm/s+u\":[\"Exibição\"],\"Xp2n93\":[\"Exibe mídia do host de arquivos confiável do seu servidor. Nenhuma requisição é feita a serviços externos.\"],\"XvjC4F\":[\"Salvando...\"],\"Y/qryO\":[\"Nenhum usuário encontrado para sua pesquisa\"],\"YAqRpI\":[\"Registro da conta \",[\"account\"],\" bem-sucedido: \",[\"message\"]],\"YEfzvP\":[\"Tópico protegido (+t)\"],\"YQOn6a\":[\"Recolher lista de membros\"],\"YRCoE9\":[\"Operador do canal\"],\"YURQaF\":[\"Ver perfil\"],\"YdBSvr\":[\"Controlar exibição de mídia e conteúdo externo\"],\"Yj6U3V\":[\"Sem servidor central:\"],\"YjvpGx\":[\"Pronomes\"],\"YqH4l4\":[\"Sem chave\"],\"YyUPpV\":[\"Conta:\"],\"ZJSWfw\":[\"Mensagem exibida ao desconectar do servidor\"],\"ZR1dJ4\":[\"Convites\"],\"ZdWg0V\":[\"Abrir no navegador\"],\"ZhRBbl\":[\"Pesquisar mensagens…\"],\"Zmcu3y\":[\"Filtros avançados\"],\"a0bHay\":[\"Desconectar <0>\",[\"0\"],\"0>?\"],\"a2/8e5\":[\"Tópico definido após (min atrás)\"],\"aHKcKc\":[\"Página anterior\"],\"aJTbXX\":[\"Senha Oper\"],\"aQryQv\":[\"O padrão já existe\"],\"aW9pLN\":[\"Número máximo de usuários permitidos. Deixe vazio para sem limite.\"],\"ah4fmZ\":[\"Também exibe visualizações do YouTube, Vimeo, SoundCloud e serviços conhecidos similares.\"],\"aifXak\":[\"Nenhuma mídia neste canal\"],\"ap2zBz\":[\"Relaxado\"],\"az8lvo\":[\"Desligado\"],\"azXSNo\":[\"Expandir lista de membros\"],\"azdliB\":[\"Entrar em uma conta\"],\"b26wlF\":[\"ela/dela\"],\"bD/+Ei\":[\"Estrito\"],\"bQ6BJn\":[\"Configure regras detalhadas de proteção contra flood. Cada regra especifica que tipo de atividade monitorar e que ação tomar quando os limites são excedidos.\"],\"beV7+y\":[\"O usuário receberá um convite para entrar em \",[\"channelName\"],\".\"],\"bk84cH\":[\"Mensagem de ausência\"],\"bkHdLj\":[\"Adicionar servidor IRC\"],\"bmQLn5\":[\"Adicionar regra\"],\"bv4cFj\":[\"Transporte\"],\"bwRvnp\":[\"Ação\"],\"c8+EVZ\":[\"Conta verificada\"],\"cGYUlD\":[\"Nenhuma visualização de mídia é carregada.\"],\"cLF98o\":[\"Mostrar comentários (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Nenhum usuário disponível\"],\"cSgpoS\":[\"Fixar Conversa Privada\"],\"cXeEKu\":[\"Isso também fechará as \",[\"0\"],\" redes vinculadas abaixo.\"],\"cde3ce\":[\"Mensagem <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Copiar saída formatada\"],\"cl/A5J\":[\"Bem-vindo ao \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Excluir\"],\"coPLXT\":[\"Não armazenamos suas comunicações IRC em nossos servidores\"],\"crYH/6\":[\"Player do SoundCloud\"],\"cv5DQb\":[\"nenhum host definido\"],\"d3sis4\":[\"Adicionar Servidor\"],\"d9aN5k\":[\"Remover \",[\"username\"],\" do canal\"],\"dEgA5A\":[\"Cancelar\"],\"dGi1We\":[\"Desafixar esta conversa de mensagem privada\"],\"dJVuyC\":[\"saiu de \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"para\"],\"dXqxlh\":[\"<0>⚠️ Risco de Segurança!0> Esta conexão pode ser vulnerável a interceptação ou ataques man-in-the-middle.\"],\"da9Q/R\":[\"Modos do canal alterados\"],\"dhJN3N\":[\"Mostrar comentários\"],\"dj2xTE\":[\"Dispensar notificação\"],\"dpCzmC\":[\"Configurações de proteção contra flood\"],\"e9dQpT\":[\"Deseja abrir este link em uma nova aba?\"],\"ePK91l\":[\"Editar\"],\"eYBDuB\":[\"Faça upload de uma imagem ou forneça uma URL com substituição opcional \",[\"size\"]],\"edBbee\":[\"Banir \",[\"username\"],\" por hostmask (impede que entre novamente pelo mesmo IP/host)\"],\"ekfzWq\":[\"Configurações do usuário\"],\"elPDWs\":[\"Personalize sua experiência no cliente IRC\"],\"eu2osY\":[\"<0>💡 Recomendação:0> Prossiga apenas se você confiar neste servidor e compreender os riscos. Evite compartilhar informações sensíveis ou senhas nesta conexão.\"],\"euEhbr\":[\"Clique para entrar em \",[\"channel\"]],\"ez3vLd\":[\"Ativar entrada multilinha\"],\"f0J5Ki\":[\"A comunicação servidor a servidor pode usar conexões não criptografadas\"],\"f9BHJk\":[\"Avisar Usuário\"],\"fDOLLd\":[\"Nenhum canal encontrado.\"],\"ffzDkB\":[\"Análises anônimas:\"],\"fq1GF9\":[\"Exibir quando usuários se desconectam do servidor\"],\"gCldcN\":[\"Alterar cor de destaque\"],\"gEF57C\":[\"Este servidor suporta apenas um tipo de conexão\"],\"gJuLUI\":[\"Lista de ignorados\"],\"gNzMrk\":[\"Avatar atual\"],\"gjPWyO\":[\"Inserir apelido...\"],\"gz6UQ3\":[\"Maximizar\"],\"h6/IMX\":[\"Adicione sua primeira rede\"],\"h6razj\":[\"Excluir máscara do nome do canal\"],\"hG6jnw\":[\"Nenhum tópico definido\"],\"hG89Ed\":[\"Imagem\"],\"hYgDIe\":[\"Criar\"],\"hZ6znB\":[\"Porta\"],\"ha+Bz5\":[\"ex.: 100:1440\"],\"he3ygx\":[\"Copiar\"],\"hehnjM\":[\"Quantidade\"],\"hzdLuQ\":[\"Apenas usuários com voz ou superior podem falar\"],\"i0qMbr\":[\"Início\"],\"iDNBZe\":[\"Notificações\"],\"iH8pgl\":[\"Voltar\"],\"iL9SZg\":[\"Banir Usuário (por Apelido)\"],\"iNt+3c\":[\"Voltar para a imagem\"],\"iQvi+a\":[\"Não me avisar sobre baixa segurança de link para este servidor\"],\"iSLIjg\":[\"Conectar\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Host do Servidor\"],\"idD8Ev\":[\"Salvo\"],\"iivqkW\":[\"Conectado em\"],\"ij+Elv\":[\"Visualização da imagem\"],\"ilIWp7\":[\"Alternar Notificações\"],\"iuaqvB\":[\"Use * para curingas. Exemplos: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Banir por máscara de host\"],\"jA4uoI\":[\"Tópico:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Motivo (opcional)\"],\"jUV7CU\":[\"Fazer upload do avatar\"],\"jW5Uwh\":[\"Controla quanto conteúdo de mídia externa é carregado. Desativado / Seguro / Fontes confiáveis / Todo conteúdo.\"],\"jXzms5\":[\"Opções de anexo\"],\"jZlrte\":[\"Cor\"],\"jfC/xh\":[\"Contato\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Carregar mensagens antigas\"],\"k3ID0F\":[\"Filtrar membros…\"],\"k65gsE\":[\"Mergulho profundo\"],\"k7Zgob\":[\"Cancelar Conexão\"],\"kAVx5h\":[\"Nenhum convite encontrado\"],\"kCLEPU\":[\"Conectado a\"],\"kF5LKb\":[\"Padrões ignorados:\"],\"kGeOx/\":[\"Entrar em \",[\"0\"]],\"kITKr8\":[\"Carregando modos do canal...\"],\"kPpPsw\":[\"Você é um IRC Operator\"],\"kWJmRL\":[\"Você\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Copiar JSON\"],\"krViRy\":[\"Clique para copiar como JSON\"],\"ks71ra\":[\"Exceções\"],\"kw4lRv\":[\"Meio operador do canal\"],\"kxgIRq\":[\"Selecione ou adicione um canal para começar.\"],\"ky6dWe\":[\"Visualização do avatar\"],\"l+GxCv\":[\"Carregando canais...\"],\"l+IUVW\":[\"Verificação da conta \",[\"account\"],\" bem-sucedida: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"reconectou\"],\"other\":[\"reconectou \",[\"reconnectCount\"],\" vezes\"]}]],\"l1l8sj\":[\"há \",[\"0\"],\"d\"],\"l5NhnV\":[\"#canal (opcional)\"],\"l5jmzx\":[[\"0\"],\" e \",[\"1\"],\" estão digitando...\"],\"lCF0wC\":[\"Atualizar\"],\"lHy8N5\":[\"Carregando mais canais...\"],\"lasgrr\":[\"usado\"],\"lbpf14\":[\"Entrar em \",[\"value\"]],\"lfFsZ4\":[\"Canais\"],\"lkNdiH\":[\"Nome da conta\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Enviar Imagem\"],\"loQxaJ\":[\"Estou de Volta\"],\"lvfaxv\":[\"INÍCIO\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Adicionar\"],\"m8flAk\":[\"Prévia (ainda não enviado)\"],\"mEPxTp\":[\"<0>⚠️ Cuidado!0> Abra apenas links de fontes confiáveis. Links maliciosos podem comprometer sua segurança ou privacidade.\"],\"mHGdhG\":[\"Informações do servidor\"],\"mHS8lb\":[\"Mensagem #\",[\"0\"]],\"mMYBD9\":[\"Amplo – Escopo de proteção mais amplo\"],\"mTGsPd\":[\"Tópico do canal\"],\"mU8j6O\":[\"Sem mensagens externas (+n)\"],\"mZp8FL\":[\"Retorno automático para linha única\"],\"mdQu8G\":[\"SeuApelido\"],\"miSSBQ\":[\"Comentários (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Usuário autenticado\"],\"mwtcGl\":[\"Fechar comentários\"],\"myL0MR\":[\"Excluir esta rede?\"],\"mzI/c+\":[\"Baixar\"],\"n3fGRk\":[\"definido por \",[\"0\"]],\"n5+j9l\":[\"ex. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Relaxado – Proteção menos agressiva\"],\"nNflMD\":[\"Sair do canal\"],\"nPXkBi\":[\"Carregando dados WHOIS...\"],\"nQnxxF\":[\"Mensagem #\",[\"0\"],\" (Shift+Enter para nova linha)\"],\"nWMRxa\":[\"Desafixar\"],\"nkC032\":[\"Sem perfil de flood\"],\"o69z4d\":[\"Enviar uma mensagem de aviso para \",[\"username\"]],\"o9ylQi\":[\"Pesquise GIFs para começar\"],\"oFGkER\":[\"Avisos do Servidor\"],\"oOi11l\":[\"Rolar para o fim\"],\"oPYIL5\":[\"rede\"],\"oQEzQR\":[\"Nova mensagem direta\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Ataques man-in-the-middle em links de servidor são possíveis\"],\"oeqmmJ\":[\"Fontes Confiáveis\"],\"optX0N\":[\"há \",[\"0\"],\"h\"],\"ovBPCi\":[\"Padrão\"],\"p0Z69r\":[\"O padrão não pode estar vazio\"],\"p1KgtK\":[\"Falha ao carregar áudio\"],\"p59pEv\":[\"Detalhes adicionais\"],\"p7sRI6\":[\"Avisar outros quando você está digitando\"],\"pBm1od\":[\"Canal secreto\"],\"pNmiXx\":[\"Seu apelido padrão para todos os servidores\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Senha da conta\"],\"peNE68\":[\"Permanente\"],\"plhHQt\":[\"Sem dados\"],\"pm6+q5\":[\"Aviso de Segurança\"],\"pn5qSs\":[\"Informações adicionais\"],\"q0cR4S\":[\"agora é conhecido como **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"O canal não aparecerá nos comandos LIST ou NAMES\"],\"qLpTm/\":[\"Remover reação \",[\"emoji\"]],\"qVkGWK\":[\"Fixar\"],\"qY8wNa\":[\"Página inicial\"],\"qb0xJ7\":[\"Curingas: * corresponde a qualquer sequência, ? a um único caractere. Exemplos: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Chave do canal (+k)\"],\"qtoOYG\":[\"Sem limite\"],\"r1W2AS\":[\"Imagem do servidor de arquivos\"],\"rIPR2O\":[\"Tópico definido antes (min atrás)\"],\"rMMSYo\":[\"O comprimento máximo é \",[\"0\"]],\"rWtzQe\":[\"A rede se dividiu e reconectou. ✅\"],\"rYG2u6\":[\"Aguarde...\"],\"rdUucN\":[\"Visualização\"],\"rjGI/Q\":[\"Privacidade\"],\"rk8iDX\":[\"Carregando GIFs...\"],\"rn6SBY\":[\"Ativar som\"],\"s/UKqq\":[\"Foi expulso do canal\"],\"s7oqXR\":[\"Selecione uma rede\"],\"s8cATI\":[\"entrou em \",[\"channelName\"]],\"sCO9ue\":[\"A conexão com <0>\",[\"serverName\"],\"0> apresenta as seguintes preocupações de segurança:\"],\"sGH11W\":[\"Servidor\"],\"sHI1H+\":[\"agora é conhecido como **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" convidou você para entrar em \",[\"channel\"]],\"sUBSbK\":[\"Nenhuma rede upstream ainda.\"],\"sby+1/\":[\"Clique para copiar\"],\"sfN25C\":[\"Seu nome real ou completo\"],\"sliuzR\":[\"Abrir Link\"],\"sqrO9R\":[\"Menções personalizadas\"],\"sr6RdJ\":[\"Multilinha com Shift+Enter\"],\"swrCpB\":[\"O canal foi renomeado de \",[\"oldName\"],\" para \",[\"newName\"],\" por \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Avançado\"],\"t/YqKh\":[\"Remover\"],\"t47eHD\":[\"Seu identificador único neste servidor\"],\"tAkAh0\":[\"URL com substituição opcional \",[\"size\"],\". Exemplo: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Mostrar ou ocultar a barra lateral de canais\"],\"tfDRzk\":[\"Salvar\"],\"tiBsJk\":[\"saiu de \",[\"channelName\"]],\"tt4/UD\":[\"saiu (\",[\"reason\"],\")\"],\"u0TcnO\":[\"O apelido {nick} já está em uso, tentando com {newNick}\"],\"u0a8B4\":[\"Autenticar como operador IRC para acesso administrativo\"],\"u0rWFU\":[\"Criado após (min atrás)\"],\"u72w3t\":[\"Usuários e padrões a ignorar\"],\"u7jc2L\":[\"saiu\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Falha ao salvar: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"Servidores IRC:\"],\"ukyW4o\":[\"Seus links de convite\"],\"usSSr/\":[\"Nível de zoom\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Shift+Enter para novas linhas (Enter envia)\"],\"vERlcd\":[\"Perfil\"],\"vK0RL8\":[\"Sem tópico\"],\"vSJd18\":[\"Vídeo\"],\"vXIe7J\":[\"Idioma\"],\"vaHYxN\":[\"Nome real\"],\"vhjbKr\":[\"Ausente\"],\"w/nogd\":[[\"0\"],\" rede\",[\"1\"],\" — escolha uma para entrar\"],\"w4NYox\":[\"cliente \",[\"title\"]],\"w8xQRx\":[\"Valor inválido\"],\"wFjjxZ\":[\"foi expulso de \",[\"channelName\"],\" por \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nenhuma exceção de banimento encontrada\"],\"wPrGnM\":[\"Administrador do canal\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Exibir quando usuários entram ou saem de canais\"],\"whqZ9r\":[\"Palavras ou frases adicionais para destacar\"],\"wm7RV4\":[\"Som de notificação\"],\"wz/Yoq\":[\"Suas mensagens podem ser interceptadas ao serem retransmitidas entre servidores\"],\"x3+y8b\":[\"Esta é a quantidade de pessoas que se registraram por este link\"],\"xCJdfg\":[\"Limpar\"],\"xOTzt5\":[\"agora mesmo\"],\"xUHRTR\":[\"Autenticar automaticamente como operador ao conectar\"],\"xWHwwQ\":[\"Banimentos\"],\"xYilR2\":[\"Mídia\"],\"xbi8D6\":[\"Este servidor não suporta links de convite (a capability<0>obby.world/invitation0>não é anunciada). Você ainda pode conversar normalmente; este painel é para redes baseadas em obbyircd.\"],\"xceQrO\":[\"Apenas websockets seguros são suportados\"],\"xdtXa+\":[\"nome-do-canal\"],\"xfXC7q\":[\"Canais de texto\"],\"xlCYOE\":[\"Carregando mais mensagens...\"],\"xlhswE\":[\"O valor mínimo é \",[\"0\"]],\"xq97Ci\":[\"Adicionar uma palavra ou frase...\"],\"xuRqRq\":[\"Limite de clientes (+l)\"],\"xwF+7J\":[[\"0\"],\" está digitando...\"],\"y1eoq1\":[\"Copiar link\"],\"yJztBY\":[\"Excluir rede\"],\"yNeucF\":[\"Este servidor não suporta metadados de perfil estendidos (extensão IRCv3 METADATA). Campos como avatar, nome de exibição e status não estão disponíveis.\"],\"yPlrca\":[\"Avatar do canal\"],\"yQE2r9\":[\"Carregando\"],\"ySU+JY\":[\"seu@email.com\"],\"yTX1Rt\":[\"Nome de usuário Oper\"],\"yYOzWD\":[\"logs\"],\"yfx9Re\":[\"Senha de operador IRC\"],\"ygCKqB\":[\"Parar\"],\"ymDxJx\":[\"Nome de usuário de operador IRC\"],\"yrpRsQ\":[\"Ordenar por nome\"],\"yz7wBu\":[\"Fechar\"],\"zJw+jA\":[\"define modo: \",[\"0\"]],\"zbymaY\":[\"há \",[\"0\"],\"min\"],\"zebeLu\":[\"Digite o nome de usuário oper\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/pt/messages.po b/src/locales/pt/messages.po
index 181cb8ca..87c3c9e8 100644
--- a/src/locales/pt/messages.po
+++ b/src/locales/pt/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - Levando o IRC para o futuro"
msgid "— open in viewer"
msgstr "— abrir no visualizador"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(herdar)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(inalterado)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} e {1} estão digitando..."
msgid "{0} is typing..."
msgstr "{0} está digitando..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} rede{1} — escolha uma para entrar"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -69,17 +85,17 @@ msgstr "{0}, {1}, {2} e mais {3} estão digitando..."
#. placeholder {0}: Math.floor(secs / 86400)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}d ago"
-msgstr "há {0}d"
+msgstr "há {0}d"
#. placeholder {0}: Math.floor(secs / 3600)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}h ago"
-msgstr "há {0}h"
+msgstr "há {0}h"
#. placeholder {0}: Math.floor(secs / 60)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}m ago"
-msgstr "há {0}min"
+msgstr "há {0}min"
#: src/lib/eventGrouping.ts
msgid "{c, plural, one {1 time} other {{c} times}}"
@@ -205,6 +221,12 @@ msgstr "Adicionar máscara de convite (ex.: nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "Adicionar servidor IRC"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Adicionar rede"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Adicionar regra"
msgid "Add Server"
msgstr "Adicionar Servidor"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Adicione sua primeira rede"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Detalhes adicionais"
@@ -377,6 +403,10 @@ msgstr "Voltar"
msgid "Back to image"
msgstr "Voltar para a imagem"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Voltar à lista de redes"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "Banir {username} por hostmask (impede que entre novamente pelo mesmo IP/host)"
@@ -424,6 +454,10 @@ msgstr "Navegar por todos os canais do servidor"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Cancelar Conexão"
msgid "Cancel reply"
msgstr "Cancelar resposta"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Alterar cor de destaque"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Alterar o nome do canal (apenas operadores)"
@@ -606,6 +645,8 @@ msgstr "Limite de clientes (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -669,6 +710,7 @@ msgid "Confirm?"
msgstr "Confirmar?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Conectar"
@@ -814,6 +856,10 @@ msgstr "Excluir Canal"
msgid "Delete message"
msgstr "Excluir mensagem"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Excluir rede"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Excluir Conversa Privada"
@@ -826,15 +872,36 @@ msgstr "Excluir este convite"
msgid "Delete this message? This cannot be undone."
msgstr "Excluir esta mensagem? Esta ação não pode ser desfeita."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Excluir esta rede?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "Descrição (opcional, ex.: \"Beta testers Q3\")"
+msgstr "Descrição (opcional, ex.: \"Beta testers Q3\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Desconectar"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "Desconectar <0>{0}0>?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Desconectar do bouncer soju?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Desconectar rede?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Descobrir"
@@ -893,18 +960,29 @@ msgstr "Baixar"
msgid "Drop files to upload"
msgstr "Solte os arquivos para enviar"
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "ex. <0>wss://host:port/socket0>"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "ex.: 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Editar"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "Editar {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Editar perfil"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "INÍCIO"
msgid "Homepage"
msgstr "Página inicial"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Host"
@@ -1323,7 +1402,7 @@ msgstr "Sair do canal"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Deixe o canal em branco para um convite genérico de rede. A descrição é apenas para seus registros — visível somente para você nesta lista."
+msgstr "Deixe o canal em branco para um convite genérico de rede. A descrição é apenas para seus registros â visÃvel somente para você nesta lista."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "Saiu do canal"
msgid "Let others know when you are typing"
msgstr "Avisar outros quando você está digitando"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Visualização do link"
@@ -1375,6 +1458,10 @@ msgstr "Carregando GIFs..."
msgid "Loading more channels..."
msgstr "Carregando mais canais..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Carregando redes do seu bouncer…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "Carregando dados WHOIS..."
@@ -1565,10 +1652,21 @@ msgstr "Nome:"
msgid "network"
msgstr "rede"
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Rede vinculada via bouncer soju"
+
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Nome da Rede"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Redes em {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Nova mensagem direta"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (ex.: spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Nenhum arquivo escolhido"
msgid "No flood profile"
msgstr "Sem perfil de flood"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "nenhum host definido"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Nenhum convite encontrado"
@@ -1672,7 +1775,7 @@ msgstr "Nenhuma visualização de mídia é carregada."
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "Nenhum tráfego IRC bruto capturado ainda. Tente conectar ou enviar uma mensagem."
+msgstr "Nenhum tráfego IRC bruto capturado ainda. Tente conectar ou enviar uma mensagem."
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "Nenhum resultado encontrado"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "Nenhum servidor selecionado. Escolha primeiro um servidor na barra lateral; os links de convite são gerenciados por servidor."
+msgstr "Nenhum servidor selecionado. Escolha primeiro um servidor na barra lateral; os links de convite são gerenciados por servidor."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "Nenhum tópico definido"
msgid "No unread mentions or messages"
msgstr "Nenhuma menção ou mensagem não lida"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Nenhuma rede upstream ainda."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Nenhum usuário disponível"
@@ -1784,6 +1891,10 @@ msgstr "Ops! Divisão de rede! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Abrir"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Abrir configurações do canal"
@@ -1887,6 +1998,10 @@ msgstr "Fixar Conversa Privada"
msgid "Pin this private message conversation"
msgstr "Fixar esta conversa de mensagem privada"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Texto puro"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "Mensagem Privada"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Porta"
@@ -2006,6 +2122,7 @@ msgstr "reagiu a esta mensagem"
msgid "Read more"
msgstr "Ler mais"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Motivo"
msgid "Reason (optional)"
msgstr "Motivo (opcional)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Reconectar ao servidor"
@@ -2095,6 +2213,7 @@ msgstr "Regras"
msgid "Safe"
msgstr "Seguro"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Avançar"
msgid "Select a channel"
msgstr "Selecionar um canal"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Selecione uma rede"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Selecionar Membro"
@@ -2276,6 +2399,10 @@ msgstr "Operadores de servidor na rede podem potencialmente ler suas mensagens"
msgid "Server Password"
msgstr "Senha do Servidor"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Senha do servidor (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "A comunicação servidor a servidor pode usar conexões não criptografadas"
@@ -2380,6 +2507,12 @@ msgstr "Conectado em"
msgid "Software:"
msgstr "Software:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "bouncer soju (controle)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Ordenar por nome"
@@ -2457,7 +2590,11 @@ msgstr "Esta imagem expirou"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "Esta é a quantidade de pessoas que se registraram por este link"
+msgstr "Esta é a quantidade de pessoas que se registraram por este link"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Isso remove a rede do seu bouncer soju. Para usá-la novamente, você precisará adicioná-la de volta."
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "Este servidor não suporta metadados de perfil estendidos (extensão IRC
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Este servidor não suporta links de convite (a capability<0>obby.world/invitation0>não é anunciada). Você ainda pode conversar normalmente; este painel é para redes baseadas em obbyircd."
+msgstr "Este servidor não suporta links de convite (a capability<0>obby.world/invitation0>não é anunciada). Você ainda pode conversar normalmente; este painel é para redes baseadas em obbyircd."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Este servidor suporta apenas um tipo de conexão"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Isso também fechará as {0} redes vinculadas abaixo."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Isso também fechará a rede vinculada abaixo."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Tempo (min)"
@@ -2479,6 +2625,10 @@ msgstr "Tempo (min)"
msgid "Time Window (seconds)"
msgstr "Janela de tempo (segundos)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Tópico:"
msgid "Total: {0}"
msgstr "Total: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Transporte"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Fontes Confiáveis"
@@ -2641,6 +2795,7 @@ msgstr "Perfil do Usuário"
msgid "User Settings"
msgstr "Configurações do usuário"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Amplo – Escopo de proteção mais amplo"
msgid "Will default to 'no reason' if left empty"
msgstr "Será definido como 'sem motivo' se deixado em branco"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Sim, excluir"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "Você tem alterações não salvas. Tem certeza que deseja fechar sem sa
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "Você ainda não criou nenhum link de convite. Use o formulário acima para criar o primeiro."
+msgstr "Você ainda não criou nenhum link de convite. Use o formulário acima para criar o primeiro."
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "Você convidou {target} para entrar em {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Você está conectado a <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Sua senha de conta para autenticação"
@@ -2822,6 +2986,10 @@ msgstr "Sua senha de conta para autenticação"
msgid "Your account username for authentication"
msgstr "Seu nome de usuário de conta para autenticação"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Seu bouncer ainda não possui redes. Adicione uma para começar."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Seu apelido padrão para todos os servidores"
diff --git a/src/locales/ro/messages.mjs b/src/locales/ro/messages.mjs
index 675078c6..ce5c86b6 100644
--- a/src/locales/ro/messages.mjs
+++ b/src/locales/ro/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Format model invalid. Folosiți formatul nick!user@host (caractere wildcard * permise)\"],\"+6NQQA\":[\"Canal general de suport\"],\"+6NyRG\":[\"Client\"],\"+K0AvT\":[\"Deconectează\"],\"+cyFdH\":[\"Mesaj implicit când vă marcați ca absent\"],\"+mVPqU\":[\"Afișați formatarea Markdown în mesaje\"],\"+vqCJH\":[\"Numele de utilizator al contului dvs. pentru autentificare\"],\"+yPBXI\":[\"Alege fișier\"],\"+zy2Nq\":[\"Tip\"],\"/09cao\":[\"Securitate scăzută a legăturii (Nivel \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Utilizatorii din afara canalului nu pot trimite mesaje\"],\"/4C8U0\":[\"Copiază tot\"],\"/6BzZF\":[\"Comută lista de membri\"],\"/AkXyp\":[\"Confirmați?\"],\"/TNOPk\":[\"Utilizatorul este absent\"],\"/XQgft\":[\"Descoperă\"],\"/cF7Rs\":[\"Volum\"],\"/dqduX\":[\"Pagina următoare\"],\"/fc3q4\":[\"Tot conținutul\"],\"/kISDh\":[\"Activați sunetele de notificare\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Redați sunete pentru mențiuni și mesaje\"],\"0/0ZGA\":[\"Mască nume canal\"],\"0D6j7U\":[\"Aflați mai multe despre regulile personalizate →\"],\"0XsHcR\":[\"Dă afară utilizatorul\"],\"0ZpE//\":[\"Sortare după utilizatori\"],\"0bEPwz\":[\"Setează ca absent\"],\"0dGkPt\":[\"Extinde lista de canale\"],\"0gS7M5\":[\"Nume afișat\"],\"0kS+M8\":[\"ExempluRET\"],\"0rgoY7\":[\"Conectați-vă doar la serverele pe care le alegeți\"],\"0wdd7X\":[\"Alătură-te\"],\"0wkVYx\":[\"Mesaje private\"],\"111uHX\":[\"Previzualizare link\"],\"196EG4\":[\"Șterge conversația privată\"],\"1DSr1i\":[\"Înregistrează un cont\"],\"1O/24y\":[\"Comută lista de canale\"],\"1VPJJ2\":[\"Avertisment link extern\"],\"1ZC/dv\":[\"Nicio mențiune sau mesaj necitit\"],\"1pO1zi\":[\"Numele serverului este obligatoriu\"],\"1uwfzQ\":[\"Vezi subiectul canalului\"],\"268g7c\":[\"Introdu numele afișat\"],\"2F9+AZ\":[\"Niciun trafic IRC brut capturat încă. Încearcă să te conectezi sau să trimiți un mesaj.\"],\"2FOFq1\":[\"Operatorii de server din rețea pot citi mesajele tale\"],\"2FYpfJ\":[\"Mai mult\"],\"2HF1Y2\":[[\"inviter\"],\" l-a invitat pe \",[\"target\"],\" să se alăture la \",[\"channel\"]],\"2I70QL\":[\"Vezi informațiile profilului utilizatorului\"],\"2QYdmE\":[\"Utilizatori:\"],\"2QpEjG\":[\"a ieșit\"],\"2YE223\":[\"Mesaj #\",[\"0\"],\" (Enter pentru linie nouă, Shift+Enter pentru trimitere)\"],\"2bimFY\":[\"Folosește parola serverului\"],\"2iTmdZ\":[\"Stocare locală:\"],\"2odkwe\":[\"Strict – Protecție mai agresivă\"],\"2uDhbA\":[\"Introdu numele de utilizator de invitat\"],\"2ygf/L\":[\"← Înapoi\"],\"2zEgxj\":[\"Caută GIF-uri...\"],\"3RdPhl\":[\"Redenumește canalul\"],\"3THokf\":[\"Utilizator cu drept de vorbire\"],\"3TSz9S\":[\"Minimizează\"],\"3jBDvM\":[\"Nume afișat canal\"],\"3ryuFU\":[\"Rapoarte opționale de erori pentru îmbunătățirea aplicației\"],\"3uBF/8\":[\"Închide vizualizatorul\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Introduceți numele contului...\"],\"4/Rr0R\":[\"Invită un utilizator în canalul curent\"],\"4EZrJN\":[\"Reguli\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Profil flood (+F)\"],\"4RZQRK\":[\"Ce faci?\"],\"4hfTrB\":[\"Poreclă\"],\"4n99LO\":[\"Deja în \",[\"0\"]],\"4t6vMV\":[\"Comutare automată la linie unică pentru mesaje scurte\"],\"4vsHmf\":[\"Timp (min)\"],\"5+INAX\":[\"Evidențiați mesajele care vă menționează\"],\"5R5Pv/\":[\"Nume oper\"],\"678PKt\":[\"Nume rețea\"],\"6Aih4U\":[\"Deconectat\"],\"6CO3WE\":[\"Parolă necesară pentru a intra în canal. Lăsați gol pentru a elimina cheia.\"],\"6HhMs3\":[\"Mesaj de ieșire\"],\"6V3Ea3\":[\"Copiat\"],\"6lGV3K\":[\"Arată mai puțin\"],\"6yFOEi\":[\"Introduceți parola oper...\"],\"7+IHTZ\":[\"Niciun fișier ales\"],\"73hrRi\":[\"nick!user@host (ex., spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Trimite mesaj privat\"],\"7U1W7c\":[\"Foarte relaxat\"],\"7Y1YQj\":[\"Nume real:\"],\"7YHArF\":[\"— deschide în vizualizator\"],\"7fjnVl\":[\"Caută utilizatori...\"],\"7jL88x\":[\"Ștergeți acest mesaj? Această acțiune nu poate fi anulată.\"],\"7nGhhM\":[\"La ce te gândești?\"],\"7sEpu1\":[\"Membri — \",[\"0\"]],\"7sNhEz\":[\"Nume de utilizator\"],\"8H0Q+x\":[\"Aflați mai multe despre profiluri →\"],\"8Phu0A\":[\"Afișează când utilizatorii își schimbă pseudonimul\"],\"8XTG9e\":[\"Introdu parola oper\"],\"8XsV2J\":[\"Reîncearcă trimiterea\"],\"8ZsakT\":[\"Parolă\"],\"8kR84m\":[\"Ești pe cale să deschizi un link extern:\"],\"8lCgih\":[\"Eliminați regula\"],\"8o3dPc\":[\"Plasează fișierele pentru a le încărca\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"s-a alăturat\"],\"few\":[\"s-a alăturat de \",[\"joinCount\"],\" ori\"],\"other\":[\"s-a alăturat de \",[\"joinCount\"],\" ori\"]}]],\"9BMLnJ\":[\"Reconectează-te la server\"],\"9OEgyT\":[\"Adaugă reacție\"],\"9PQ8m2\":[\"G-Line (ban global)\"],\"9Qs99X\":[\"Email:\"],\"9QupBP\":[\"Elimină șablon\"],\"9bG48P\":[\"Se trimite\"],\"9f5f0u\":[\"Întrebări despre confidențialitate? Contactați-ne:\"],\"9unqs3\":[\"Absent:\"],\"9v3hwv\":[\"Niciun server găsit.\"],\"9zb2WA\":[\"Se conectează\"],\"A1taO8\":[\"Caută\"],\"A2adVi\":[\"Trimiteți notificări de tastare\"],\"A9Rhec\":[\"Nume canal\"],\"AWOSPo\":[\"Mărește\"],\"AXSpEQ\":[\"Oper la conectare\"],\"AeXO77\":[\"Cont\"],\"AhNP40\":[\"Derulare\"],\"Ai2U7L\":[\"Gazdă\"],\"AjBQnf\":[\"Poreclă schimbată\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Anulează răspunsul\"],\"ApSx0O\":[\"S-au găsit \",[\"0\"],\" mesaje care corespund cu \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Niciun rezultat găsit\"],\"AyNqAB\":[\"Afișează toate evenimentele serverului în chat\"],\"B/QqGw\":[\"Departe de tastatură\"],\"B8AaMI\":[\"Acest câmp este obligatoriu\"],\"BA2c49\":[\"Serverul nu acceptă filtrarea avansată LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" și alți \",[\"3\"],\" scriu...\"],\"BGul2A\":[\"Ai modificări nesalvate. Ești sigur că vrei să închizi fără să salvezi?\"],\"BIf9fi\":[\"Mesajul dvs. de stare\"],\"BPm98R\":[\"Niciun server nu este selectat. Alege mai întâi un server din bara laterală; linkurile de invitație sunt gestionate per server.\"],\"BZz3md\":[\"Site-ul dvs. web personal\"],\"Bgm/H7\":[\"Permite introducerea mai multor rânduri de text\"],\"BiQIl1\":[\"Fixează această conversație privată\"],\"BlNZZ2\":[\"Click pentru a sări la mesaj\"],\"Bowq3c\":[\"Doar operatorii pot schimba subiectul canalului\"],\"Btozzp\":[\"Această imagine a expirat\"],\"Bycfjm\":[\"Total: \",[\"0\"]],\"C6IBQc\":[\"Copiați JSON complet\"],\"C9L9wL\":[\"Colectare de date\"],\"CDq4wC\":[\"Moderează utilizatorul\"],\"CHVRxG\":[\"Mesaj @\",[\"0\"],\" (Shift+Enter pentru linie nouă)\"],\"CN9zdR\":[\"Numele oper și parola sunt obligatorii\"],\"CW3sYa\":[\"Adaugă reacția \",[\"emoji\"]],\"CaAkqd\":[\"Afișați deconectările\"],\"CbvaYj\":[\"Banare după poreclă\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Selectează un canal\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"Sistem\"],\"D28t6+\":[\"s-a alăturat și a ieșit\"],\"DB8zMK\":[\"Aplică\"],\"DBcWHr\":[\"Fișier audio de notificare personalizat\"],\"DTy9Xw\":[\"Previzualizări media\"],\"Dj4pSr\":[\"Alege o parolă sigură\"],\"Du+zn+\":[\"Se caută...\"],\"Du2T2f\":[\"Setarea nu a fost găsită\"],\"DwsSVQ\":[\"Aplică filtrele și actualizează\"],\"E3W/zd\":[\"Pseudonim implicit\"],\"E6nRW7\":[\"Copiază URL\"],\"E703RG\":[\"Moduri:\"],\"EAeu1Z\":[\"Trimiteți invitația\"],\"EFKJQT\":[\"Setare\"],\"EGPQBv\":[\"Reguli flood personalizate (+f)\"],\"ELik0r\":[\"Vizualizați politica completă de confidențialitate\"],\"EPbeC2\":[\"Vezi sau editează subiectul canalului\"],\"EQCDNT\":[\"Introduceți numele de utilizator oper...\"],\"EUvulZ\":[\"S-a găsit 1 mesaj care corespunde cu \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Imaginea următoare\"],\"EdQY6l\":[\"Niciunul\"],\"EnqLYU\":[\"Caută servere...\"],\"F0OKMc\":[\"Editează server\"],\"F6Int2\":[\"Activați evidențierile\"],\"FDoLyE\":[\"Utilizatori max.\"],\"FUU/hZ\":[\"Controlează câte conținuturi media externe sunt încărcate în chat.\"],\"Fdp03t\":[\"activ\"],\"FfPWR0\":[\"Modal\"],\"FjkaiT\":[\"Micșorează\"],\"FlqOE9\":[\"Ce înseamnă aceasta:\"],\"FolHNl\":[\"Gestionează-ți contul și autentificarea\"],\"Fp2Dif\":[\"A ieșit de pe server\"],\"G5KmCc\":[\"GZ-Line (Z-Line globală)\"],\"GDs0lz\":[\"<0>Risc:0> Informațiile sensibile (mesaje, conversații private, date de autentificare) pot fi expuse administratorilor de rețea sau atacatorilor poziționați între serverele IRC.\"],\"GR+2I3\":[\"Adaugă mască de invitație (ex. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Închide notificările server detașate\"],\"GdhD7H\":[\"Faceți clic din nou pentru a confirma\"],\"GlHnXw\":[\"Schimbarea poreclelei a eșuat: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Previzualizare:\"],\"GtmO8/\":[\"de la\"],\"GtuHUQ\":[\"Redenumiți acest canal pe server. Toți utilizatorii vor vedea noul nume.\"],\"GuGfFX\":[\"Comută căutarea\"],\"GxkJXS\":[\"Se încarcă...\"],\"GzbwnK\":[\"S-a alăturat canalului\"],\"GzsUDB\":[\"Profil extins\"],\"H/PnT8\":[\"Inserează emoji\"],\"H6Izzl\":[\"Codul dvs. de culoare preferat\"],\"H9jIv+\":[\"Afișați intrări/ieșiri\"],\"HAKBY9\":[\"Încărcați fișiere\"],\"HdE1If\":[\"Canal\"],\"Hk4AW9\":[\"Numele dvs. de afișare preferat\"],\"HmHDk7\":[\"Selectează un membru\"],\"HrQzPU\":[\"Canale pe \",[\"networkName\"]],\"I2tXQ5\":[\"Mesaj @\",[\"0\"],\" (Enter pentru linie nouă, Shift+Enter pentru trimitere)\"],\"I6bw/h\":[\"Banează utilizatorul\"],\"I92Z+b\":[\"Activează notificările\"],\"I9D72S\":[\"Sigur doriți să ștergeți acest mesaj? Această acțiune nu poate fi anulată.\"],\"IA+1wo\":[\"Afișează când utilizatorii sunt expulzați din canale\"],\"IDwkJx\":[\"Operator IRC\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Salvează modificările\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" și \",[\"2\"],\" scriu...\"],\"IgrLD/\":[\"Pauză\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Răspunde\"],\"IoHMnl\":[\"Valoarea maximă este \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Se conectează...\"],\"J5T9NW\":[\"Informații utilizator\"],\"J8Y5+z\":[\"Oops! Rețeaua s-a împărțit! ⚠️\"],\"JBHkBA\":[\"A părăsit canalul\"],\"JCwL0Q\":[\"Introdu motivul (opțional)\"],\"JFciKP\":[\"Comută\"],\"JXGkhG\":[\"Schimbă numele canalului (numai operatori)\"],\"JcD7qf\":[\"Mai multe acțiuni\"],\"JdkA+c\":[\"Secret (+s)\"],\"Jmu12l\":[\"Canale server\"],\"JvQ++s\":[\"Activați Markdown\"],\"K2jwh/\":[\"Nu există date WHOIS disponibile\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Șterge mesaj\"],\"KKBlUU\":[\"Încorporare\"],\"KM0pLb\":[\"Bine ai venit în canal!\"],\"KR6W2h\":[\"Nu mai ignora utilizatorul\"],\"KV+Bi1\":[\"Doar pe invitație (+i)\"],\"KdCtwE\":[\"Câte secunde se monitorizează activitatea flood înainte de resetarea contoarelor\"],\"Kkezga\":[\"Parolă server\"],\"KsiQ/8\":[\"Utilizatorii trebuie invitați pentru a intra în canal\"],\"L+gB/D\":[\"Informații canal\"],\"LC1a7n\":[\"Serverul IRC a raportat că legăturile sale server-la-server au un nivel scăzut de securitate. Aceasta înseamnă că atunci când mesajele tale sunt transmise între serverele IRC din rețea, este posibil ca acestea să nu fie criptate corespunzător sau certificatele SSL/TLS să nu fie validate corect.\"],\"LNfLR5\":[\"Afișați expulzările\"],\"LQb0W/\":[\"Afișați toate evenimentele\"],\"LU7/yA\":[\"Nume alternativ pentru afișaj. Poate conține spații, emoji și caractere speciale. Numele real (\",[\"channelName\"],\") va fi folosit în continuare pentru comenzile IRC.\"],\"LUb9O7\":[\"Este necesar un port de server valid\"],\"LV4fT6\":[\"Descriere (opțional, ex. „Testeri beta T3”)\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Politică de confidențialitate\"],\"LcuSDR\":[\"Gestionează informațiile profilului și metadatele\"],\"LqLS9B\":[\"Afișați schimbările de pseudonim\"],\"LsDQt2\":[\"Setări canal\"],\"LtI9AS\":[\"Proprietar\"],\"LuNhhL\":[\"a reacționat la acest mesaj\"],\"M/AZNG\":[\"URL-ul imaginii dvs. de avatar\"],\"M/WIer\":[\"Trimite mesaj\"],\"M8er/5\":[\"Nume:\"],\"MHk+7g\":[\"Imaginea anterioară\"],\"MRorGe\":[\"Mesaj privat\"],\"MVbSGP\":[\"Fereastră de timp (secunde)\"],\"MkpcsT\":[\"Mesajele și setările dvs. sunt stocate local pe dispozitivul dvs.\"],\"N/hDSy\":[\"Marcați ca bot, de obicei 'on' sau gol\"],\"N7TQbE\":[\"Invitați utilizatorul în \",[\"channelName\"]],\"NCca/o\":[\"Introduceți porecla implicită...\"],\"Nqs6B9\":[\"Afișează toate conținuturile externe. Orice URL poate genera o solicitare către un server necunoscut.\"],\"Nt+9O7\":[\"Utilizați WebSocket în loc de TCP brut\"],\"NxIHzc\":[\"Expulzați utilizatorul\"],\"O+v/cL\":[\"Răsfoiește toate canalele de pe server\"],\"ODwSCk\":[\"Trimite un GIF\"],\"OGQ5kK\":[\"Configurează sunetele de notificare și evidențierile\"],\"OIPt1Z\":[\"Afișează sau ascunde bara laterală cu lista de membri\"],\"OKSNq/\":[\"Foarte strict\"],\"ONWvwQ\":[\"Încărcați\"],\"OVKoQO\":[\"Parola contului dvs. pentru autentificare\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Aducem IRC în viitor\"],\"OhCpra\":[\"Setează un subiect…\"],\"OkltoQ\":[\"Banează \",[\"username\"],\" după poreclă (împiedică reconectarea cu același nick)\"],\"P+t/Te\":[\"Nicio dată suplimentară\"],\"P42Wcc\":[\"Sigur\"],\"PD38l0\":[\"Previzualizare avatar canal\"],\"PD9mEt\":[\"Scrie un mesaj...\"],\"PPqfdA\":[\"Deschide setările de configurare ale canalului\"],\"PSCjfZ\":[\"Subiectul afișat pentru acest canal. Toți utilizatorii îl pot vedea.\"],\"PZCecv\":[\"Previzualizare PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 dată\"],\"few\":[[\"c\"],\" ori\"],\"other\":[[\"c\"],\" ori\"]}]],\"PguS2C\":[\"Adaugă mască de excepție (ex. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Se afișează \",[\"displayedChannelsCount\"],\" din \",[\"0\"],\" canale\"],\"PqhVlJ\":[\"Banează utilizator (după hostmask)\"],\"Q+chwU\":[\"Nume utilizator:\"],\"Q2QY4/\":[\"Șterge această invitație\"],\"Q6hhn8\":[\"Preferințe\"],\"QF4a34\":[\"Introduceți un nume de utilizator\"],\"QGqSZ2\":[\"Culoare și formatare\"],\"QJQd1J\":[\"Editați profilul\"],\"QSzGDE\":[\"Inactiv\"],\"QUlny5\":[\"Bine ai venit la \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Citește mai mult\"],\"QuSkCF\":[\"Filtrează canale...\"],\"QwUrDZ\":[\"a schimbat subiectul la: \",[\"topic\"]],\"R0UH07\":[\"Imaginea \",[\"0\"],\" din \",[\"1\"]],\"R7SsBE\":[\"Dezactivare sunet\"],\"R8rf1X\":[\"Click pentru a seta subiectul\"],\"RArB3D\":[\"a fost dat afară din \",[\"channelName\"],\" de \",[\"username\"]],\"RI3cWd\":[\"Descoperă lumea IRC cu ObsidianIRC\"],\"RIfHS5\":[\"Creează un nou link de invitație\"],\"RMMaN5\":[\"Moderat (+m)\"],\"RWw9Lg\":[\"Închide fereastra\"],\"RZ2BuZ\":[\"Înregistrarea contului \",[\"account\"],\" necesită verificare: \",[\"message\"]],\"RySp6q\":[\"Ascundeți comentariile\"],\"SPKQTd\":[\"Porecla este obligatorie\"],\"SPVjfj\":[\"Va fi implicit „niciun motiv\\\" dacă este lăsat gol\"],\"SQKPvQ\":[\"Invită utilizator\"],\"SkZcl+\":[\"Alegeți un profil de protecție flood predefinit. Aceste profiluri oferă setări de protecție echilibrate pentru diferite cazuri de utilizare.\"],\"Slr+3C\":[\"Utilizatori min.\"],\"Spnlre\":[\"L-ai invitat pe \",[\"target\"],\" să se alăture la \",[\"channel\"]],\"T/ckN5\":[\"Deschide în vizualizator\"],\"T91vKp\":[\"Redare\"],\"TV2Wdu\":[\"Aflați cum gestionăm datele dvs. și vă protejăm confidențialitatea.\"],\"TgFpwD\":[\"Se aplică...\"],\"TkzSFB\":[\"Nicio modificare\"],\"TtserG\":[\"Introdu numele real\"],\"Ttz9J1\":[\"Introduceți parola...\"],\"Tz0i8g\":[\"Setări\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reacționează\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Nu ai creat încă niciun link de invitație. Folosește formularul de mai sus pentru a-l crea pe primul.\"],\"UGT5vp\":[\"Salvați setările\"],\"UV5hLB\":[\"Nu s-au găsit banuri\"],\"Uaj3Nd\":[\"Mesaje de stare\"],\"Ue3uny\":[\"Implicit (fără profil)\"],\"UkARhe\":[\"Normal – Protecție standard\"],\"Umn7Cj\":[\"Niciun comentariu. Fiți primul!\"],\"UtUIRh\":[[\"0\"],\" mesaje mai vechi\"],\"UwzP+U\":[\"Conexiune securizată\"],\"V0/A4O\":[\"Proprietar de canal\"],\"V4qgxE\":[\"Creat înainte (min în urmă)\"],\"V8yTm6\":[\"Șterge căutarea\"],\"VJMMyz\":[\"ObsidianIRC - Aducând IRC în viitor\"],\"VJScHU\":[\"Motiv\"],\"VLsmVV\":[\"Dezactivează notificările\"],\"VbyRUy\":[\"Comentarii\"],\"Vmx0mQ\":[\"Setat de:\"],\"VqnIZz\":[\"Vezi politica noastră de confidențialitate și practicile privind datele\"],\"VrMygG\":[\"Lungimea minimă este \",[\"0\"]],\"VrnTui\":[\"Pronumele dvs., afișate în profil\"],\"W8E3qn\":[\"Cont autentificat\"],\"WAakm9\":[\"Șterge canal\"],\"WFxTHC\":[\"Adaugă mască de banare (ex. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Adresa serverului este obligatorie\"],\"WRYdXW\":[\"Poziție audio\"],\"WUOH5B\":[\"Ignoră utilizatorul\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Arată 1 element în plus\"],\"few\":[\"Arată \",[\"1\"],\" elemente în plus\"],\"other\":[\"Arată \",[\"1\"],\" de elemente în plus\"]}]],\"WYxRzo\":[\"Creează și administrează linkurile tale de invitație\"],\"Wd38W1\":[\"Lasă canalul gol pentru o invitație generică în rețea. Descrierea este doar pentru evidența ta — vizibilă doar pentru tine în această listă.\"],\"Weq9zb\":[\"General\"],\"Wfj7Sk\":[\"Activează sau dezactivează sunetele de notificare\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Profil utilizator\"],\"X6S3lt\":[\"Caută setări, canale, servere...\"],\"XEHan5\":[\"Continuă oricum\"],\"XI1+wb\":[\"Format invalid\"],\"XIXeuC\":[\"Mesaj @\",[\"0\"]],\"XMS+k4\":[\"Începe un mesaj privat\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Anulează fixarea conversației private\"],\"Xm/s+u\":[\"Afișare\"],\"Xp2n93\":[\"Afișează media de pe gazda de fișiere de încredere a serverului. Nu se fac solicitări către servicii externe.\"],\"XvjC4F\":[\"Se salvează...\"],\"Y/qryO\":[\"Niciun utilizator găsit care să corespundă căutării\"],\"YAqRpI\":[\"Înregistrarea contului \",[\"account\"],\" a reușit: \",[\"message\"]],\"YEfzvP\":[\"Subiect protejat (+t)\"],\"YQOn6a\":[\"Restrânge lista de membri\"],\"YRCoE9\":[\"Operator de canal\"],\"YURQaF\":[\"Vizualizați profilul\"],\"YdBSvr\":[\"Controlează afișarea media și conținutul extern\"],\"Yj6U3V\":[\"Fără server central:\"],\"YjvpGx\":[\"Pronume\"],\"YqH4l4\":[\"Nicio cheie\"],\"YyUPpV\":[\"Cont:\"],\"ZJSWfw\":[\"Mesaj afișat la deconectarea de la server\"],\"ZR1dJ4\":[\"Invitații\"],\"ZdWg0V\":[\"Deschide în browser\"],\"ZhRBbl\":[\"Caută mesaje…\"],\"Zmcu3y\":[\"Filtre avansate\"],\"a2/8e5\":[\"Subiect setat după (min în urmă)\"],\"aHKcKc\":[\"Pagina anterioară\"],\"aJTbXX\":[\"Parolă oper\"],\"aQryQv\":[\"Modelul există deja\"],\"aW9pLN\":[\"Numărul maxim de utilizatori permis în canal. Lăsați gol pentru fără limită.\"],\"ah4fmZ\":[\"Afișează și previzualizări de pe YouTube, Vimeo, SoundCloud și servicii similare cunoscute.\"],\"aifXak\":[\"Niciun fișier media în acest canal\"],\"ap2zBz\":[\"Relaxat\"],\"az8lvo\":[\"Oprit\"],\"azXSNo\":[\"Extinde lista de membri\"],\"azdliB\":[\"Conectează-te la un cont\"],\"b26wlF\":[\"ea/ei\"],\"bD/+Ei\":[\"Strict\"],\"bQ6BJn\":[\"Configurați reguli detaliate de protecție flood. Fiecare regulă specifică tipul de activitate de monitorizat și acțiunea de întreprins când pragurile sunt depășite.\"],\"beV7+y\":[\"Utilizatorul va primi o invitație să se alăture \",[\"channelName\"],\".\"],\"bk84cH\":[\"Mesaj de absență\"],\"bkHdLj\":[\"Adaugă server IRC\"],\"bmQLn5\":[\"Adaugă regulă\"],\"bwRvnp\":[\"Acțiune\"],\"c8+EVZ\":[\"Cont verificat\"],\"cGYUlD\":[\"Nu sunt încărcate previzualizări media.\"],\"cLF98o\":[\"Afișați comentariile (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Niciun utilizator disponibil\"],\"cSgpoS\":[\"Fixează conversația privată\"],\"cde3ce\":[\"Mesaj <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Copiați ieșirea formatată\"],\"cl/A5J\":[\"Bine ai venit la \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Șterge\"],\"coPLXT\":[\"Nu stocăm comunicațiile dvs. IRC pe serverele noastre\"],\"crYH/6\":[\"Player SoundCloud\"],\"d3sis4\":[\"Adaugă server\"],\"d9aN5k\":[\"Elimină \",[\"username\"],\" din canal\"],\"dEgA5A\":[\"Anulează\"],\"dGi1We\":[\"Anulează fixarea acestei conversații private\"],\"dJVuyC\":[\"a părăsit \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"către\"],\"dXqxlh\":[\"<0>⚠️ Risc de securitate!0> Această conexiune poate fi vulnerabilă la interceptare sau atacuri de tip man-in-the-middle.\"],\"da9Q/R\":[\"Moduri canal modificate\"],\"dhJN3N\":[\"Afișați comentariile\"],\"dj2xTE\":[\"Respinge notificarea\"],\"dpCzmC\":[\"Setări de protecție flood\"],\"e9dQpT\":[\"Dorești să deschizi acest link într-o filă nouă?\"],\"ePK91l\":[\"Editează\"],\"eYBDuB\":[\"Încărcați o imagine sau furnizați o adresă URL cu înlocuire opțională \",[\"size\"]],\"edBbee\":[\"Banează \",[\"username\"],\" după hostmask (împiedică reconectarea de pe același IP/host)\"],\"ekfzWq\":[\"Setări utilizator\"],\"elPDWs\":[\"Personalizează experiența clientului IRC\"],\"eu2osY\":[\"<0>💡 Recomandare:0> Continuați doar dacă aveți încredere în acest server și înțelegeți riscurile. Evitați să partajați informații sensibile sau parole prin această conexiune.\"],\"euEhbr\":[\"Faceți clic pentru a vă alătura \",[\"channel\"]],\"ez3vLd\":[\"Activați introducerea pe mai multe rânduri\"],\"f0J5Ki\":[\"Comunicarea server-la-server poate folosi conexiuni necriptate\"],\"f9BHJk\":[\"Avertizează utilizatorul\"],\"fDOLLd\":[\"Nu s-au găsit canale.\"],\"ffzDkB\":[\"Analize anonime:\"],\"fq1GF9\":[\"Afișează când utilizatorii se deconectează de la server\"],\"gEF57C\":[\"Acest server acceptă doar un tip de conexiune\"],\"gJuLUI\":[\"Listă de ignorați\"],\"gNzMrk\":[\"Avatar curent\"],\"gjPWyO\":[\"Introduceți porecla...\"],\"gz6UQ3\":[\"Maximizează\"],\"h6razj\":[\"Excludeți masca numelui canalului\"],\"hG6jnw\":[\"Niciun subiect setat\"],\"hG89Ed\":[\"Imagine\"],\"hYgDIe\":[\"Creează\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"ex., 100:1440\"],\"he3ygx\":[\"Copiază\"],\"hehnjM\":[\"Cantitate\"],\"hzdLuQ\":[\"Doar utilizatorii cu voice sau mai mult pot vorbi\"],\"i0qMbr\":[\"Acasă\"],\"iDNBZe\":[\"Notificări\"],\"iH8pgl\":[\"Înapoi\"],\"iL9SZg\":[\"Banează utilizator (după poreclă)\"],\"iNt+3c\":[\"Înapoi la imagine\"],\"iQvi+a\":[\"Nu mă avertiza despre securitatea scăzută a legăturilor pentru acest server\"],\"iSLIjg\":[\"Conectare\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Adresă server\"],\"idD8Ev\":[\"Salvat\"],\"iivqkW\":[\"Conectat la\"],\"ij+Elv\":[\"Previzualizare imagine\"],\"ilIWp7\":[\"Comută notificările\"],\"iuaqvB\":[\"Folosiți * pentru wildcard. Exemple: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Banare după mască gazdă\"],\"jA4uoI\":[\"Subiect:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Motiv (opțional)\"],\"jUV7CU\":[\"Încarcă avatar\"],\"jW5Uwh\":[\"Controlează câtă media externă se încarcă. Dezactivat / Sigur / Surse de încredere / Tot conținutul.\"],\"jXzms5\":[\"Opțiuni atașament\"],\"jZlrte\":[\"Culoare\"],\"jfC/xh\":[\"Contact\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Încarcă mesaje mai vechi\"],\"k3ID0F\":[\"Filtrează membri…\"],\"k65gsE\":[\"Detalii\"],\"k7Zgob\":[\"Anulează conexiunea\"],\"kAVx5h\":[\"Nu s-au găsit invitații\"],\"kCLEPU\":[\"Conectat la\"],\"kF5LKb\":[\"Modele ignorate:\"],\"kGeOx/\":[\"Alătură-te la \",[\"0\"]],\"kITKr8\":[\"Se încarcă modurile canalului...\"],\"kPpPsw\":[\"Ești un Operator IRC\"],\"kWJmRL\":[\"Tu\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Copiați JSON\"],\"krViRy\":[\"Clic pentru copiere ca JSON\"],\"ks71ra\":[\"Excepții\"],\"kw4lRv\":[\"Semi-operator de canal\"],\"kxgIRq\":[\"Selectează sau adaugă un canal pentru a începe.\"],\"ky6dWe\":[\"Previzualizare avatar\"],\"l+GxCv\":[\"Se încarcă canalele...\"],\"l+IUVW\":[\"Verificarea contului \",[\"account\"],\" a reușit: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"s-a reconectat\"],\"few\":[\"s-a reconectat de \",[\"reconnectCount\"],\" ori\"],\"other\":[\"s-a reconectat de \",[\"reconnectCount\"],\" ori\"]}]],\"l1l8sj\":[\"acum \",[\"0\"],\"z\"],\"l5NhnV\":[\"#canal (opțional)\"],\"l5jmzx\":[[\"0\"],\" și \",[\"1\"],\" scriu...\"],\"lCF0wC\":[\"Reîmprospătează\"],\"lHy8N5\":[\"Se încarcă mai multe canale...\"],\"lasgrr\":[\"utilizat\"],\"lbpf14\":[\"Intrați în \",[\"value\"]],\"lfFsZ4\":[\"Canale\"],\"lkNdiH\":[\"Nume cont\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Încarcă imagine\"],\"loQxaJ\":[\"M-am întors\"],\"lvfaxv\":[\"ACASĂ\"],\"m16xKo\":[\"Adaugă\"],\"m8flAk\":[\"Previzualizare (neîncărcat încă)\"],\"mEPxTp\":[\"<0>⚠️ Atenție!0> Deschide numai linkuri din surse de încredere. Linkurile malițioase îți pot compromite securitatea sau confidențialitatea.\"],\"mHGdhG\":[\"Informații server\"],\"mHS8lb\":[\"Mesaj #\",[\"0\"]],\"mMYBD9\":[\"Larg – Domeniu de protecție mai amplu\"],\"mTGsPd\":[\"Subiect canal\"],\"mU8j6O\":[\"Fără mesaje externe (+n)\"],\"mZp8FL\":[\"Revenire automată la o singură linie\"],\"mdQu8G\":[\"NumeleTău\"],\"miSSBQ\":[\"Comentarii (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Utilizatorul este autentificat\"],\"mwtcGl\":[\"Închide comentariile\"],\"mzI/c+\":[\"Descarcă\"],\"n3fGRk\":[\"setat de \",[\"0\"]],\"nE9jsU\":[\"Relaxat – Protecție mai puțin agresivă\"],\"nNflMD\":[\"Părăsește canalul\"],\"nPXkBi\":[\"Se încarcă datele WHOIS...\"],\"nQnxxF\":[\"Mesaj #\",[\"0\"],\" (Shift+Enter pentru linie nouă)\"],\"nWMRxa\":[\"Anulează fixarea\"],\"nkC032\":[\"Niciun profil anti-flood\"],\"o69z4d\":[\"Trimite un mesaj de avertizare către \",[\"username\"]],\"o9ylQi\":[\"Căutați GIF-uri pentru a începe\"],\"oFGkER\":[\"Notificări server\"],\"oOi11l\":[\"Derulează în jos\"],\"oPYIL5\":[\"rețea\"],\"oQEzQR\":[\"Mesaj direct nou\"],\"oXOSPE\":[\"Conectat\"],\"oal760\":[\"Atacurile man-in-the-middle asupra legăturilor de server sunt posibile\"],\"oeqmmJ\":[\"Surse de încredere\"],\"optX0N\":[\"acum \",[\"0\"],\"h\"],\"ovBPCi\":[\"Implicit\"],\"p0Z69r\":[\"Modelul nu poate fi gol\"],\"p1KgtK\":[\"Eroare la încărcarea audio\"],\"p59pEv\":[\"Detalii suplimentare\"],\"p7sRI6\":[\"Anunțați ceilalți când scrieți\"],\"pBm1od\":[\"Canal secret\"],\"pNmiXx\":[\"Pseudonimul dvs. implicit pentru toate serverele\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Parolă cont\"],\"peNE68\":[\"Permanent\"],\"plhHQt\":[\"Fără date\"],\"pm6+q5\":[\"Avertisment de securitate\"],\"pn5qSs\":[\"Informații suplimentare\"],\"q0cR4S\":[\"acum este cunoscut ca **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Canalul nu va apărea în comenzile LIST sau NAMES\"],\"qLpTm/\":[\"Elimină reacția \",[\"emoji\"]],\"qVkGWK\":[\"Fixează\"],\"qY8wNa\":[\"Pagină principală\"],\"qb0xJ7\":[\"Wildcard: * se potrivește oricărei secvențe, ? unui singur caracter. Exemple: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Cheie canal (+k)\"],\"qtoOYG\":[\"Nicio limită\"],\"r1W2AS\":[\"Imagine filehost\"],\"rIPR2O\":[\"Subiect setat înainte (min în urmă)\"],\"rMMSYo\":[\"Lungimea maximă este \",[\"0\"]],\"rWtzQe\":[\"Rețeaua s-a împărțit și s-a reconectat. ✅\"],\"rYG2u6\":[\"Vă rugăm așteptați...\"],\"rdUucN\":[\"Previzualizare\"],\"rjGI/Q\":[\"Confidențialitate\"],\"rk8iDX\":[\"Se încarcă GIF-urile...\"],\"rn6SBY\":[\"Activare sunet\"],\"s/UKqq\":[\"A fost dat afară din canal\"],\"s8cATI\":[\"s-a alăturat la \",[\"channelName\"]],\"sCO9ue\":[\"Conexiunea la <0>\",[\"serverName\"],\"0> prezintă următoarele probleme de securitate:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"acum este cunoscut ca **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" te-a invitat să te alături la \",[\"channel\"]],\"sby+1/\":[\"Faceți clic pentru a copia\"],\"sfN25C\":[\"Numele dvs. real sau complet\"],\"sliuzR\":[\"Deschide linkul\"],\"sqrO9R\":[\"Mențiuni personalizate\"],\"sr6RdJ\":[\"Multilinie cu Shift+Enter\"],\"swrCpB\":[\"Canalul a fost redenumit din \",[\"oldName\"],\" în \",[\"newName\"],\" de \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Avansat\"],\"t/YqKh\":[\"Elimină\"],\"t47eHD\":[\"Identificatorul dvs. unic pe acest server\"],\"tAkAh0\":[\"URL cu înlocuire opțională \",[\"size\"],\". Exemplu: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Afișează sau ascunde bara laterală cu lista de canale\"],\"tfDRzk\":[\"Salvează\"],\"tiBsJk\":[\"a părăsit \",[\"channelName\"]],\"tt4/UD\":[\"a ieșit (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Porecla {nick} este deja utilizată, reîncercare cu {newNick}\"],\"u0a8B4\":[\"Autentificați-vă ca operator IRC pentru acces administrativ\"],\"u0rWFU\":[\"Creat după (min în urmă)\"],\"u72w3t\":[\"Utilizatori și modele de ignorat\"],\"u7jc2L\":[\"a ieșit\"],\"uAQUqI\":[\"Stare\"],\"uB85T3\":[\"Salvare eșuată: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"Servere IRC:\"],\"ukyW4o\":[\"Linkurile tale de invitație\"],\"usSSr/\":[\"Nivel de zoom\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Shift+Enter pentru rânduri noi (Enter trimite)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Fără subiect\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Limbă\"],\"vaHYxN\":[\"Nume real\"],\"vhjbKr\":[\"Absent\"],\"w4NYox\":[\"client \",[\"title\"]],\"w8xQRx\":[\"Valoare invalidă\"],\"wFjjxZ\":[\"a fost dat afară din \",[\"channelName\"],\" de \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nu s-au găsit excepții de banare\"],\"wPrGnM\":[\"Administrator de canal\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Afișează când utilizatorii intră sau ies din canale\"],\"whqZ9r\":[\"Cuvinte sau fraze suplimentare de evidențiat\"],\"wm7RV4\":[\"Sunet de notificare\"],\"wz/Yoq\":[\"Mesajele tale pot fi interceptate când sunt transmise între servere\"],\"x3+y8b\":[\"Atâtea persoane s-au înregistrat prin acest link\"],\"xCJdfg\":[\"Șterge\"],\"xOTzt5\":[\"chiar acum\"],\"xUHRTR\":[\"Autentificare automată ca operator la conectare\"],\"xWHwwQ\":[\"Banuri\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"Acest server nu acceptă linkuri de invitație (capabilitatea<0>obby.world/invitation0>nu este anunțată). Poți discuta în continuare normal; acest panou este pentru rețelele bazate pe obbyircd.\"],\"xceQrO\":[\"Sunt acceptate numai websocket-uri securizate\"],\"xdtXa+\":[\"nume-canal\"],\"xfXC7q\":[\"Canale text\"],\"xlCYOE\":[\"Se încarcă mai multe mesaje...\"],\"xlhswE\":[\"Valoarea minimă este \",[\"0\"]],\"xq97Ci\":[\"Adaugă un cuvânt sau o expresie...\"],\"xuRqRq\":[\"Limită clienți (+l)\"],\"xwF+7J\":[[\"0\"],\" scrie...\"],\"y1eoq1\":[\"Copiază linkul\"],\"yNeucF\":[\"Acest server nu acceptă metadate extinse de profil (extensia IRCv3 METADATA). Câmpurile precum avatar, nume afișat și stare nu sunt disponibile.\"],\"yPlrca\":[\"Avatar canal\"],\"yQE2r9\":[\"Se încarcă\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Nume utilizator operator\"],\"yYOzWD\":[\"jurnale\"],\"yfx9Re\":[\"Parola operatorului IRC\"],\"ygCKqB\":[\"Oprește\"],\"ymDxJx\":[\"Numele de utilizator al operatorului IRC\"],\"yrpRsQ\":[\"Sortare după nume\"],\"yz7wBu\":[\"Închide\"],\"zJw+jA\":[\"setează modul: \",[\"0\"]],\"zbymaY\":[\"acum \",[\"0\"],\"min\"],\"zebeLu\":[\"Introdu numele de utilizator oper\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Format model invalid. Folosiți formatul nick!user@host (caractere wildcard * permise)\"],\"+6NQQA\":[\"Canal general de suport\"],\"+6NyRG\":[\"Client\"],\"+K0AvT\":[\"Deconectează\"],\"+cyFdH\":[\"Mesaj implicit când vă marcați ca absent\"],\"+mVPqU\":[\"Afișați formatarea Markdown în mesaje\"],\"+vqCJH\":[\"Numele de utilizator al contului dvs. pentru autentificare\"],\"+yPBXI\":[\"Alege fișier\"],\"+zy2Nq\":[\"Tip\"],\"/09cao\":[\"Securitate scăzută a legăturii (Nivel \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Utilizatorii din afara canalului nu pot trimite mesaje\"],\"/4C8U0\":[\"CopiazÄ tot\"],\"/6BzZF\":[\"Comută lista de membri\"],\"/AkXyp\":[\"ConfirmaÈi?\"],\"/TNOPk\":[\"Utilizatorul este absent\"],\"/XQgft\":[\"Descoperă\"],\"/cF7Rs\":[\"Volum\"],\"/dqduX\":[\"Pagina următoare\"],\"/fc3q4\":[\"Tot conținutul\"],\"/kISDh\":[\"Activați sunetele de notificare\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Audio\"],\"/rfkZe\":[\"Redați sunete pentru mențiuni și mesaje\"],\"0/0ZGA\":[\"Mască nume canal\"],\"0D6j7U\":[\"Aflați mai multe despre regulile personalizate →\"],\"0XsHcR\":[\"Dă afară utilizatorul\"],\"0ZpE//\":[\"Sortare după utilizatori\"],\"0bEPwz\":[\"Setează ca absent\"],\"0dGkPt\":[\"Extinde lista de canale\"],\"0gS7M5\":[\"Nume afișat\"],\"0kS+M8\":[\"ExempluRET\"],\"0rgoY7\":[\"Conectați-vă doar la serverele pe care le alegeți\"],\"0wdd7X\":[\"Alătură-te\"],\"0wkVYx\":[\"Mesaje private\"],\"111uHX\":[\"Previzualizare link\"],\"196EG4\":[\"Șterge conversația privată\"],\"1DSr1i\":[\"Înregistrează un cont\"],\"1O/24y\":[\"Comută lista de canale\"],\"1TNIig\":[\"Deschide\"],\"1VPJJ2\":[\"Avertisment link extern\"],\"1ZC/dv\":[\"Nicio mențiune sau mesaj necitit\"],\"1pO1zi\":[\"Numele serverului este obligatoriu\"],\"1uwfzQ\":[\"Vezi subiectul canalului\"],\"268g7c\":[\"Introdu numele afișat\"],\"2CEOW6\":[\"Rețea legată prin bouncerul soju\"],\"2F9+AZ\":[\"Niciun trafic IRC brut capturat încÄ. ÃncearcÄ sÄ te conectezi sau sÄ trimiÈi un mesaj.\"],\"2FOFq1\":[\"Operatorii de server din rețea pot citi mesajele tale\"],\"2FYpfJ\":[\"Mai mult\"],\"2HF1Y2\":[[\"inviter\"],\" l-a invitat pe \",[\"target\"],\" să se alăture la \",[\"channel\"]],\"2I70QL\":[\"Vezi informațiile profilului utilizatorului\"],\"2QYdmE\":[\"Utilizatori:\"],\"2QpEjG\":[\"a ieșit\"],\"2YE223\":[\"Mesaj #\",[\"0\"],\" (Enter pentru linie nouă, Shift+Enter pentru trimitere)\"],\"2bimFY\":[\"Folosește parola serverului\"],\"2iTmdZ\":[\"Stocare locală:\"],\"2odkwe\":[\"Strict – Protecție mai agresivă\"],\"2uDhbA\":[\"Introdu numele de utilizator de invitat\"],\"2ygf/L\":[\"← Înapoi\"],\"2zEgxj\":[\"Caută GIF-uri...\"],\"3RdPhl\":[\"Redenumește canalul\"],\"3THokf\":[\"Utilizator cu drept de vorbire\"],\"3TSz9S\":[\"Minimizează\"],\"3jBDvM\":[\"Nume afișat canal\"],\"3ryuFU\":[\"Rapoarte opționale de erori pentru îmbunătățirea aplicației\"],\"3uBF/8\":[\"Închide vizualizatorul\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Introduceți numele contului...\"],\"4/Rr0R\":[\"Invită un utilizator în canalul curent\"],\"4EZrJN\":[\"Reguli\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Profil flood (+F)\"],\"4RZQRK\":[\"Ce faci?\"],\"4hfTrB\":[\"Poreclă\"],\"4n99LO\":[\"Deja în \",[\"0\"]],\"4t6vMV\":[\"Comutare automată la linie unică pentru mesaje scurte\"],\"4vsHmf\":[\"Timp (min)\"],\"4x/Axu\":[\"Bouncer-ul tău nu are încă nicio rețea. Adaugă una pentru a începe.\"],\"5+INAX\":[\"Evidențiați mesajele care vă menționează\"],\"5R5Pv/\":[\"Nume oper\"],\"678PKt\":[\"Nume rețea\"],\"6Aih4U\":[\"Deconectat\"],\"6CO3WE\":[\"Parolă necesară pentru a intra în canal. Lăsați gol pentru a elimina cheia.\"],\"6HhMs3\":[\"Mesaj de ieșire\"],\"6V3Ea3\":[\"Copiat\"],\"6lGV3K\":[\"Arată mai puțin\"],\"6yFOEi\":[\"Introduceți parola oper...\"],\"7+IHTZ\":[\"Niciun fișier ales\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (ex., spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Trimite mesaj privat\"],\"7U1W7c\":[\"Foarte relaxat\"],\"7Y1YQj\":[\"Nume real:\"],\"7YHArF\":[\"— deschide în vizualizator\"],\"7fjnVl\":[\"Caută utilizatori...\"],\"7jL88x\":[\"Ștergeți acest mesaj? Această acțiune nu poate fi anulată.\"],\"7nGhhM\":[\"La ce te gândești?\"],\"7sEpu1\":[\"Membri — \",[\"0\"]],\"7sNhEz\":[\"Nume de utilizator\"],\"8H0Q+x\":[\"Aflați mai multe despre profiluri →\"],\"8Phu0A\":[\"Afișează când utilizatorii își schimbă pseudonimul\"],\"8XTG9e\":[\"Introdu parola oper\"],\"8XsV2J\":[\"Reîncearcă trimiterea\"],\"8ZsakT\":[\"Parolă\"],\"8kR84m\":[\"Ești pe cale să deschizi un link extern:\"],\"8lCgih\":[\"Eliminați regula\"],\"8o3dPc\":[\"PlaseazÄ fiÈierele pentru a le încÄrca\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"s-a alăturat\"],\"few\":[\"s-a alăturat de \",[\"joinCount\"],\" ori\"],\"other\":[\"s-a alăturat de \",[\"joinCount\"],\" ori\"]}]],\"9BMLnJ\":[\"Reconectează-te la server\"],\"9OEgyT\":[\"Adaugă reacție\"],\"9PQ8m2\":[\"G-Line (ban global)\"],\"9Qs99X\":[\"Email:\"],\"9QupBP\":[\"Elimină șablon\"],\"9W7tl5\":[\"(nemodificat)\"],\"9bG48P\":[\"Se trimite\"],\"9f5f0u\":[\"Întrebări despre confidențialitate? Contactați-ne:\"],\"9iweoP\":[\"Rețele pe \",[\"0\"]],\"9unqs3\":[\"Absent:\"],\"9v3hwv\":[\"Niciun server găsit.\"],\"9zb2WA\":[\"Se conectează\"],\"A1taO8\":[\"Caută\"],\"A2adVi\":[\"Trimiteți notificări de tastare\"],\"A9Rhec\":[\"Nume canal\"],\"AWOSPo\":[\"Mărește\"],\"AXSpEQ\":[\"Oper la conectare\"],\"AdKRCX\":[\"Sunteți conectat la <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Cont\"],\"AhNP40\":[\"Derulare\"],\"Ai2U7L\":[\"Gazdă\"],\"AjBQnf\":[\"Poreclă schimbată\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Anulează răspunsul\"],\"ApSx0O\":[\"S-au găsit \",[\"0\"],\" mesaje care corespund cu \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Niciun rezultat găsit\"],\"AyNqAB\":[\"Afișează toate evenimentele serverului în chat\"],\"B/QqGw\":[\"Departe de tastatură\"],\"B0sB2k\":[\"Text simplu\"],\"B8AaMI\":[\"Acest câmp este obligatoriu\"],\"BA2c49\":[\"Serverul nu acceptă filtrarea avansată LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" și alți \",[\"3\"],\" scriu...\"],\"BGul2A\":[\"Ai modificări nesalvate. Ești sigur că vrei să închizi fără să salvezi?\"],\"BIf9fi\":[\"Mesajul dvs. de stare\"],\"BOJWfb\":[\"Deconectați de la bouncerul soju?\"],\"BPm98R\":[\"Niciun server nu este selectat. Alege mai întâi un server din bara lateralÄ; linkurile de invitaÈie sunt gestionate per server.\"],\"BZz3md\":[\"Site-ul dvs. web personal\"],\"Bgm/H7\":[\"Permite introducerea mai multor rânduri de text\"],\"BiQIl1\":[\"Fixează această conversație privată\"],\"BlNZZ2\":[\"Click pentru a sări la mesaj\"],\"Bowq3c\":[\"Doar operatorii pot schimba subiectul canalului\"],\"Btozzp\":[\"Această imagine a expirat\"],\"Bycfjm\":[\"Total: \",[\"0\"]],\"C6IBQc\":[\"Copiați JSON complet\"],\"C9L9wL\":[\"Colectare de date\"],\"CDq4wC\":[\"Moderează utilizatorul\"],\"CHVRxG\":[\"Mesaj @\",[\"0\"],\" (Shift+Enter pentru linie nouă)\"],\"CN9zdR\":[\"Numele oper și parola sunt obligatorii\"],\"CW3sYa\":[\"Adaugă reacția \",[\"emoji\"]],\"CaAkqd\":[\"Afișați deconectările\"],\"CbvaYj\":[\"Banare după poreclă\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Selectează un canal\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"Sistem\"],\"D28t6+\":[\"s-a alăturat și a ieșit\"],\"DB8zMK\":[\"Aplică\"],\"DBcWHr\":[\"Fișier audio de notificare personalizat\"],\"DTy9Xw\":[\"Previzualizări media\"],\"Dj4pSr\":[\"Alege o parolă sigură\"],\"Du+zn+\":[\"Se caută...\"],\"Du2T2f\":[\"Setarea nu a fost găsită\"],\"DwsSVQ\":[\"Aplică filtrele și actualizează\"],\"E3W/zd\":[\"Pseudonim implicit\"],\"E6nRW7\":[\"Copiază URL\"],\"E703RG\":[\"Moduri:\"],\"EAeu1Z\":[\"Trimiteți invitația\"],\"EFKJQT\":[\"Setare\"],\"EGPQBv\":[\"Reguli flood personalizate (+f)\"],\"ELik0r\":[\"Vizualizați politica completă de confidențialitate\"],\"EPbeC2\":[\"Vezi sau editează subiectul canalului\"],\"EQCDNT\":[\"Introduceți numele de utilizator oper...\"],\"EUvulZ\":[\"S-a găsit 1 mesaj care corespunde cu \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Imaginea următoare\"],\"EdQY6l\":[\"Niciunul\"],\"EnqLYU\":[\"Caută servere...\"],\"F0OKMc\":[\"Editează server\"],\"F6Int2\":[\"Activați evidențierile\"],\"FDoLyE\":[\"Utilizatori max.\"],\"FUU/hZ\":[\"Controlează câte conținuturi media externe sunt încărcate în chat.\"],\"Fdp03t\":[\"activ\"],\"FfPWR0\":[\"Modal\"],\"FjkaiT\":[\"Micșorează\"],\"FlqOE9\":[\"Ce înseamnă aceasta:\"],\"FolHNl\":[\"Gestionează-ți contul și autentificarea\"],\"Fp2Dif\":[\"A ieșit de pe server\"],\"G5KmCc\":[\"GZ-Line (Z-Line globală)\"],\"GDs0lz\":[\"<0>Risc:0> Informațiile sensibile (mesaje, conversații private, date de autentificare) pot fi expuse administratorilor de rețea sau atacatorilor poziționați între serverele IRC.\"],\"GR+2I3\":[\"Adaugă mască de invitație (ex. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Închide notificările server detașate\"],\"GdhD7H\":[\"FaceÈi clic din nou pentru a confirma\"],\"GjRZex\":[\"Deconectați rețeaua?\"],\"GlHnXw\":[\"Schimbarea poreclelei a eșuat: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Previzualizare:\"],\"GtmO8/\":[\"de la\"],\"GtuHUQ\":[\"Redenumiți acest canal pe server. Toți utilizatorii vor vedea noul nume.\"],\"GuGfFX\":[\"Comută căutarea\"],\"GxkJXS\":[\"Se încarcă...\"],\"GzbwnK\":[\"S-a alăturat canalului\"],\"GzsUDB\":[\"Profil extins\"],\"H/PnT8\":[\"Inserează emoji\"],\"H6Izzl\":[\"Codul dvs. de culoare preferat\"],\"H9jIv+\":[\"Afișați intrări/ieșiri\"],\"HAKBY9\":[\"Încărcați fișiere\"],\"HdE1If\":[\"Canal\"],\"Hk4AW9\":[\"Numele dvs. de afișare preferat\"],\"HmHDk7\":[\"Selectează un membru\"],\"HrQzPU\":[\"Canale pe \",[\"networkName\"]],\"I2tXQ5\":[\"Mesaj @\",[\"0\"],\" (Enter pentru linie nouă, Shift+Enter pentru trimitere)\"],\"I6bw/h\":[\"Banează utilizatorul\"],\"I92Z+b\":[\"Activează notificările\"],\"I9D72S\":[\"Sigur doriți să ștergeți acest mesaj? Această acțiune nu poate fi anulată.\"],\"IA+1wo\":[\"Afișează când utilizatorii sunt expulzați din canale\"],\"IDwkJx\":[\"Operator IRC\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Salvează modificările\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" și \",[\"2\"],\" scriu...\"],\"IgrLD/\":[\"Pauză\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Răspunde\"],\"IoHMnl\":[\"Valoarea maximă este \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Se conectează...\"],\"J5T9NW\":[\"Informații utilizator\"],\"J8Y5+z\":[\"Oops! Rețeaua s-a împărțit! ⚠️\"],\"JBHkBA\":[\"A părăsit canalul\"],\"JCwL0Q\":[\"Introdu motivul (opțional)\"],\"JFciKP\":[\"Comută\"],\"JXGkhG\":[\"Schimbă numele canalului (numai operatori)\"],\"JcD7qf\":[\"Mai multe acțiuni\"],\"JdkA+c\":[\"Secret (+s)\"],\"Jmu12l\":[\"Canale server\"],\"JoQY+E\":[\"Aceasta elimină rețeaua din bouncerul tău soju. Pentru a o folosi din nou, va trebui să o adăugați înapoi.\"],\"JvQ++s\":[\"Activați Markdown\"],\"K2jwh/\":[\"Nu există date WHOIS disponibile\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Șterge mesaj\"],\"KKBlUU\":[\"Încorporare\"],\"KM0pLb\":[\"Bine ai venit în canal!\"],\"KR6W2h\":[\"Nu mai ignora utilizatorul\"],\"KV+Bi1\":[\"Doar pe invitație (+i)\"],\"KdCtwE\":[\"Câte secunde se monitorizează activitatea flood înainte de resetarea contoarelor\"],\"Kkezga\":[\"Parolă server\"],\"KsiQ/8\":[\"Utilizatorii trebuie invitați pentru a intra în canal\"],\"L+gB/D\":[\"Informații canal\"],\"LC1a7n\":[\"Serverul IRC a raportat că legăturile sale server-la-server au un nivel scăzut de securitate. Aceasta înseamnă că atunci când mesajele tale sunt transmise între serverele IRC din rețea, este posibil ca acestea să nu fie criptate corespunzător sau certificatele SSL/TLS să nu fie validate corect.\"],\"LEwpeL\":[\"bouncer soju (control)\"],\"LNfLR5\":[\"Afișați expulzările\"],\"LP+1Z7\":[\"Adaugă rețea\"],\"LQb0W/\":[\"Afișați toate evenimentele\"],\"LU7/yA\":[\"Nume alternativ pentru afișaj. Poate conține spații, emoji și caractere speciale. Numele real (\",[\"channelName\"],\") va fi folosit în continuare pentru comenzile IRC.\"],\"LUb9O7\":[\"Este necesar un port de server valid\"],\"LV4fT6\":[\"Descriere (opÈional, ex. âTesteri beta T3â)\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Politică de confidențialitate\"],\"LcuSDR\":[\"Gestionează informațiile profilului și metadatele\"],\"LqLS9B\":[\"Afișați schimbările de pseudonim\"],\"LsDQt2\":[\"Setări canal\"],\"LtI9AS\":[\"Proprietar\"],\"LuNhhL\":[\"a reacționat la acest mesaj\"],\"M/AZNG\":[\"URL-ul imaginii dvs. de avatar\"],\"M/WIer\":[\"Trimite mesaj\"],\"M8er/5\":[\"Nume:\"],\"MHk+7g\":[\"Imaginea anterioară\"],\"MRorGe\":[\"Mesaj privat\"],\"MVbSGP\":[\"Fereastră de timp (secunde)\"],\"MkpcsT\":[\"Mesajele și setările dvs. sunt stocate local pe dispozitivul dvs.\"],\"MzPdC2\":[\"Parolă server (PASS)\"],\"N/hDSy\":[\"Marcați ca bot, de obicei 'on' sau gol\"],\"N6j2JH\":[\"Editează \",[\"0\"]],\"N7TQbE\":[\"Invitați utilizatorul în \",[\"channelName\"]],\"NCca/o\":[\"Introduceți porecla implicită...\"],\"Nqs6B9\":[\"Afișează toate conținuturile externe. Orice URL poate genera o solicitare către un server necunoscut.\"],\"Nt+9O7\":[\"Utilizați WebSocket în loc de TCP brut\"],\"NxIHzc\":[\"Expulzați utilizatorul\"],\"O+v/cL\":[\"Răsfoiește toate canalele de pe server\"],\"OCGpR4\":[\"(moștenește)\"],\"ODwSCk\":[\"Trimite un GIF\"],\"OGQ5kK\":[\"Configurează sunetele de notificare și evidențierile\"],\"OIPt1Z\":[\"Afișează sau ascunde bara laterală cu lista de membri\"],\"OKSNq/\":[\"Foarte strict\"],\"ONWvwQ\":[\"Încărcați\"],\"OVKoQO\":[\"Parola contului dvs. pentru autentificare\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Aducem IRC în viitor\"],\"OhCpra\":[\"Setează un subiect…\"],\"OkltoQ\":[\"Banează \",[\"username\"],\" după poreclă (împiedică reconectarea cu același nick)\"],\"P+t/Te\":[\"Nicio dată suplimentară\"],\"P42Wcc\":[\"Sigur\"],\"PD38l0\":[\"Previzualizare avatar canal\"],\"PD9mEt\":[\"Scrie un mesaj...\"],\"PPqfdA\":[\"Deschide setările de configurare ale canalului\"],\"PSCjfZ\":[\"Subiectul afișat pentru acest canal. Toți utilizatorii îl pot vedea.\"],\"PZCecv\":[\"Previzualizare PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 dată\"],\"few\":[[\"c\"],\" ori\"],\"other\":[[\"c\"],\" ori\"]}]],\"PguS2C\":[\"Adaugă mască de excepție (ex. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Se afișează \",[\"displayedChannelsCount\"],\" din \",[\"0\"],\" canale\"],\"PqhVlJ\":[\"Banează utilizator (după hostmask)\"],\"Q+chwU\":[\"Nume utilizator:\"],\"Q2QY4/\":[\"Èterge aceastÄ invitaÈie\"],\"Q3v9Wc\":[\"Da, șterge\"],\"Q6hhn8\":[\"Preferințe\"],\"QF4a34\":[\"Introduceți un nume de utilizator\"],\"QGqSZ2\":[\"Culoare și formatare\"],\"QJQd1J\":[\"Editați profilul\"],\"QSzGDE\":[\"Inactiv\"],\"QUlny5\":[\"Bine ai venit la \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Citește mai mult\"],\"QuSkCF\":[\"Filtrează canale...\"],\"QwUrDZ\":[\"a schimbat subiectul la: \",[\"topic\"]],\"R0UH07\":[\"Imaginea \",[\"0\"],\" din \",[\"1\"]],\"R7SsBE\":[\"Dezactivare sunet\"],\"R8rf1X\":[\"Click pentru a seta subiectul\"],\"RArB3D\":[\"a fost dat afară din \",[\"channelName\"],\" de \",[\"username\"]],\"RI3cWd\":[\"Descoperă lumea IRC cu ObsidianIRC\"],\"RIfHS5\":[\"CreeazÄ un nou link de invitaÈie\"],\"RMMaN5\":[\"Moderat (+m)\"],\"RWw9Lg\":[\"Închide fereastra\"],\"RZ2BuZ\":[\"Înregistrarea contului \",[\"account\"],\" necesită verificare: \",[\"message\"]],\"RySp6q\":[\"Ascundeți comentariile\"],\"S5Togi\":[\"Se încarcă rețelele de pe bouncer-ul tău…\"],\"SPKQTd\":[\"Porecla este obligatorie\"],\"SPVjfj\":[\"Va fi implicit „niciun motiv\\\" dacă este lăsat gol\"],\"SQKPvQ\":[\"Invită utilizator\"],\"STmlpb\":[\"Înapoi la lista de rețele\"],\"SkZcl+\":[\"Alegeți un profil de protecție flood predefinit. Aceste profiluri oferă setări de protecție echilibrate pentru diferite cazuri de utilizare.\"],\"Slr+3C\":[\"Utilizatori min.\"],\"Spnlre\":[\"L-ai invitat pe \",[\"target\"],\" să se alăture la \",[\"channel\"]],\"T/ckN5\":[\"Deschide în vizualizator\"],\"T91vKp\":[\"Redare\"],\"TV2Wdu\":[\"Aflați cum gestionăm datele dvs. și vă protejăm confidențialitatea.\"],\"TgFpwD\":[\"Se aplică...\"],\"TkzSFB\":[\"Nicio modificare\"],\"TtserG\":[\"Introdu numele real\"],\"Ttz9J1\":[\"Introduceți parola...\"],\"Tz0i8g\":[\"Setări\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reacționează\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Nu ai creat încÄ niciun link de invitaÈie. FoloseÈte formularul de mai sus pentru a-l crea pe primul.\"],\"UGT5vp\":[\"Salvați setările\"],\"UV5hLB\":[\"Nu s-au găsit banuri\"],\"Uaj3Nd\":[\"Mesaje de stare\"],\"Ue3uny\":[\"Implicit (fără profil)\"],\"UkARhe\":[\"Normal – Protecție standard\"],\"Umn7Cj\":[\"Niciun comentariu. Fiți primul!\"],\"UtUIRh\":[[\"0\"],\" mesaje mai vechi\"],\"UwzP+U\":[\"Conexiune securizată\"],\"V0/A4O\":[\"Proprietar de canal\"],\"V0zZWc\":[\"Aceasta va închide și rețeaua legată de mai jos.\"],\"V4qgxE\":[\"Creat înainte (min în urmă)\"],\"V8yTm6\":[\"Șterge căutarea\"],\"VJMMyz\":[\"ObsidianIRC - Aducând IRC în viitor\"],\"VJScHU\":[\"Motiv\"],\"VLsmVV\":[\"Dezactivează notificările\"],\"VbyRUy\":[\"Comentarii\"],\"Vmx0mQ\":[\"Setat de:\"],\"VqnIZz\":[\"Vezi politica noastră de confidențialitate și practicile privind datele\"],\"VrMygG\":[\"Lungimea minimă este \",[\"0\"]],\"VrnTui\":[\"Pronumele dvs., afișate în profil\"],\"W8E3qn\":[\"Cont autentificat\"],\"WAakm9\":[\"Șterge canal\"],\"WFxTHC\":[\"Adaugă mască de banare (ex. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Adresa serverului este obligatorie\"],\"WRYdXW\":[\"Poziție audio\"],\"WUOH5B\":[\"Ignoră utilizatorul\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Arată 1 element în plus\"],\"few\":[\"Arată \",[\"1\"],\" elemente în plus\"],\"other\":[\"Arată \",[\"1\"],\" de elemente în plus\"]}]],\"WYxRzo\":[\"CreeazÄ Èi administreazÄ linkurile tale de invitaÈie\"],\"Wd38W1\":[\"LasÄ canalul gol pentru o invitaÈie genericÄ Ã®n reÈea. Descrierea este doar pentru evidenÈa ta â vizibilÄ doar pentru tine în aceastÄ listÄ.\"],\"Weq9zb\":[\"General\"],\"Wfj7Sk\":[\"Activează sau dezactivează sunetele de notificare\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Profil utilizator\"],\"X6S3lt\":[\"Caută setări, canale, servere...\"],\"XEHan5\":[\"Continuă oricum\"],\"XI1+wb\":[\"Format invalid\"],\"XIXeuC\":[\"Mesaj @\",[\"0\"]],\"XMS+k4\":[\"Începe un mesaj privat\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Anulează fixarea conversației private\"],\"Xm/s+u\":[\"Afișare\"],\"Xp2n93\":[\"Afișează media de pe gazda de fișiere de încredere a serverului. Nu se fac solicitări către servicii externe.\"],\"XvjC4F\":[\"Se salvează...\"],\"Y/qryO\":[\"Niciun utilizator găsit care să corespundă căutării\"],\"YAqRpI\":[\"Înregistrarea contului \",[\"account\"],\" a reușit: \",[\"message\"]],\"YEfzvP\":[\"Subiect protejat (+t)\"],\"YQOn6a\":[\"Restrânge lista de membri\"],\"YRCoE9\":[\"Operator de canal\"],\"YURQaF\":[\"Vizualizați profilul\"],\"YdBSvr\":[\"Controlează afișarea media și conținutul extern\"],\"Yj6U3V\":[\"Fără server central:\"],\"YjvpGx\":[\"Pronume\"],\"YqH4l4\":[\"Nicio cheie\"],\"YyUPpV\":[\"Cont:\"],\"ZJSWfw\":[\"Mesaj afișat la deconectarea de la server\"],\"ZR1dJ4\":[\"Invitații\"],\"ZdWg0V\":[\"Deschide în browser\"],\"ZhRBbl\":[\"Caută mesaje…\"],\"Zmcu3y\":[\"Filtre avansate\"],\"a0bHay\":[\"Deconectați <0>\",[\"0\"],\"0>?\"],\"a2/8e5\":[\"Subiect setat după (min în urmă)\"],\"aHKcKc\":[\"Pagina anterioară\"],\"aJTbXX\":[\"Parolă oper\"],\"aQryQv\":[\"Modelul există deja\"],\"aW9pLN\":[\"Numărul maxim de utilizatori permis în canal. Lăsați gol pentru fără limită.\"],\"ah4fmZ\":[\"Afișează și previzualizări de pe YouTube, Vimeo, SoundCloud și servicii similare cunoscute.\"],\"aifXak\":[\"Niciun fișier media în acest canal\"],\"ap2zBz\":[\"Relaxat\"],\"az8lvo\":[\"Oprit\"],\"azXSNo\":[\"Extinde lista de membri\"],\"azdliB\":[\"Conectează-te la un cont\"],\"b26wlF\":[\"ea/ei\"],\"bD/+Ei\":[\"Strict\"],\"bQ6BJn\":[\"Configurați reguli detaliate de protecție flood. Fiecare regulă specifică tipul de activitate de monitorizat și acțiunea de întreprins când pragurile sunt depășite.\"],\"beV7+y\":[\"Utilizatorul va primi o invitație să se alăture \",[\"channelName\"],\".\"],\"bk84cH\":[\"Mesaj de absență\"],\"bkHdLj\":[\"Adaugă server IRC\"],\"bmQLn5\":[\"Adaugă regulă\"],\"bv4cFj\":[\"Transport\"],\"bwRvnp\":[\"Acțiune\"],\"c8+EVZ\":[\"Cont verificat\"],\"cGYUlD\":[\"Nu sunt încărcate previzualizări media.\"],\"cLF98o\":[\"Afișați comentariile (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Niciun utilizator disponibil\"],\"cSgpoS\":[\"Fixează conversația privată\"],\"cXeEKu\":[\"Aceasta va închide și cele \",[\"0\"],\" rețele legate de mai jos.\"],\"cde3ce\":[\"Mesaj <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Copiați ieșirea formatată\"],\"cl/A5J\":[\"Bine ai venit la \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Șterge\"],\"coPLXT\":[\"Nu stocăm comunicațiile dvs. IRC pe serverele noastre\"],\"crYH/6\":[\"Player SoundCloud\"],\"cv5DQb\":[\"nicio gazdă setată\"],\"d3sis4\":[\"Adaugă server\"],\"d9aN5k\":[\"Elimină \",[\"username\"],\" din canal\"],\"dEgA5A\":[\"Anulează\"],\"dGi1We\":[\"Anulează fixarea acestei conversații private\"],\"dJVuyC\":[\"a părăsit \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"către\"],\"dXqxlh\":[\"<0>⚠️ Risc de securitate!0> Această conexiune poate fi vulnerabilă la interceptare sau atacuri de tip man-in-the-middle.\"],\"da9Q/R\":[\"Moduri canal modificate\"],\"dhJN3N\":[\"Afișați comentariile\"],\"dj2xTE\":[\"Respinge notificarea\"],\"dpCzmC\":[\"Setări de protecție flood\"],\"e9dQpT\":[\"Dorești să deschizi acest link într-o filă nouă?\"],\"ePK91l\":[\"Editează\"],\"eYBDuB\":[\"Încărcați o imagine sau furnizați o adresă URL cu înlocuire opțională \",[\"size\"]],\"edBbee\":[\"Banează \",[\"username\"],\" după hostmask (împiedică reconectarea de pe același IP/host)\"],\"ekfzWq\":[\"Setări utilizator\"],\"elPDWs\":[\"Personalizează experiența clientului IRC\"],\"eu2osY\":[\"<0>💡 Recomandare:0> Continuați doar dacă aveți încredere în acest server și înțelegeți riscurile. Evitați să partajați informații sensibile sau parole prin această conexiune.\"],\"euEhbr\":[\"Faceți clic pentru a vă alătura \",[\"channel\"]],\"ez3vLd\":[\"Activați introducerea pe mai multe rânduri\"],\"f0J5Ki\":[\"Comunicarea server-la-server poate folosi conexiuni necriptate\"],\"f9BHJk\":[\"Avertizează utilizatorul\"],\"fDOLLd\":[\"Nu s-au găsit canale.\"],\"ffzDkB\":[\"Analize anonime:\"],\"fq1GF9\":[\"Afișează când utilizatorii se deconectează de la server\"],\"gCldcN\":[\"Schimbați culoarea de accent\"],\"gEF57C\":[\"Acest server acceptă doar un tip de conexiune\"],\"gJuLUI\":[\"Listă de ignorați\"],\"gNzMrk\":[\"Avatar curent\"],\"gjPWyO\":[\"Introduceți porecla...\"],\"gz6UQ3\":[\"Maximizează\"],\"h6/IMX\":[\"Adaugă prima ta rețea\"],\"h6razj\":[\"Excludeți masca numelui canalului\"],\"hG6jnw\":[\"Niciun subiect setat\"],\"hG89Ed\":[\"Imagine\"],\"hYgDIe\":[\"CreeazÄ\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"ex., 100:1440\"],\"he3ygx\":[\"CopiazÄ\"],\"hehnjM\":[\"Cantitate\"],\"hzdLuQ\":[\"Doar utilizatorii cu voice sau mai mult pot vorbi\"],\"i0qMbr\":[\"Acasă\"],\"iDNBZe\":[\"Notificări\"],\"iH8pgl\":[\"Înapoi\"],\"iL9SZg\":[\"Banează utilizator (după poreclă)\"],\"iNt+3c\":[\"Înapoi la imagine\"],\"iQvi+a\":[\"Nu mă avertiza despre securitatea scăzută a legăturilor pentru acest server\"],\"iSLIjg\":[\"Conectare\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Adresă server\"],\"idD8Ev\":[\"Salvat\"],\"iivqkW\":[\"Conectat la\"],\"ij+Elv\":[\"Previzualizare imagine\"],\"ilIWp7\":[\"Comută notificările\"],\"iuaqvB\":[\"Folosiți * pentru wildcard. Exemple: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Banare după mască gazdă\"],\"jA4uoI\":[\"Subiect:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Motiv (opțional)\"],\"jUV7CU\":[\"Încarcă avatar\"],\"jW5Uwh\":[\"Controlează câtă media externă se încarcă. Dezactivat / Sigur / Surse de încredere / Tot conținutul.\"],\"jXzms5\":[\"Opțiuni atașament\"],\"jZlrte\":[\"Culoare\"],\"jfC/xh\":[\"Contact\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Încarcă mesaje mai vechi\"],\"k3ID0F\":[\"Filtrează membri…\"],\"k65gsE\":[\"Detalii\"],\"k7Zgob\":[\"Anulează conexiunea\"],\"kAVx5h\":[\"Nu s-au găsit invitații\"],\"kCLEPU\":[\"Conectat la\"],\"kF5LKb\":[\"Modele ignorate:\"],\"kGeOx/\":[\"Alătură-te la \",[\"0\"]],\"kITKr8\":[\"Se încarcă modurile canalului...\"],\"kPpPsw\":[\"Ești un Operator IRC\"],\"kWJmRL\":[\"Tu\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Copiați JSON\"],\"krViRy\":[\"Clic pentru copiere ca JSON\"],\"ks71ra\":[\"Excepții\"],\"kw4lRv\":[\"Semi-operator de canal\"],\"kxgIRq\":[\"Selectează sau adaugă un canal pentru a începe.\"],\"ky6dWe\":[\"Previzualizare avatar\"],\"l+GxCv\":[\"Se încarcă canalele...\"],\"l+IUVW\":[\"Verificarea contului \",[\"account\"],\" a reușit: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"s-a reconectat\"],\"few\":[\"s-a reconectat de \",[\"reconnectCount\"],\" ori\"],\"other\":[\"s-a reconectat de \",[\"reconnectCount\"],\" ori\"]}]],\"l1l8sj\":[\"acum \",[\"0\"],\"z\"],\"l5NhnV\":[\"#canal (opÈional)\"],\"l5jmzx\":[[\"0\"],\" și \",[\"1\"],\" scriu...\"],\"lCF0wC\":[\"ReîmprospÄteazÄ\"],\"lHy8N5\":[\"Se încarcă mai multe canale...\"],\"lasgrr\":[\"utilizat\"],\"lbpf14\":[\"Intrați în \",[\"value\"]],\"lfFsZ4\":[\"Canale\"],\"lkNdiH\":[\"Nume cont\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Încarcă imagine\"],\"loQxaJ\":[\"M-am întors\"],\"lvfaxv\":[\"ACASĂ\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Adaugă\"],\"m8flAk\":[\"Previzualizare (neîncărcat încă)\"],\"mEPxTp\":[\"<0>⚠️ Atenție!0> Deschide numai linkuri din surse de încredere. Linkurile malițioase îți pot compromite securitatea sau confidențialitatea.\"],\"mHGdhG\":[\"Informații server\"],\"mHS8lb\":[\"Mesaj #\",[\"0\"]],\"mMYBD9\":[\"Larg – Domeniu de protecție mai amplu\"],\"mTGsPd\":[\"Subiect canal\"],\"mU8j6O\":[\"Fără mesaje externe (+n)\"],\"mZp8FL\":[\"Revenire automată la o singură linie\"],\"mdQu8G\":[\"NumeleTău\"],\"miSSBQ\":[\"Comentarii (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Utilizatorul este autentificat\"],\"mwtcGl\":[\"Închide comentariile\"],\"myL0MR\":[\"Ștergeți această rețea?\"],\"mzI/c+\":[\"Descarcă\"],\"n3fGRk\":[\"setat de \",[\"0\"]],\"n5+j9l\":[\"ex. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Relaxat – Protecție mai puțin agresivă\"],\"nNflMD\":[\"Părăsește canalul\"],\"nPXkBi\":[\"Se încarcă datele WHOIS...\"],\"nQnxxF\":[\"Mesaj #\",[\"0\"],\" (Shift+Enter pentru linie nouă)\"],\"nWMRxa\":[\"Anulează fixarea\"],\"nkC032\":[\"Niciun profil anti-flood\"],\"o69z4d\":[\"Trimite un mesaj de avertizare către \",[\"username\"]],\"o9ylQi\":[\"Căutați GIF-uri pentru a începe\"],\"oFGkER\":[\"Notificări server\"],\"oOi11l\":[\"Derulează în jos\"],\"oPYIL5\":[\"reÈea\"],\"oQEzQR\":[\"Mesaj direct nou\"],\"oXOSPE\":[\"Conectat\"],\"oal760\":[\"Atacurile man-in-the-middle asupra legăturilor de server sunt posibile\"],\"oeqmmJ\":[\"Surse de încredere\"],\"optX0N\":[\"acum \",[\"0\"],\"h\"],\"ovBPCi\":[\"Implicit\"],\"p0Z69r\":[\"Modelul nu poate fi gol\"],\"p1KgtK\":[\"Eroare la încărcarea audio\"],\"p59pEv\":[\"Detalii suplimentare\"],\"p7sRI6\":[\"Anunțați ceilalți când scrieți\"],\"pBm1od\":[\"Canal secret\"],\"pNmiXx\":[\"Pseudonimul dvs. implicit pentru toate serverele\"],\"pUUo9G\":[\"Hostname:\"],\"pVGPmz\":[\"Parolă cont\"],\"peNE68\":[\"Permanent\"],\"plhHQt\":[\"Fără date\"],\"pm6+q5\":[\"Avertisment de securitate\"],\"pn5qSs\":[\"Informații suplimentare\"],\"q0cR4S\":[\"acum este cunoscut ca **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Canalul nu va apărea în comenzile LIST sau NAMES\"],\"qLpTm/\":[\"Elimină reacția \",[\"emoji\"]],\"qVkGWK\":[\"Fixează\"],\"qY8wNa\":[\"Pagină principală\"],\"qb0xJ7\":[\"Wildcard: * se potrivește oricărei secvențe, ? unui singur caracter. Exemple: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Cheie canal (+k)\"],\"qtoOYG\":[\"Nicio limită\"],\"r1W2AS\":[\"Imagine filehost\"],\"rIPR2O\":[\"Subiect setat înainte (min în urmă)\"],\"rMMSYo\":[\"Lungimea maximă este \",[\"0\"]],\"rWtzQe\":[\"Rețeaua s-a împărțit și s-a reconectat. ✅\"],\"rYG2u6\":[\"Vă rugăm așteptați...\"],\"rdUucN\":[\"Previzualizare\"],\"rjGI/Q\":[\"Confidențialitate\"],\"rk8iDX\":[\"Se încarcă GIF-urile...\"],\"rn6SBY\":[\"Activare sunet\"],\"s/UKqq\":[\"A fost dat afară din canal\"],\"s7oqXR\":[\"Selectați o rețea\"],\"s8cATI\":[\"s-a alăturat la \",[\"channelName\"]],\"sCO9ue\":[\"Conexiunea la <0>\",[\"serverName\"],\"0> prezintă următoarele probleme de securitate:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"acum este cunoscut ca **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" te-a invitat să te alături la \",[\"channel\"]],\"sUBSbK\":[\"Încă nu există rețele upstream.\"],\"sby+1/\":[\"Faceți clic pentru a copia\"],\"sfN25C\":[\"Numele dvs. real sau complet\"],\"sliuzR\":[\"Deschide linkul\"],\"sqrO9R\":[\"Mențiuni personalizate\"],\"sr6RdJ\":[\"Multilinie cu Shift+Enter\"],\"swrCpB\":[\"Canalul a fost redenumit din \",[\"oldName\"],\" în \",[\"newName\"],\" de \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Avansat\"],\"t/YqKh\":[\"Elimină\"],\"t47eHD\":[\"Identificatorul dvs. unic pe acest server\"],\"tAkAh0\":[\"URL cu înlocuire opțională \",[\"size\"],\". Exemplu: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Afișează sau ascunde bara laterală cu lista de canale\"],\"tfDRzk\":[\"Salvează\"],\"tiBsJk\":[\"a părăsit \",[\"channelName\"]],\"tt4/UD\":[\"a ieșit (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Porecla {nick} este deja utilizată, reîncercare cu {newNick}\"],\"u0a8B4\":[\"Autentificați-vă ca operator IRC pentru acces administrativ\"],\"u0rWFU\":[\"Creat după (min în urmă)\"],\"u72w3t\":[\"Utilizatori și modele de ignorat\"],\"u7jc2L\":[\"a ieșit\"],\"uAQUqI\":[\"Stare\"],\"uB85T3\":[\"Salvare eșuată: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"Servere IRC:\"],\"ukyW4o\":[\"Linkurile tale de invitaÈie\"],\"usSSr/\":[\"Nivel de zoom\"],\"v7uvcf\":[\"Software:\"],\"vE8kb+\":[\"Shift+Enter pentru rânduri noi (Enter trimite)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Fără subiect\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Limbă\"],\"vaHYxN\":[\"Nume real\"],\"vhjbKr\":[\"Absent\"],\"w/nogd\":[[\"0\"],\" rețea\",[\"1\"],\" — alegeți una pentru a vă alătura\"],\"w4NYox\":[\"client \",[\"title\"]],\"w8xQRx\":[\"Valoare invalidă\"],\"wFjjxZ\":[\"a fost dat afară din \",[\"channelName\"],\" de \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Nu s-au găsit excepții de banare\"],\"wPrGnM\":[\"Administrator de canal\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Afișează când utilizatorii intră sau ies din canale\"],\"whqZ9r\":[\"Cuvinte sau fraze suplimentare de evidențiat\"],\"wm7RV4\":[\"Sunet de notificare\"],\"wz/Yoq\":[\"Mesajele tale pot fi interceptate când sunt transmise între servere\"],\"x3+y8b\":[\"Atâtea persoane s-au înregistrat prin acest link\"],\"xCJdfg\":[\"Șterge\"],\"xOTzt5\":[\"chiar acum\"],\"xUHRTR\":[\"Autentificare automată ca operator la conectare\"],\"xWHwwQ\":[\"Banuri\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"Acest server nu acceptÄ linkuri de invitaÈie (capabilitatea<0>obby.world/invitation0>nu este anunÈatÄ). PoÈi discuta în continuare normal; acest panou este pentru reÈelele bazate pe obbyircd.\"],\"xceQrO\":[\"Sunt acceptate numai websocket-uri securizate\"],\"xdtXa+\":[\"nume-canal\"],\"xfXC7q\":[\"Canale text\"],\"xlCYOE\":[\"Se încarcă mai multe mesaje...\"],\"xlhswE\":[\"Valoarea minimă este \",[\"0\"]],\"xq97Ci\":[\"Adaugă un cuvânt sau o expresie...\"],\"xuRqRq\":[\"Limită clienți (+l)\"],\"xwF+7J\":[[\"0\"],\" scrie...\"],\"y1eoq1\":[\"CopiazÄ linkul\"],\"yJztBY\":[\"Șterge rețeaua\"],\"yNeucF\":[\"Acest server nu acceptă metadate extinse de profil (extensia IRCv3 METADATA). Câmpurile precum avatar, nume afișat și stare nu sunt disponibile.\"],\"yPlrca\":[\"Avatar canal\"],\"yQE2r9\":[\"Se încarcă\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Nume utilizator operator\"],\"yYOzWD\":[\"jurnale\"],\"yfx9Re\":[\"Parola operatorului IRC\"],\"ygCKqB\":[\"Oprește\"],\"ymDxJx\":[\"Numele de utilizator al operatorului IRC\"],\"yrpRsQ\":[\"Sortare după nume\"],\"yz7wBu\":[\"Închide\"],\"zJw+jA\":[\"setează modul: \",[\"0\"]],\"zbymaY\":[\"acum \",[\"0\"],\"min\"],\"zebeLu\":[\"Introdu numele de utilizator oper\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/ro/messages.po b/src/locales/ro/messages.po
index de442e81..c327b246 100644
--- a/src/locales/ro/messages.po
+++ b/src/locales/ro/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - Aducem IRC în viitor"
msgid "— open in viewer"
msgstr "— deschide în vizualizator"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(moștenește)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(nemodificat)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} și {1} scriu..."
msgid "{0} is typing..."
msgstr "{0} scrie..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} rețea{1} — alegeți una pentru a vă alătura"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -115,7 +131,7 @@ msgstr "*spam*"
#: src/components/ui/InvitationsPanel.tsx
msgid "#channel (optional)"
-msgstr "#canal (opțional)"
+msgstr "#canal (opÈional)"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "#new-channel-name"
@@ -205,6 +221,12 @@ msgstr "Adaugă mască de invitație (ex. nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "Adaugă server IRC"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Adaugă rețea"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Adaugă regulă"
msgid "Add Server"
msgstr "Adaugă server"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Adaugă prima ta rețea"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Detalii suplimentare"
@@ -377,6 +403,10 @@ msgstr "Înapoi"
msgid "Back to image"
msgstr "Înapoi la imagine"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Înapoi la lista de rețele"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "Banează {username} după hostmask (împiedică reconectarea de pe același IP/host)"
@@ -424,6 +454,10 @@ msgstr "Răsfoiește toate canalele de pe server"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Anulează conexiunea"
msgid "Cancel reply"
msgstr "Anulează răspunsul"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Schimbați culoarea de accent"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Schimbă numele canalului (numai operatori)"
@@ -564,7 +603,7 @@ msgstr "Șterge căutarea"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "Faceți clic din nou pentru a confirma"
+msgstr "FaceÈi clic din nou pentru a confirma"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "Limită clienți (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -666,9 +707,10 @@ msgstr "Configurează sunetele de notificare și evidențierile"
#: src/components/ui/InvitationsPanel.tsx
msgid "Confirm?"
-msgstr "Confirmați?"
+msgstr "ConfirmaÈi?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Conectare"
@@ -711,11 +753,11 @@ msgstr "Copiat"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy"
-msgstr "Copiază"
+msgstr "CopiazÄ"
#: src/components/ui/RawLogViewer.tsx
msgid "Copy all"
-msgstr "Copiază tot"
+msgstr "CopiazÄ tot"
#: src/components/message/JsonLogMessage.tsx
msgid "Copy entire JSON"
@@ -731,7 +773,7 @@ msgstr "Copiați JSON"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy link"
-msgstr "Copiază linkul"
+msgstr "CopiazÄ linkul"
#: src/components/ui/ExternalLinkWarningModal.tsx
msgid "Copy URL"
@@ -739,15 +781,15 @@ msgstr "Copiază URL"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create"
-msgstr "Creează"
+msgstr "CreeazÄ"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "Creează un nou link de invitație"
+msgstr "CreeazÄ un nou link de invitaÈie"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "Creează și administrează linkurile tale de invitație"
+msgstr "CreeazÄ Èi administreazÄ linkurile tale de invitaÈie"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,27 +856,52 @@ msgstr "Șterge canal"
msgid "Delete message"
msgstr "Șterge mesaj"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Șterge rețeaua"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Șterge conversația privată"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "Șterge această invitație"
+msgstr "Èterge aceastÄ invitaÈie"
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "Ștergeți acest mesaj? Această acțiune nu poate fi anulată."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Ștergeți această rețea?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "Descriere (opțional, ex. „Testeri beta T3”)"
+msgstr "Descriere (opÈional, ex. âTesteri beta T3â)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Deconectează"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "Deconectați <0>{0}0>?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Deconectați de la bouncerul soju?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Deconectați rețeaua?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Descoperă"
@@ -891,20 +958,31 @@ msgstr "Descarcă"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "Plasează fișierele pentru a le încărca"
+msgstr "PlaseazÄ fiÈierele pentru a le încÄrca"
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "ex. <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "ex., 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Editează"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "Editează {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Editați profilul"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "ACASĂ"
msgid "Homepage"
msgstr "Pagină principală"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Gazdă"
@@ -1323,7 +1402,7 @@ msgstr "Părăsește canalul"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Lasă canalul gol pentru o invitație generică în rețea. Descrierea este doar pentru evidența ta — vizibilă doar pentru tine în această listă."
+msgstr "LasÄ canalul gol pentru o invitaÈie genericÄ Ã®n reÈea. Descrierea este doar pentru evidenÈa ta â vizibilÄ doar pentru tine în aceastÄ listÄ."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "A părăsit canalul"
msgid "Let others know when you are typing"
msgstr "Anunțați ceilalți când scrieți"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Previzualizare link"
@@ -1375,6 +1458,10 @@ msgstr "Se încarcă GIF-urile..."
msgid "Loading more channels..."
msgstr "Se încarcă mai multe canale..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Se încarcă rețelele de pe bouncer-ul tău…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "Se încarcă datele WHOIS..."
@@ -1563,12 +1650,23 @@ msgstr "Nume:"
#: src/components/ui/InvitationsPanel.tsx
msgid "network"
-msgstr "rețea"
+msgstr "reÈea"
+
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Rețea legată prin bouncerul soju"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Nume rețea"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Rețele pe {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Mesaj direct nou"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (ex., spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Niciun fișier ales"
msgid "No flood profile"
msgstr "Niciun profil anti-flood"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "nicio gazdă setată"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Nu s-au găsit invitații"
@@ -1672,7 +1775,7 @@ msgstr "Nu sunt încărcate previzualizări media."
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "Niciun trafic IRC brut capturat încă. Încearcă să te conectezi sau să trimiți un mesaj."
+msgstr "Niciun trafic IRC brut capturat încÄ. ÃncearcÄ sÄ te conectezi sau sÄ trimiÈi un mesaj."
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "Niciun rezultat găsit"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "Niciun server nu este selectat. Alege mai întâi un server din bara laterală; linkurile de invitație sunt gestionate per server."
+msgstr "Niciun server nu este selectat. Alege mai întâi un server din bara lateralÄ; linkurile de invitaÈie sunt gestionate per server."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "Niciun subiect setat"
msgid "No unread mentions or messages"
msgstr "Nicio mențiune sau mesaj necitit"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Încă nu există rețele upstream."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Niciun utilizator disponibil"
@@ -1784,6 +1891,10 @@ msgstr "Oops! Rețeaua s-a împărțit! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Deschide"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Deschide setările de configurare ale canalului"
@@ -1887,6 +1998,10 @@ msgstr "Fixează conversația privată"
msgid "Pin this private message conversation"
msgstr "Fixează această conversație privată"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Text simplu"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "Mesaj privat"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Port"
@@ -2006,6 +2122,7 @@ msgstr "a reacționat la acest mesaj"
msgid "Read more"
msgstr "Citește mai mult"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Motiv"
msgid "Reason (optional)"
msgstr "Motiv (opțional)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Reconectează-te la server"
@@ -2031,7 +2149,7 @@ msgstr "Reconectează-te la server"
#: src/components/ui/InvitationsPanel.tsx
#: src/components/ui/InvitationsPanel.tsx
msgid "Refresh"
-msgstr "Reîmprospătează"
+msgstr "ReîmprospÄteazÄ"
#: src/components/ui/AddServerModal.tsx
msgid "Register for an account"
@@ -2095,6 +2213,7 @@ msgstr "Reguli"
msgid "Safe"
msgstr "Sigur"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Derulare"
msgid "Select a channel"
msgstr "Selectează un canal"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Selectați o rețea"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Selectează un membru"
@@ -2276,6 +2399,10 @@ msgstr "Operatorii de server din rețea pot citi mesajele tale"
msgid "Server Password"
msgstr "Parolă server"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Parolă server (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Comunicarea server-la-server poate folosi conexiuni necriptate"
@@ -2380,6 +2507,12 @@ msgstr "Conectat la"
msgid "Software:"
msgstr "Software:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "bouncer soju (control)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Sortare după nume"
@@ -2457,7 +2590,11 @@ msgstr "Această imagine a expirat"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "Atâtea persoane s-au înregistrat prin acest link"
+msgstr "Atâtea persoane s-au înregistrat prin acest link"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Aceasta elimină rețeaua din bouncerul tău soju. Pentru a o folosi din nou, va trebui să o adăugați înapoi."
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "Acest server nu acceptă metadate extinse de profil (extensia IRCv3 META
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Acest server nu acceptă linkuri de invitație (capabilitatea<0>obby.world/invitation0>nu este anunțată). Poți discuta în continuare normal; acest panou este pentru rețelele bazate pe obbyircd."
+msgstr "Acest server nu acceptÄ linkuri de invitaÈie (capabilitatea<0>obby.world/invitation0>nu este anunÈatÄ). PoÈi discuta în continuare normal; acest panou este pentru reÈelele bazate pe obbyircd."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Acest server acceptă doar un tip de conexiune"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Aceasta va închide și cele {0} rețele legate de mai jos."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Aceasta va închide și rețeaua legată de mai jos."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Timp (min)"
@@ -2479,6 +2625,10 @@ msgstr "Timp (min)"
msgid "Time Window (seconds)"
msgstr "Fereastră de timp (secunde)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Subiect:"
msgid "Total: {0}"
msgstr "Total: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Transport"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Surse de încredere"
@@ -2641,6 +2795,7 @@ msgstr "Profil utilizator"
msgid "User Settings"
msgstr "Setări utilizator"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Larg – Domeniu de protecție mai amplu"
msgid "Will default to 'no reason' if left empty"
msgstr "Va fi implicit „niciun motiv\" dacă este lăsat gol"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Da, șterge"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "Ai modificări nesalvate. Ești sigur că vrei să închizi fără să s
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "Nu ai creat încă niciun link de invitație. Folosește formularul de mai sus pentru a-l crea pe primul."
+msgstr "Nu ai creat încÄ niciun link de invitaÈie. FoloseÈte formularul de mai sus pentru a-l crea pe primul."
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "L-ai invitat pe {target} să se alăture la {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Sunteți conectat la <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Parola contului dvs. pentru autentificare"
@@ -2822,13 +2986,17 @@ msgstr "Parola contului dvs. pentru autentificare"
msgid "Your account username for authentication"
msgstr "Numele de utilizator al contului dvs. pentru autentificare"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Bouncer-ul tău nu are încă nicio rețea. Adaugă una pentru a începe."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Pseudonimul dvs. implicit pentru toate serverele"
#: src/components/ui/InvitationsPanel.tsx
msgid "Your invite links"
-msgstr "Linkurile tale de invitație"
+msgstr "Linkurile tale de invitaÈie"
#: src/components/ui/UserSettings.tsx
msgid "Your messages and settings are stored locally on your device"
diff --git a/src/locales/ru/messages.mjs b/src/locales/ru/messages.mjs
index 0f0fe7a3..151434c8 100644
--- a/src/locales/ru/messages.mjs
+++ b/src/locales/ru/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Неверный формат шаблона. Используйте формат nick!user@host (допускаются маски *)\"],\"+6NQQA\":[\"Канал общей поддержки\"],\"+6NyRG\":[\"Клиент\"],\"+K0AvT\":[\"Отключиться\"],\"+cyFdH\":[\"Сообщение по умолчанию при переходе в режим отсутствия\"],\"+mVPqU\":[\"Отображать форматирование Markdown в сообщениях\"],\"+vqCJH\":[\"Имя пользователя вашего аккаунта для аутентификации\"],\"+yPBXI\":[\"Выбрать файл\"],\"+zy2Nq\":[\"Тип\"],\"/09cao\":[\"Низкий уровень безопасности соединения (уровень \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Пользователи вне канала не могут отправлять в него сообщения\"],\"/4C8U0\":[\"Копировать всё\"],\"/6BzZF\":[\"Показать/скрыть список участников\"],\"/AkXyp\":[\"Подтвердить?\"],\"/TNOPk\":[\"Пользователь отсутствует\"],\"/XQgft\":[\"Обзор\"],\"/cF7Rs\":[\"Громкость\"],\"/dqduX\":[\"Следующая страница\"],\"/fc3q4\":[\"Весь контент\"],\"/kISDh\":[\"Включить звуки уведомлений\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Аудио\"],\"/rfkZe\":[\"Воспроизводить звуки для упоминаний и сообщений\"],\"0/0ZGA\":[\"Маска имени канала\"],\"0D6j7U\":[\"Подробнее о пользовательских правилах →\"],\"0XsHcR\":[\"Исключить пользователя\"],\"0ZpE//\":[\"Сортировать по пользователям\"],\"0bEPwz\":[\"Отметиться как отсутствующий\"],\"0dGkPt\":[\"Развернуть список каналов\"],\"0gS7M5\":[\"Отображаемое имя\"],\"0kS+M8\":[\"ПримерНЕТ\"],\"0rgoY7\":[\"Подключайтесь только к выбранным вами серверам\"],\"0wdd7X\":[\"Войти\"],\"0wkVYx\":[\"Личные сообщения\"],\"111uHX\":[\"Предпросмотр ссылки\"],\"196EG4\":[\"Удалить личную переписку\"],\"1DSr1i\":[\"Зарегистрировать аккаунт\"],\"1O/24y\":[\"Показать/скрыть список каналов\"],\"1VPJJ2\":[\"Предупреждение о внешней ссылке\"],\"1ZC/dv\":[\"Нет непрочитанных упоминаний или сообщений\"],\"1pO1zi\":[\"Необходимо указать имя сервера\"],\"1uwfzQ\":[\"Просмотреть тему канала\"],\"268g7c\":[\"Введите отображаемое имя\"],\"2F9+AZ\":[\"Необработанный IRC-трафик пока не зафиксирован. Попробуйте подключиться или отправить сообщение.\"],\"2FOFq1\":[\"Операторы серверов сети потенциально могут читать ваши сообщения\"],\"2FYpfJ\":[\"Ещё\"],\"2HF1Y2\":[[\"inviter\"],\" пригласил \",[\"target\"],\" присоединиться к \",[\"channel\"]],\"2I70QL\":[\"Просмотреть информацию профиля пользователя\"],\"2QYdmE\":[\"Пользователи:\"],\"2QpEjG\":[\"вышел\"],\"2YE223\":[\"Сообщение #\",[\"0\"],\" (Enter — новая строка, Shift+Enter — отправить)\"],\"2bimFY\":[\"Использовать пароль сервера\"],\"2iTmdZ\":[\"Локальное хранилище:\"],\"2odkwe\":[\"Строгий — более агрессивная защита\"],\"2uDhbA\":[\"Введите имя пользователя для приглашения\"],\"2ygf/L\":[\"← Назад\"],\"2zEgxj\":[\"Поиск GIF...\"],\"3RdPhl\":[\"Переименовать канал\"],\"3THokf\":[\"Пользователь с голосом\"],\"3TSz9S\":[\"Свернуть\"],\"3jBDvM\":[\"Отображаемое имя канала\"],\"3ryuFU\":[\"Необязательные отчёты о сбоях для улучшения приложения\"],\"3uBF/8\":[\"Закрыть просмотрщик\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Введите имя аккаунта...\"],\"4/Rr0R\":[\"Пригласить пользователя в текущий канал\"],\"4EZrJN\":[\"Правила\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Профиль флуда (+F)\"],\"4RZQRK\":[\"Чем ты занимаешься?\"],\"4hfTrB\":[\"Никнейм\"],\"4n99LO\":[\"Уже в \",[\"0\"]],\"4t6vMV\":[\"Автоматически переключаться на однострочный режим для коротких сообщений\"],\"4vsHmf\":[\"Время (мин)\"],\"5+INAX\":[\"Выделять сообщения, в которых упоминается ваш никнейм\"],\"5R5Pv/\":[\"Имя оператора\"],\"678PKt\":[\"Название сети\"],\"6Aih4U\":[\"Не в сети\"],\"6CO3WE\":[\"Пароль для входа в канал. Оставьте пустым, чтобы удалить ключ.\"],\"6HhMs3\":[\"Сообщение при выходе\"],\"6V3Ea3\":[\"Скопировано\"],\"6lGV3K\":[\"Свернуть\"],\"6yFOEi\":[\"Введите пароль опера...\"],\"7+IHTZ\":[\"Файл не выбран\"],\"73hrRi\":[\"nick!user@host (например: spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Отправить личное сообщение\"],\"7U1W7c\":[\"Очень мягкий\"],\"7Y1YQj\":[\"Имя:\"],\"7YHArF\":[\"— открыть в просмотрщике\"],\"7fjnVl\":[\"Поиск пользователей...\"],\"7jL88x\":[\"Удалить это сообщение? Это действие нельзя отменить.\"],\"7nGhhM\":[\"О чём вы думаете?\"],\"7sEpu1\":[\"Участники — \",[\"0\"]],\"7sNhEz\":[\"Имя пользователя\"],\"8H0Q+x\":[\"Подробнее о профилях →\"],\"8Phu0A\":[\"Показывать, когда пользователи меняют никнейм\"],\"8XTG9e\":[\"Введите пароль оператора\"],\"8XsV2J\":[\"Повторить отправку\"],\"8ZsakT\":[\"Пароль\"],\"8kR84m\":[\"Вы собираетесь открыть внешнюю ссылку:\"],\"8lCgih\":[\"Удалить правило\"],\"8o3dPc\":[\"Перетащите файлы для загрузки\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"присоединился\"],\"few\":[\"присоединился \",[\"joinCount\"],\" раза\"],\"many\":[\"присоединился \",[\"joinCount\"],\" раз\"],\"other\":[\"присоединился \",[\"joinCount\"],\" раза\"]}]],\"9BMLnJ\":[\"Переподключиться к серверу\"],\"9OEgyT\":[\"Добавить реакцию\"],\"9PQ8m2\":[\"G-Line (глобальный бан)\"],\"9Qs99X\":[\"Email:\"],\"9QupBP\":[\"Удалить шаблон\"],\"9bG48P\":[\"Отправка\"],\"9f5f0u\":[\"Вопросы о конфиденциальности? Свяжитесь с нами:\"],\"9unqs3\":[\"Отсутствие:\"],\"9v3hwv\":[\"Серверы не найдены.\"],\"9zb2WA\":[\"Подключение\"],\"A1taO8\":[\"Поиск\"],\"A2adVi\":[\"Отправлять уведомления о наборе текста\"],\"A9Rhec\":[\"Имя канала\"],\"AWOSPo\":[\"Увеличить\"],\"AXSpEQ\":[\"Войти как оператор при подключении\"],\"AeXO77\":[\"Аккаунт\"],\"AhNP40\":[\"Перемотка\"],\"Ai2U7L\":[\"Хост\"],\"AjBQnf\":[\"Изменил никнейм\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Отменить ответ\"],\"ApSx0O\":[\"Найдено \",[\"0\"],\" сообщений, соответствующих \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Результаты не найдены\"],\"AyNqAB\":[\"Отображать все события сервера в чате\"],\"B/QqGw\":[\"Отошёл от клавиатуры\"],\"B8AaMI\":[\"Это поле обязательно для заполнения\"],\"BA2c49\":[\"Сервер не поддерживает расширенную фильтрацию LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" и ещё \",[\"3\"],\" печатают...\"],\"BGul2A\":[\"У вас есть несохранённые изменения. Вы уверены, что хотите закрыть без сохранения?\"],\"BIf9fi\":[\"Ваше статусное сообщение\"],\"BPm98R\":[\"Сервер не выбран. Сначала выберите сервер на боковой панели; пригласительные ссылки управляются отдельно для каждого сервера.\"],\"BZz3md\":[\"Ваш личный сайт\"],\"Bgm/H7\":[\"Разрешить ввод нескольких строк текста\"],\"BiQIl1\":[\"Закрепить эту личную переписку\"],\"BlNZZ2\":[\"Нажмите, чтобы перейти к сообщению\"],\"Bowq3c\":[\"Только операторы могут изменять тему канала\"],\"Btozzp\":[\"Срок действия этого изображения истёк\"],\"Bycfjm\":[\"Всего: \",[\"0\"]],\"C6IBQc\":[\"Копировать весь JSON\"],\"C9L9wL\":[\"Сбор данных\"],\"CDq4wC\":[\"Модерировать пользователя\"],\"CHVRxG\":[\"Сообщение @\",[\"0\"],\" (Shift+Enter — новая строка)\"],\"CN9zdR\":[\"Необходимо указать имя и пароль оператора\"],\"CW3sYa\":[\"Добавить реакцию \",[\"emoji\"]],\"CaAkqd\":[\"Показывать выходы\"],\"CbvaYj\":[\"Забанить по никнейму\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Выберите канал\"],\"CsekCi\":[\"Обычный\"],\"D+NlUC\":[\"Система\"],\"D28t6+\":[\"подключился и отключился\"],\"DB8zMK\":[\"Применить\"],\"DBcWHr\":[\"Пользовательский файл звука уведомления\"],\"DTy9Xw\":[\"Предпросмотр медиа\"],\"Dj4pSr\":[\"Выберите надёжный пароль\"],\"Du+zn+\":[\"Поиск...\"],\"Du2T2f\":[\"Настройка не найдена\"],\"DwsSVQ\":[\"Применить фильтры и обновить\"],\"E3W/zd\":[\"Никнейм по умолчанию\"],\"E6nRW7\":[\"Копировать URL\"],\"E703RG\":[\"Режимы:\"],\"EAeu1Z\":[\"Отправить приглашение\"],\"EFKJQT\":[\"Настройка\"],\"EGPQBv\":[\"Пользовательские правила флуда (+f)\"],\"ELik0r\":[\"Просмотреть полную политику конфиденциальности\"],\"EPbeC2\":[\"Просмотреть или изменить тему канала\"],\"EQCDNT\":[\"Введите имя пользователя опера...\"],\"EUvulZ\":[\"Найдено 1 сообщение, соответствующее \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Следующее изображение\"],\"EdQY6l\":[\"Нет\"],\"EnqLYU\":[\"Поиск серверов...\"],\"F0OKMc\":[\"Редактировать сервер\"],\"F6Int2\":[\"Включить выделения\"],\"FDoLyE\":[\"Макс. пользователей\"],\"FUU/hZ\":[\"Управляет количеством внешних медиафайлов, загружаемых в чат.\"],\"Fdp03t\":[\"вкл\"],\"FfPWR0\":[\"Диалог\"],\"FjkaiT\":[\"Уменьшить\"],\"FlqOE9\":[\"Что это означает:\"],\"FolHNl\":[\"Управление аккаунтом и аутентификацией\"],\"Fp2Dif\":[\"Покинул сервер\"],\"G5KmCc\":[\"GZ-Line (глобальная Z-Line)\"],\"GDs0lz\":[\"<0>Риск:0> Конфиденциальная информация (сообщения, личные переписки, данные аутентификации) может быть раскрыта сетевым администраторам или злоумышленникам, находящимся между IRC-серверами.\"],\"GR+2I3\":[\"Добавить маску приглашения (например: nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Закрыть всплывающие уведомления сервера\"],\"GdhD7H\":[\"Нажмите ещё раз для подтверждения\"],\"GlHnXw\":[\"Смена ника не удалась: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Предпросмотр:\"],\"GtmO8/\":[\"от\"],\"GtuHUQ\":[\"Переименовать этот канал на сервере. Все пользователи увидят новое имя.\"],\"GuGfFX\":[\"Включить/выключить поиск\"],\"GxkJXS\":[\"Загрузка...\"],\"GzbwnK\":[\"Присоединился к каналу\"],\"GzsUDB\":[\"Расширенный профиль\"],\"H/PnT8\":[\"Вставить эмодзи\"],\"H6Izzl\":[\"Ваш предпочтительный цветовой код\"],\"H9jIv+\":[\"Показывать входы/выходы\"],\"HAKBY9\":[\"Загрузить файлы\"],\"HdE1If\":[\"Канал\"],\"Hk4AW9\":[\"Ваше предпочтительное отображаемое имя\"],\"HmHDk7\":[\"Выбрать участника\"],\"HrQzPU\":[\"Каналы на \",[\"networkName\"]],\"I2tXQ5\":[\"Сообщение @\",[\"0\"],\" (Enter — новая строка, Shift+Enter — отправить)\"],\"I6bw/h\":[\"Забанить пользователя\"],\"I92Z+b\":[\"Включить уведомления\"],\"I9D72S\":[\"Вы уверены, что хотите удалить это сообщение? Это действие нельзя отменить.\"],\"IA+1wo\":[\"Показывать, когда пользователей исключают из каналов\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Инфо:\"],\"IUwGEM\":[\"Сохранить изменения\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" и \",[\"2\"],\" печатают...\"],\"IgrLD/\":[\"Пауза\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Ответить\"],\"IoHMnl\":[\"Максимальное значение: \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Подключение...\"],\"J5T9NW\":[\"Информация о пользователе\"],\"J8Y5+z\":[\"Упс! Разрыв сети! ⚠️\"],\"JBHkBA\":[\"Покинул канал\"],\"JCwL0Q\":[\"Укажите причину (необязательно)\"],\"JFciKP\":[\"Переключить\"],\"JXGkhG\":[\"Изменить имя канала (только для операторов)\"],\"JcD7qf\":[\"Другие действия\"],\"JdkA+c\":[\"Секретный (+s)\"],\"Jmu12l\":[\"Каналы сервера\"],\"JvQ++s\":[\"Включить Markdown\"],\"K2jwh/\":[\"Данные WHOIS недоступны\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Удалить сообщение\"],\"KKBlUU\":[\"Встроить\"],\"KM0pLb\":[\"Добро пожаловать в канал!\"],\"KR6W2h\":[\"Перестать игнорировать пользователя\"],\"KV+Bi1\":[\"Только по приглашению (+i)\"],\"KdCtwE\":[\"Сколько секунд отслеживать флуд-активность до сброса счётчиков\"],\"Kkezga\":[\"Пароль сервера\"],\"KsiQ/8\":[\"Для входа в канал необходимо приглашение\"],\"L+gB/D\":[\"Информация о канале\"],\"LC1a7n\":[\"IRC-сервер сообщил о низком уровне безопасности межсерверных соединений. Это означает, что при передаче ваших сообщений между IRC-серверами сети они могут быть недостаточно зашифрованы или SSL/TLS-сертификаты могут не проверяться должным образом.\"],\"LNfLR5\":[\"Показывать исключения\"],\"LQb0W/\":[\"Показывать все события\"],\"LU7/yA\":[\"Альтернативное имя для отображения в интерфейсе. Может содержать пробелы, эмодзи и специальные символы. Настоящее имя канала (\",[\"channelName\"],\") по-прежнему будет использоваться для IRC-команд.\"],\"LUb9O7\":[\"Необходимо указать корректный порт сервера\"],\"LV4fT6\":[\"Описание (необязательно, например «Бета-тестеры Q3»)\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Политика конфиденциальности\"],\"LcuSDR\":[\"Управление информацией профиля и метаданными\"],\"LqLS9B\":[\"Показывать смену никнейма\"],\"LsDQt2\":[\"Настройки канала\"],\"LtI9AS\":[\"Владелец\"],\"LuNhhL\":[\"отреагировал на это сообщение\"],\"M/AZNG\":[\"URL вашего аватара\"],\"M/WIer\":[\"Отправить сообщение\"],\"M8er/5\":[\"Имя:\"],\"MHk+7g\":[\"Предыдущее изображение\"],\"MRorGe\":[\"Написать в личку\"],\"MVbSGP\":[\"Временное окно (секунды)\"],\"MkpcsT\":[\"Ваши сообщения и настройки хранятся локально на вашем устройстве\"],\"N/hDSy\":[\"Пометить как бота — обычно «on» или пусто\"],\"N7TQbE\":[\"Пригласить пользователя в \",[\"channelName\"]],\"NCca/o\":[\"Введите ник по умолчанию...\"],\"Nqs6B9\":[\"Показывает весь внешний медиаконтент. Любой URL может вызвать запрос к неизвестному серверу.\"],\"Nt+9O7\":[\"Использовать WebSocket вместо обычного TCP\"],\"NxIHzc\":[\"Отключить пользователя\"],\"O+v/cL\":[\"Просмотреть все каналы на сервере\"],\"ODwSCk\":[\"Отправить GIF\"],\"OGQ5kK\":[\"Настройка звуков уведомлений и выделений\"],\"OIPt1Z\":[\"Показать или скрыть боковую панель списка участников\"],\"OKSNq/\":[\"Очень строгий\"],\"ONWvwQ\":[\"Загрузить\"],\"OVKoQO\":[\"Пароль вашего аккаунта для аутентификации\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Перенося IRC в будущее\"],\"OhCpra\":[\"Задать тему…\"],\"OkltoQ\":[\"Забанить \",[\"username\"],\" по никнейму (запрещает переподключение с тем же ником)\"],\"P+t/Te\":[\"Нет дополнительных данных\"],\"P42Wcc\":[\"Безопасно\"],\"PD38l0\":[\"Предпросмотр аватара канала\"],\"PD9mEt\":[\"Введите сообщение...\"],\"PPqfdA\":[\"Открыть настройки конфигурации канала\"],\"PSCjfZ\":[\"Тема, которая будет отображаться для этого канала. Тему видят все пользователи.\"],\"PZCecv\":[\"Предпросмотр PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 раз\"],\"few\":[[\"c\"],\" раза\"],\"many\":[[\"c\"],\" раз\"],\"other\":[[\"c\"],\" раза\"]}]],\"PguS2C\":[\"Добавить маску исключения (например: nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Показано \",[\"displayedChannelsCount\"],\" из \",[\"0\"],\" каналов\"],\"PqhVlJ\":[\"Забанить пользователя (по hostmask)\"],\"Q+chwU\":[\"Имя пользователя:\"],\"Q2QY4/\":[\"Удалить это приглашение\"],\"Q6hhn8\":[\"Настройки\"],\"QF4a34\":[\"Введите имя пользователя\"],\"QGqSZ2\":[\"Цвет и форматирование\"],\"QJQd1J\":[\"Редактировать профиль\"],\"QSzGDE\":[\"Не активен\"],\"QUlny5\":[\"Добро пожаловать на \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Читать далее\"],\"QuSkCF\":[\"Фильтр каналов...\"],\"QwUrDZ\":[\"изменил тему на: \",[\"topic\"]],\"R0UH07\":[\"Изображение \",[\"0\"],\" из \",[\"1\"]],\"R7SsBE\":[\"Выкл. звук\"],\"R8rf1X\":[\"Нажмите, чтобы задать тему\"],\"RArB3D\":[\"был кикнут из \",[\"channelName\"],\" пользователем \",[\"username\"]],\"RI3cWd\":[\"Откройте мир IRC вместе с ObsidianIRC\"],\"RIfHS5\":[\"Создать новую пригласительную ссылку\"],\"RMMaN5\":[\"Модерируемый (+m)\"],\"RWw9Lg\":[\"Закрыть диалог\"],\"RZ2BuZ\":[\"Регистрация аккаунта \",[\"account\"],\" требует подтверждения: \",[\"message\"]],\"RySp6q\":[\"Скрыть комментарии\"],\"SPKQTd\":[\"Необходимо указать никнейм\"],\"SPVjfj\":[\"Если оставить пустым, будет использоваться «без причины»\"],\"SQKPvQ\":[\"Пригласить пользователя\"],\"SkZcl+\":[\"Выберите заранее заданный профиль защиты от флуда. Эти профили предоставляют сбалансированные настройки защиты для различных сценариев использования.\"],\"Slr+3C\":[\"Мин. пользователей\"],\"Spnlre\":[\"Вы пригласили \",[\"target\"],\" присоединиться к \",[\"channel\"]],\"T/ckN5\":[\"Открыть в просмотрщике\"],\"T91vKp\":[\"Воспроизвести\"],\"TV2Wdu\":[\"Узнайте, как мы обрабатываем ваши данные и защищаем вашу конфиденциальность.\"],\"TgFpwD\":[\"Применяется...\"],\"TkzSFB\":[\"Нет изменений\"],\"TtserG\":[\"Введите настоящее имя\"],\"Ttz9J1\":[\"Введите пароль...\"],\"Tz0i8g\":[\"Настройки\"],\"U3pytU\":[\"Администратор\"],\"UDb2YD\":[\"Реакция\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"Вы ещё не создали ни одной пригласительной ссылки. Используйте форму выше, чтобы создать первую.\"],\"UGT5vp\":[\"Сохранить настройки\"],\"UV5hLB\":[\"Баны не найдены\"],\"Uaj3Nd\":[\"Статусные сообщения\"],\"Ue3uny\":[\"По умолчанию (без профиля)\"],\"UkARhe\":[\"Обычный — стандартная защита\"],\"Umn7Cj\":[\"Комментариев пока нет. Будьте первым!\"],\"UtUIRh\":[[\"0\"],\" старых сообщений\"],\"UwzP+U\":[\"Защищённое соединение\"],\"V0/A4O\":[\"Владелец канала\"],\"V4qgxE\":[\"Создан до (мин назад)\"],\"V8yTm6\":[\"Очистить поиск\"],\"VJMMyz\":[\"ObsidianIRC — IRC будущего\"],\"VJScHU\":[\"Причина\"],\"VLsmVV\":[\"Отключить уведомления\"],\"VbyRUy\":[\"Комментарии\"],\"Vmx0mQ\":[\"Установлено:\"],\"VqnIZz\":[\"Ознакомьтесь с нашей политикой конфиденциальности и практикой обработки данных\"],\"VrMygG\":[\"Минимальная длина: \",[\"0\"]],\"VrnTui\":[\"Ваши местоимения, отображаемые в профиле\"],\"W8E3qn\":[\"Аутентифицированный аккаунт\"],\"WAakm9\":[\"Удалить канал\"],\"WFxTHC\":[\"Добавить маску бана (например: nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Необходимо указать хост сервера\"],\"WRYdXW\":[\"Позиция в аудио\"],\"WUOH5B\":[\"Игнорировать пользователя\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Показать ещё 1 элемент\"],\"few\":[\"Показать ещё \",[\"1\"],\" элемента\"],\"many\":[\"Показать ещё \",[\"1\"],\" элементов\"],\"other\":[\"Показать ещё \",[\"1\"],\" элемента\"]}]],\"WYxRzo\":[\"Создание и управление вашими пригласительными ссылками\"],\"Wd38W1\":[\"Оставьте поле канала пустым для общего приглашения в сеть. Описание нужно только для ваших записей — оно видно только вам в этом списке.\"],\"Weq9zb\":[\"Основное\"],\"Wfj7Sk\":[\"Включить или отключить звуки уведомлений\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Профиль пользователя\"],\"X6S3lt\":[\"Поиск настроек, каналов, серверов...\"],\"XEHan5\":[\"Всё равно продолжить\"],\"XI1+wb\":[\"Неверный формат\"],\"XIXeuC\":[\"Сообщение @\",[\"0\"]],\"XMS+k4\":[\"Начать личный чат\"],\"XWgxXq\":[\"Альбом\"],\"Xd7+IT\":[\"Открепить личный чат\"],\"Xm/s+u\":[\"Отображение\"],\"Xp2n93\":[\"Показывает медиафайлы с доверенного файлового хоста вашего сервера. Запросы к внешним сервисам не выполняются.\"],\"XvjC4F\":[\"Сохранение...\"],\"Y/qryO\":[\"Пользователи по вашему запросу не найдены\"],\"YAqRpI\":[\"Регистрация аккаунта \",[\"account\"],\" успешна: \",[\"message\"]],\"YEfzvP\":[\"Защищённая тема (+t)\"],\"YQOn6a\":[\"Свернуть список участников\"],\"YRCoE9\":[\"Оператор канала\"],\"YURQaF\":[\"Просмотреть профиль\"],\"YdBSvr\":[\"Управление отображением медиа и внешнего контента\"],\"Yj6U3V\":[\"Нет центрального сервера:\"],\"YjvpGx\":[\"Местоимения\"],\"YqH4l4\":[\"Без ключа\"],\"YyUPpV\":[\"Аккаунт:\"],\"ZJSWfw\":[\"Сообщение, отображаемое при отключении от сервера\"],\"ZR1dJ4\":[\"Приглашения\"],\"ZdWg0V\":[\"Открыть в браузере\"],\"ZhRBbl\":[\"Поиск сообщений…\"],\"Zmcu3y\":[\"Расширенные фильтры\"],\"a2/8e5\":[\"Тема установлена после (мин назад)\"],\"aHKcKc\":[\"Предыдущая страница\"],\"aJTbXX\":[\"Пароль оператора\"],\"aQryQv\":[\"Такой шаблон уже существует\"],\"aW9pLN\":[\"Максимальное количество пользователей в канале. Оставьте пустым для снятия ограничения.\"],\"ah4fmZ\":[\"Также показывает превью с YouTube, Vimeo, SoundCloud и других известных сервисов.\"],\"aifXak\":[\"В этом канале нет медиафайлов\"],\"ap2zBz\":[\"Мягкий\"],\"az8lvo\":[\"Выкл.\"],\"azXSNo\":[\"Развернуть список участников\"],\"azdliB\":[\"Войти в аккаунт\"],\"b26wlF\":[\"она/её\"],\"bD/+Ei\":[\"Строгий\"],\"bQ6BJn\":[\"Настройте подробные правила защиты от флуда. Каждое правило задаёт тип активности для мониторинга и действие при превышении порога.\"],\"beV7+y\":[\"Пользователь получит приглашение вступить в \",[\"channelName\"],\".\"],\"bk84cH\":[\"Сообщение об отсутствии\"],\"bkHdLj\":[\"Добавить IRC-сервер\"],\"bmQLn5\":[\"Добавить правило\"],\"bwRvnp\":[\"Действие\"],\"c8+EVZ\":[\"Верифицированный аккаунт\"],\"cGYUlD\":[\"Предпросмотр медиа не загружается.\"],\"cLF98o\":[\"Показать комментарии (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Нет доступных пользователей\"],\"cSgpoS\":[\"Закрепить личный чат\"],\"cde3ce\":[\"Написать <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Копировать форматированный вывод\"],\"cl/A5J\":[\"Добро пожаловать на \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Удалить\"],\"coPLXT\":[\"Мы не храним ваши IRC-переписки на наших серверах\"],\"crYH/6\":[\"Плеер SoundCloud\"],\"d3sis4\":[\"Добавить сервер\"],\"d9aN5k\":[\"Удалить \",[\"username\"],\" из канала\"],\"dEgA5A\":[\"Отмена\"],\"dGi1We\":[\"Открепить эту личную переписку\"],\"dJVuyC\":[\"покинул \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"кому\"],\"dXqxlh\":[\"<0>⚠️ Угроза безопасности!0> Это соединение может быть уязвимо для перехвата или атак типа «человек посередине».\"],\"da9Q/R\":[\"Изменил режимы канала\"],\"dhJN3N\":[\"Показать комментарии\"],\"dj2xTE\":[\"Закрыть уведомление\"],\"dpCzmC\":[\"Настройки защиты от флуда\"],\"e9dQpT\":[\"Открыть эту ссылку в новой вкладке?\"],\"ePK91l\":[\"Изменить\"],\"eYBDuB\":[\"Загрузите изображение или укажите URL с необязательной подстановкой \",[\"size\"],\" для динамического масштабирования\"],\"edBbee\":[\"Забанить \",[\"username\"],\" по hostmask (запрещает переподключение с того же IP/хоста)\"],\"ekfzWq\":[\"Настройки пользователя\"],\"elPDWs\":[\"Настройте IRC-клиент под себя\"],\"eu2osY\":[\"<0>💡 Рекомендация:0> Продолжайте только если вы доверяете этому серверу и понимаете риски. Избегайте передачи конфиденциальной информации или паролей через это соединение.\"],\"euEhbr\":[\"Нажмите, чтобы войти в \",[\"channel\"]],\"ez3vLd\":[\"Включить многострочный ввод\"],\"f0J5Ki\":[\"Межсерверное взаимодействие может использовать незашифрованные соединения\"],\"f9BHJk\":[\"Предупредить пользователя\"],\"fDOLLd\":[\"Каналы не найдены.\"],\"ffzDkB\":[\"Анонимная аналитика:\"],\"fq1GF9\":[\"Показывать, когда пользователи отключаются от сервера\"],\"gEF57C\":[\"Этот сервер поддерживает только один тип подключения\"],\"gJuLUI\":[\"Список игнорирования\"],\"gNzMrk\":[\"Текущий аватар\"],\"gjPWyO\":[\"Введите ник...\"],\"gz6UQ3\":[\"Развернуть\"],\"h6razj\":[\"Исключить маску имени канала\"],\"hG6jnw\":[\"Тема не задана\"],\"hG89Ed\":[\"Изображение\"],\"hYgDIe\":[\"Создать\"],\"hZ6znB\":[\"Порт\"],\"ha+Bz5\":[\"например: 100:1440\"],\"he3ygx\":[\"Копировать\"],\"hehnjM\":[\"Количество\"],\"hzdLuQ\":[\"Говорить могут только пользователи с голосом или выше\"],\"i0qMbr\":[\"Главная\"],\"iDNBZe\":[\"Уведомления\"],\"iH8pgl\":[\"Назад\"],\"iL9SZg\":[\"Забанить пользователя (по никнейму)\"],\"iNt+3c\":[\"Вернуться к изображению\"],\"iQvi+a\":[\"Не предупреждать меня о низком уровне безопасности соединений для этого сервера\"],\"iSLIjg\":[\"Подключиться\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Хост сервера\"],\"idD8Ev\":[\"Сохранено\"],\"iivqkW\":[\"В сети с\"],\"ij+Elv\":[\"Предпросмотр изображения\"],\"ilIWp7\":[\"Включить/выключить уведомления\"],\"iuaqvB\":[\"Используйте * в качестве маски. Примеры: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Бот\"],\"j2DGR0\":[\"Забанить по hostmask\"],\"jA4uoI\":[\"Тема:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Причина (необязательно)\"],\"jUV7CU\":[\"Загрузить аватар\"],\"jW5Uwh\":[\"Управление загрузкой внешних медиафайлов. Выкл / Безопасно / Доверенные источники / Весь контент.\"],\"jXzms5\":[\"Параметры вложения\"],\"jZlrte\":[\"Цвет\"],\"jfC/xh\":[\"Контакт\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Загрузить старые сообщения\"],\"k3ID0F\":[\"Фильтр участников…\"],\"k65gsE\":[\"Подробнее\"],\"k7Zgob\":[\"Отменить подключение\"],\"kAVx5h\":[\"Приглашения не найдены\"],\"kCLEPU\":[\"Подключён к\"],\"kF5LKb\":[\"Игнорируемые шаблоны:\"],\"kGeOx/\":[\"Присоединиться к \",[\"0\"]],\"kITKr8\":[\"Загрузка режимов канала...\"],\"kPpPsw\":[\"Вы являетесь IRC-оператором\"],\"kWJmRL\":[\"ты\"],\"kfcRb0\":[\"Аватар\"],\"kjMqSj\":[\"Копировать JSON\"],\"krViRy\":[\"Нажмите для копирования как JSON\"],\"ks71ra\":[\"Исключения\"],\"kw4lRv\":[\"Полуоператор канала\"],\"kxgIRq\":[\"Выберите или добавьте канал для начала.\"],\"ky6dWe\":[\"Предпросмотр аватара\"],\"l+GxCv\":[\"Загрузка каналов...\"],\"l+IUVW\":[\"Верификация аккаунта \",[\"account\"],\" успешна: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"переподключился\"],\"few\":[\"переподключился \",[\"reconnectCount\"],\" раза\"],\"many\":[\"переподключился \",[\"reconnectCount\"],\" раз\"],\"other\":[\"переподключился \",[\"reconnectCount\"],\" раза\"]}]],\"l1l8sj\":[[\"0\"],\" дн. назад\"],\"l5NhnV\":[\"#канал (необязательно)\"],\"l5jmzx\":[[\"0\"],\" и \",[\"1\"],\" печатают...\"],\"lCF0wC\":[\"Обновить\"],\"lHy8N5\":[\"Загрузка дополнительных каналов...\"],\"lasgrr\":[\"использовано\"],\"lbpf14\":[\"Войти в \",[\"value\"]],\"lfFsZ4\":[\"Каналы\"],\"lkNdiH\":[\"Имя аккаунта\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Загрузить изображение\"],\"loQxaJ\":[\"Я вернулся\"],\"lvfaxv\":[\"ГЛАВНАЯ\"],\"m16xKo\":[\"Добавить\"],\"m8flAk\":[\"Предпросмотр (ещё не загружено)\"],\"mEPxTp\":[\"<0>⚠️ Будьте осторожны!0> Открывайте ссылки только из доверенных источников. Вредоносные ссылки могут угрожать вашей безопасности или конфиденциальности.\"],\"mHGdhG\":[\"Информация о сервере\"],\"mHS8lb\":[\"Сообщение #\",[\"0\"]],\"mMYBD9\":[\"Широкий — более широкая область защиты\"],\"mTGsPd\":[\"Тема канала\"],\"mU8j6O\":[\"Без внешних сообщений (+n)\"],\"mZp8FL\":[\"Автоматический возврат к однострочному режиму\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"Комментарии (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Пользователь аутентифицирован\"],\"mwtcGl\":[\"Закрыть комментарии\"],\"mzI/c+\":[\"Скачать\"],\"n3fGRk\":[\"установил \",[\"0\"]],\"nE9jsU\":[\"Мягкий — менее строгая защита\"],\"nNflMD\":[\"Покинуть канал\"],\"nPXkBi\":[\"Загрузка данных WHOIS...\"],\"nQnxxF\":[\"Сообщение #\",[\"0\"],\" (Shift+Enter — новая строка)\"],\"nWMRxa\":[\"Открепить\"],\"nkC032\":[\"Без профиля флуда\"],\"o69z4d\":[\"Отправить предупреждение пользователю \",[\"username\"]],\"o9ylQi\":[\"Найдите GIF для начала\"],\"oFGkER\":[\"Уведомления сервера\"],\"oOi11l\":[\"Прокрутить вниз\"],\"oPYIL5\":[\"сеть\"],\"oQEzQR\":[\"Новое DM\"],\"oXOSPE\":[\"В сети\"],\"oal760\":[\"Возможны атаки типа «человек посередине» на межсерверные соединения\"],\"oeqmmJ\":[\"Доверенные источники\"],\"optX0N\":[[\"0\"],\" ч. назад\"],\"ovBPCi\":[\"По умолчанию\"],\"p0Z69r\":[\"Шаблон не может быть пустым\"],\"p1KgtK\":[\"Не удалось загрузить аудио\"],\"p59pEv\":[\"Подробности\"],\"p7sRI6\":[\"Сообщать другим, когда вы печатаете\"],\"pBm1od\":[\"Секретный канал\"],\"pNmiXx\":[\"Ваш никнейм по умолчанию для всех серверов\"],\"pUUo9G\":[\"Хост:\"],\"pVGPmz\":[\"Пароль аккаунта\"],\"peNE68\":[\"Навсегда\"],\"plhHQt\":[\"Нет данных\"],\"pm6+q5\":[\"Предупреждение безопасности\"],\"pn5qSs\":[\"Дополнительная информация\"],\"q0cR4S\":[\"теперь известен как **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Канал не будет отображаться в командах LIST и NAMES\"],\"qLpTm/\":[\"Убрать реакцию \",[\"emoji\"]],\"qVkGWK\":[\"Закрепить\"],\"qY8wNa\":[\"Сайт\"],\"qb0xJ7\":[\"Используйте маски: * соответствует любой последовательности, ? — любому одному символу. Примеры: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Ключ канала (+k)\"],\"qtoOYG\":[\"Без ограничений\"],\"r1W2AS\":[\"Изображение с файлового хостинга\"],\"rIPR2O\":[\"Тема установлена до (мин назад)\"],\"rMMSYo\":[\"Максимальная длина: \",[\"0\"]],\"rWtzQe\":[\"Сеть разделилась и воссоединилась. ✅\"],\"rYG2u6\":[\"Пожалуйста, подождите...\"],\"rdUucN\":[\"Предпросмотр\"],\"rjGI/Q\":[\"Конфиденциальность\"],\"rk8iDX\":[\"Загрузка GIF...\"],\"rn6SBY\":[\"Вкл. звук\"],\"s/UKqq\":[\"Был исключён из канала\"],\"s8cATI\":[\"присоединился к \",[\"channelName\"]],\"sCO9ue\":[\"Соединение с <0>\",[\"serverName\"],\"0> имеет следующие проблемы безопасности:\"],\"sGH11W\":[\"Сервер\"],\"sHI1H+\":[\"теперь известен как **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" пригласил вас присоединиться к \",[\"channel\"]],\"sby+1/\":[\"Нажмите, чтобы скопировать\"],\"sfN25C\":[\"Ваше настоящее или полное имя\"],\"sliuzR\":[\"Открыть ссылку\"],\"sqrO9R\":[\"Пользовательские упоминания\"],\"sr6RdJ\":[\"Многострочный режим по Shift+Enter\"],\"swrCpB\":[\"Канал был переименован с \",[\"oldName\"],\" на \",[\"newName\"],\" пользователем \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Дополнительно\"],\"t/YqKh\":[\"Удалить\"],\"t47eHD\":[\"Ваш уникальный идентификатор на этом сервере\"],\"tAkAh0\":[\"URL с необязательной подстановкой \",[\"size\"],\" для динамического масштабирования. Пример: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Показать или скрыть боковую панель списка каналов\"],\"tfDRzk\":[\"Сохранить\"],\"tiBsJk\":[\"покинул \",[\"channelName\"]],\"tt4/UD\":[\"вышел (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Ник {nick} уже используется, повторная попытка с {newNick}\"],\"u0a8B4\":[\"Аутентифицироваться как IRC-оператор для административного доступа\"],\"u0rWFU\":[\"Создан после (мин назад)\"],\"u72w3t\":[\"Пользователи и шаблоны для игнорирования\"],\"u7jc2L\":[\"вышел\"],\"uAQUqI\":[\"Статус\"],\"uB85T3\":[\"Ошибка сохранения: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC-серверы:\"],\"ukyW4o\":[\"Ваши пригласительные ссылки\"],\"usSSr/\":[\"Масштаб\"],\"v7uvcf\":[\"Программа:\"],\"vE8kb+\":[\"Shift+Enter для новой строки (Enter отправляет)\"],\"vERlcd\":[\"Профиль\"],\"vK0RL8\":[\"Без темы\"],\"vSJd18\":[\"Видео\"],\"vXIe7J\":[\"Язык\"],\"vaHYxN\":[\"Настоящее имя\"],\"vhjbKr\":[\"Отсутствую\"],\"w4NYox\":[\"клиент \",[\"title\"]],\"w8xQRx\":[\"Неверное значение\"],\"wFjjxZ\":[\"был кикнут из \",[\"channelName\"],\" пользователем \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Исключения из банов не найдены\"],\"wPrGnM\":[\"Администратор канала\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Показывать, когда пользователи входят в каналы или покидают их\"],\"whqZ9r\":[\"Дополнительные слова или фразы для выделения\"],\"wm7RV4\":[\"Звук уведомления\"],\"wz/Yoq\":[\"Ваши сообщения могут быть перехвачены при передаче между серверами\"],\"x3+y8b\":[\"Столько человек зарегистрировалось по этой ссылке\"],\"xCJdfg\":[\"Очистить\"],\"xOTzt5\":[\"только что\"],\"xUHRTR\":[\"Автоматически аутентифицироваться как оператор при подключении\"],\"xWHwwQ\":[\"Баны\"],\"xYilR2\":[\"Медиа\"],\"xbi8D6\":[\"Этот сервер не поддерживает пригласительные ссылки (возможность<0>obby.world/invitation0>не объявлена). Вы по-прежнему можете общаться в чате как обычно; эта панель предназначена для сетей на базе obbyircd.\"],\"xceQrO\":[\"Поддерживаются только защищённые WebSocket-соединения\"],\"xdtXa+\":[\"имя-канала\"],\"xfXC7q\":[\"Текстовые каналы\"],\"xlCYOE\":[\"Загрузка сообщений...\"],\"xlhswE\":[\"Минимальное значение: \",[\"0\"]],\"xq97Ci\":[\"Добавить слово или фразу...\"],\"xuRqRq\":[\"Лимит пользователей (+l)\"],\"xwF+7J\":[[\"0\"],\" печатает...\"],\"y1eoq1\":[\"Копировать ссылку\"],\"yNeucF\":[\"Этот сервер не поддерживает расширенные метаданные профиля (расширение IRCv3 METADATA). Дополнительные поля, такие как аватар, отображаемое имя и статус, недоступны.\"],\"yPlrca\":[\"Аватар канала\"],\"yQE2r9\":[\"Загрузка\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Имя пользователя оператора\"],\"yYOzWD\":[\"логи\"],\"yfx9Re\":[\"Пароль IRC-оператора\"],\"ygCKqB\":[\"Стоп\"],\"ymDxJx\":[\"Имя пользователя IRC-оператора\"],\"yrpRsQ\":[\"Сортировать по имени\"],\"yz7wBu\":[\"Закрыть\"],\"zJw+jA\":[\"устанавливает режим: \",[\"0\"]],\"zbymaY\":[[\"0\"],\" мин. назад\"],\"zebeLu\":[\"Введите имя пользователя оператора\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Неверный формат шаблона. Используйте формат nick!user@host (допускаются маски *)\"],\"+6NQQA\":[\"Канал общей поддержки\"],\"+6NyRG\":[\"Клиент\"],\"+K0AvT\":[\"Отключиться\"],\"+cyFdH\":[\"Сообщение по умолчанию при переходе в режим отсутствия\"],\"+mVPqU\":[\"Отображать форматирование Markdown в сообщениях\"],\"+vqCJH\":[\"Имя пользователя вашего аккаунта для аутентификации\"],\"+yPBXI\":[\"Выбрать файл\"],\"+zy2Nq\":[\"Тип\"],\"/09cao\":[\"Низкий уровень безопасности соединения (уровень \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Пользователи вне канала не могут отправлять в него сообщения\"],\"/4C8U0\":[\"ÐопиÑоваÑÑ Ð²ÑÑ\"],\"/6BzZF\":[\"Показать/скрыть список участников\"],\"/AkXyp\":[\"ÐодÑвеÑдиÑÑ?\"],\"/TNOPk\":[\"Пользователь отсутствует\"],\"/XQgft\":[\"Обзор\"],\"/cF7Rs\":[\"Громкость\"],\"/dqduX\":[\"Следующая страница\"],\"/fc3q4\":[\"Весь контент\"],\"/kISDh\":[\"Включить звуки уведомлений\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Аудио\"],\"/rfkZe\":[\"Воспроизводить звуки для упоминаний и сообщений\"],\"0/0ZGA\":[\"Маска имени канала\"],\"0D6j7U\":[\"Подробнее о пользовательских правилах →\"],\"0XsHcR\":[\"Исключить пользователя\"],\"0ZpE//\":[\"Сортировать по пользователям\"],\"0bEPwz\":[\"Отметиться как отсутствующий\"],\"0dGkPt\":[\"Развернуть список каналов\"],\"0gS7M5\":[\"Отображаемое имя\"],\"0kS+M8\":[\"ПримерНЕТ\"],\"0rgoY7\":[\"Подключайтесь только к выбранным вами серверам\"],\"0wdd7X\":[\"Войти\"],\"0wkVYx\":[\"Личные сообщения\"],\"111uHX\":[\"Предпросмотр ссылки\"],\"196EG4\":[\"Удалить личную переписку\"],\"1DSr1i\":[\"Зарегистрировать аккаунт\"],\"1O/24y\":[\"Показать/скрыть список каналов\"],\"1TNIig\":[\"Открыть\"],\"1VPJJ2\":[\"Предупреждение о внешней ссылке\"],\"1ZC/dv\":[\"Нет непрочитанных упоминаний или сообщений\"],\"1pO1zi\":[\"Необходимо указать имя сервера\"],\"1uwfzQ\":[\"Просмотреть тему канала\"],\"268g7c\":[\"Введите отображаемое имя\"],\"2CEOW6\":[\"Сеть, связанная через bouncer soju\"],\"2F9+AZ\":[\"ÐеобÑабоÑаннÑй IRC-ÑÑаÑик пока не заÑикÑиÑован. ÐопÑобÑйÑе подклÑÑиÑÑÑÑ Ð¸Ð»Ð¸ оÑпÑавиÑÑ ÑообÑение.\"],\"2FOFq1\":[\"Операторы серверов сети потенциально могут читать ваши сообщения\"],\"2FYpfJ\":[\"Ещё\"],\"2HF1Y2\":[[\"inviter\"],\" пригласил \",[\"target\"],\" присоединиться к \",[\"channel\"]],\"2I70QL\":[\"Просмотреть информацию профиля пользователя\"],\"2QYdmE\":[\"Пользователи:\"],\"2QpEjG\":[\"вышел\"],\"2YE223\":[\"Сообщение #\",[\"0\"],\" (Enter — новая строка, Shift+Enter — отправить)\"],\"2bimFY\":[\"Использовать пароль сервера\"],\"2iTmdZ\":[\"Локальное хранилище:\"],\"2odkwe\":[\"Строгий — более агрессивная защита\"],\"2uDhbA\":[\"Введите имя пользователя для приглашения\"],\"2ygf/L\":[\"← Назад\"],\"2zEgxj\":[\"Поиск GIF...\"],\"3RdPhl\":[\"Переименовать канал\"],\"3THokf\":[\"Пользователь с голосом\"],\"3TSz9S\":[\"Свернуть\"],\"3jBDvM\":[\"Отображаемое имя канала\"],\"3ryuFU\":[\"Необязательные отчёты о сбоях для улучшения приложения\"],\"3uBF/8\":[\"Закрыть просмотрщик\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Введите имя аккаунта...\"],\"4/Rr0R\":[\"Пригласить пользователя в текущий канал\"],\"4EZrJN\":[\"Правила\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Профиль флуда (+F)\"],\"4RZQRK\":[\"Чем ты занимаешься?\"],\"4hfTrB\":[\"Никнейм\"],\"4n99LO\":[\"Уже в \",[\"0\"]],\"4t6vMV\":[\"Автоматически переключаться на однострочный режим для коротких сообщений\"],\"4vsHmf\":[\"Время (мин)\"],\"4x/Axu\":[\"У вашего баунсера ещё нет сетей. Добавьте одну, чтобы начать.\"],\"5+INAX\":[\"Выделять сообщения, в которых упоминается ваш никнейм\"],\"5R5Pv/\":[\"Имя оператора\"],\"678PKt\":[\"Название сети\"],\"6Aih4U\":[\"Не в сети\"],\"6CO3WE\":[\"Пароль для входа в канал. Оставьте пустым, чтобы удалить ключ.\"],\"6HhMs3\":[\"Сообщение при выходе\"],\"6V3Ea3\":[\"Скопировано\"],\"6lGV3K\":[\"Свернуть\"],\"6yFOEi\":[\"Введите пароль опера...\"],\"7+IHTZ\":[\"Файл не выбран\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (например: spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Отправить личное сообщение\"],\"7U1W7c\":[\"Очень мягкий\"],\"7Y1YQj\":[\"Имя:\"],\"7YHArF\":[\"— открыть в просмотрщике\"],\"7fjnVl\":[\"Поиск пользователей...\"],\"7jL88x\":[\"Удалить это сообщение? Это действие нельзя отменить.\"],\"7nGhhM\":[\"О чём вы думаете?\"],\"7sEpu1\":[\"Участники — \",[\"0\"]],\"7sNhEz\":[\"Имя пользователя\"],\"8H0Q+x\":[\"Подробнее о профилях →\"],\"8Phu0A\":[\"Показывать, когда пользователи меняют никнейм\"],\"8XTG9e\":[\"Введите пароль оператора\"],\"8XsV2J\":[\"Повторить отправку\"],\"8ZsakT\":[\"Пароль\"],\"8kR84m\":[\"Вы собираетесь открыть внешнюю ссылку:\"],\"8lCgih\":[\"Удалить правило\"],\"8o3dPc\":[\"ÐеÑеÑаÑиÑе ÑÐ°Ð¹Ð»Ñ Ð´Ð»Ñ Ð·Ð°Ð³ÑÑзки\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"присоединился\"],\"few\":[\"присоединился \",[\"joinCount\"],\" раза\"],\"many\":[\"присоединился \",[\"joinCount\"],\" раз\"],\"other\":[\"присоединился \",[\"joinCount\"],\" раза\"]}]],\"9BMLnJ\":[\"Переподключиться к серверу\"],\"9OEgyT\":[\"Добавить реакцию\"],\"9PQ8m2\":[\"G-Line (глобальный бан)\"],\"9Qs99X\":[\"Email:\"],\"9QupBP\":[\"Удалить шаблон\"],\"9W7tl5\":[\"(без изменений)\"],\"9bG48P\":[\"Отправка\"],\"9f5f0u\":[\"Вопросы о конфиденциальности? Свяжитесь с нами:\"],\"9iweoP\":[\"Сети на \",[\"0\"]],\"9unqs3\":[\"Отсутствие:\"],\"9v3hwv\":[\"Серверы не найдены.\"],\"9zb2WA\":[\"Подключение\"],\"A1taO8\":[\"Поиск\"],\"A2adVi\":[\"Отправлять уведомления о наборе текста\"],\"A9Rhec\":[\"Имя канала\"],\"AWOSPo\":[\"Увеличить\"],\"AXSpEQ\":[\"Войти как оператор при подключении\"],\"AdKRCX\":[\"Вы подключены к <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Аккаунт\"],\"AhNP40\":[\"Перемотка\"],\"Ai2U7L\":[\"Хост\"],\"AjBQnf\":[\"Изменил никнейм\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Отменить ответ\"],\"ApSx0O\":[\"Найдено \",[\"0\"],\" сообщений, соответствующих \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Результаты не найдены\"],\"AyNqAB\":[\"Отображать все события сервера в чате\"],\"B/QqGw\":[\"Отошёл от клавиатуры\"],\"B0sB2k\":[\"Открытый текст\"],\"B8AaMI\":[\"Это поле обязательно для заполнения\"],\"BA2c49\":[\"Сервер не поддерживает расширенную фильтрацию LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" и ещё \",[\"3\"],\" печатают...\"],\"BGul2A\":[\"У вас есть несохранённые изменения. Вы уверены, что хотите закрыть без сохранения?\"],\"BIf9fi\":[\"Ваше статусное сообщение\"],\"BOJWfb\":[\"Отключиться от bouncer soju?\"],\"BPm98R\":[\"СеÑÐ²ÐµÑ Ð½Ðµ вÑбÑан. СнаÑала вÑбеÑиÑе ÑеÑÐ²ÐµÑ Ð½Ð° боковой панели; пÑиглаÑиÑелÑнÑе ÑÑÑлки ÑпÑавлÑÑÑÑÑ Ð¾ÑделÑно Ð´Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾ ÑеÑвеÑа.\"],\"BZz3md\":[\"Ваш личный сайт\"],\"Bgm/H7\":[\"Разрешить ввод нескольких строк текста\"],\"BiQIl1\":[\"Закрепить эту личную переписку\"],\"BlNZZ2\":[\"Нажмите, чтобы перейти к сообщению\"],\"Bowq3c\":[\"Только операторы могут изменять тему канала\"],\"Btozzp\":[\"Срок действия этого изображения истёк\"],\"Bycfjm\":[\"Всего: \",[\"0\"]],\"C6IBQc\":[\"Копировать весь JSON\"],\"C9L9wL\":[\"Сбор данных\"],\"CDq4wC\":[\"Модерировать пользователя\"],\"CHVRxG\":[\"Сообщение @\",[\"0\"],\" (Shift+Enter — новая строка)\"],\"CN9zdR\":[\"Необходимо указать имя и пароль оператора\"],\"CW3sYa\":[\"Добавить реакцию \",[\"emoji\"]],\"CaAkqd\":[\"Показывать выходы\"],\"CbvaYj\":[\"Забанить по никнейму\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Выберите канал\"],\"CsekCi\":[\"Обычный\"],\"D+NlUC\":[\"Система\"],\"D28t6+\":[\"подключился и отключился\"],\"DB8zMK\":[\"Применить\"],\"DBcWHr\":[\"Пользовательский файл звука уведомления\"],\"DTy9Xw\":[\"Предпросмотр медиа\"],\"Dj4pSr\":[\"Выберите надёжный пароль\"],\"Du+zn+\":[\"Поиск...\"],\"Du2T2f\":[\"Настройка не найдена\"],\"DwsSVQ\":[\"Применить фильтры и обновить\"],\"E3W/zd\":[\"Никнейм по умолчанию\"],\"E6nRW7\":[\"Копировать URL\"],\"E703RG\":[\"Режимы:\"],\"EAeu1Z\":[\"Отправить приглашение\"],\"EFKJQT\":[\"Настройка\"],\"EGPQBv\":[\"Пользовательские правила флуда (+f)\"],\"ELik0r\":[\"Просмотреть полную политику конфиденциальности\"],\"EPbeC2\":[\"Просмотреть или изменить тему канала\"],\"EQCDNT\":[\"Введите имя пользователя опера...\"],\"EUvulZ\":[\"Найдено 1 сообщение, соответствующее \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Следующее изображение\"],\"EdQY6l\":[\"Нет\"],\"EnqLYU\":[\"Поиск серверов...\"],\"F0OKMc\":[\"Редактировать сервер\"],\"F6Int2\":[\"Включить выделения\"],\"FDoLyE\":[\"Макс. пользователей\"],\"FUU/hZ\":[\"Управляет количеством внешних медиафайлов, загружаемых в чат.\"],\"Fdp03t\":[\"вкл\"],\"FfPWR0\":[\"Диалог\"],\"FjkaiT\":[\"Уменьшить\"],\"FlqOE9\":[\"Что это означает:\"],\"FolHNl\":[\"Управление аккаунтом и аутентификацией\"],\"Fp2Dif\":[\"Покинул сервер\"],\"G5KmCc\":[\"GZ-Line (глобальная Z-Line)\"],\"GDs0lz\":[\"<0>Риск:0> Конфиденциальная информация (сообщения, личные переписки, данные аутентификации) может быть раскрыта сетевым администраторам или злоумышленникам, находящимся между IRC-серверами.\"],\"GR+2I3\":[\"Добавить маску приглашения (например: nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Закрыть всплывающие уведомления сервера\"],\"GdhD7H\":[\"ÐажмиÑе еÑÑ Ñаз Ð´Ð»Ñ Ð¿Ð¾Ð´ÑвеÑждениÑ\"],\"GjRZex\":[\"Отключить сеть?\"],\"GlHnXw\":[\"Смена ника не удалась: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Предпросмотр:\"],\"GtmO8/\":[\"от\"],\"GtuHUQ\":[\"Переименовать этот канал на сервере. Все пользователи увидят новое имя.\"],\"GuGfFX\":[\"Включить/выключить поиск\"],\"GxkJXS\":[\"Загрузка...\"],\"GzbwnK\":[\"Присоединился к каналу\"],\"GzsUDB\":[\"Расширенный профиль\"],\"H/PnT8\":[\"Вставить эмодзи\"],\"H6Izzl\":[\"Ваш предпочтительный цветовой код\"],\"H9jIv+\":[\"Показывать входы/выходы\"],\"HAKBY9\":[\"Загрузить файлы\"],\"HdE1If\":[\"Канал\"],\"Hk4AW9\":[\"Ваше предпочтительное отображаемое имя\"],\"HmHDk7\":[\"Выбрать участника\"],\"HrQzPU\":[\"Каналы на \",[\"networkName\"]],\"I2tXQ5\":[\"Сообщение @\",[\"0\"],\" (Enter — новая строка, Shift+Enter — отправить)\"],\"I6bw/h\":[\"Забанить пользователя\"],\"I92Z+b\":[\"Включить уведомления\"],\"I9D72S\":[\"Вы уверены, что хотите удалить это сообщение? Это действие нельзя отменить.\"],\"IA+1wo\":[\"Показывать, когда пользователей исключают из каналов\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"Инфо:\"],\"IUwGEM\":[\"Сохранить изменения\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" и \",[\"2\"],\" печатают...\"],\"IgrLD/\":[\"Пауза\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"Ответить\"],\"IoHMnl\":[\"Максимальное значение: \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Подключение...\"],\"J5T9NW\":[\"Информация о пользователе\"],\"J8Y5+z\":[\"Упс! Разрыв сети! ⚠️\"],\"JBHkBA\":[\"Покинул канал\"],\"JCwL0Q\":[\"Укажите причину (необязательно)\"],\"JFciKP\":[\"Переключить\"],\"JXGkhG\":[\"Изменить имя канала (только для операторов)\"],\"JcD7qf\":[\"Другие действия\"],\"JdkA+c\":[\"Секретный (+s)\"],\"Jmu12l\":[\"Каналы сервера\"],\"JoQY+E\":[\"Это удалит сеть из вашего bouncer soju. Чтобы использовать её снова, вам нужно будет добавить её обратно.\"],\"JvQ++s\":[\"Включить Markdown\"],\"K2jwh/\":[\"Данные WHOIS недоступны\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Удалить сообщение\"],\"KKBlUU\":[\"Встроить\"],\"KM0pLb\":[\"Добро пожаловать в канал!\"],\"KR6W2h\":[\"Перестать игнорировать пользователя\"],\"KV+Bi1\":[\"Только по приглашению (+i)\"],\"KdCtwE\":[\"Сколько секунд отслеживать флуд-активность до сброса счётчиков\"],\"Kkezga\":[\"Пароль сервера\"],\"KsiQ/8\":[\"Для входа в канал необходимо приглашение\"],\"L+gB/D\":[\"Информация о канале\"],\"LC1a7n\":[\"IRC-сервер сообщил о низком уровне безопасности межсерверных соединений. Это означает, что при передаче ваших сообщений между IRC-серверами сети они могут быть недостаточно зашифрованы или SSL/TLS-сертификаты могут не проверяться должным образом.\"],\"LEwpeL\":[\"bouncer soju (управление)\"],\"LNfLR5\":[\"Показывать исключения\"],\"LP+1Z7\":[\"Добавить сеть\"],\"LQb0W/\":[\"Показывать все события\"],\"LU7/yA\":[\"Альтернативное имя для отображения в интерфейсе. Может содержать пробелы, эмодзи и специальные символы. Настоящее имя канала (\",[\"channelName\"],\") по-прежнему будет использоваться для IRC-команд.\"],\"LUb9O7\":[\"Необходимо указать корректный порт сервера\"],\"LV4fT6\":[\"ÐпиÑание (необÑзаÑелÑно, напÑÐ¸Ð¼ÐµÑ Â«ÐеÑа-ÑеÑÑеÑÑ Q3»)\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Политика конфиденциальности\"],\"LcuSDR\":[\"Управление информацией профиля и метаданными\"],\"LqLS9B\":[\"Показывать смену никнейма\"],\"LsDQt2\":[\"Настройки канала\"],\"LtI9AS\":[\"Владелец\"],\"LuNhhL\":[\"отреагировал на это сообщение\"],\"M/AZNG\":[\"URL вашего аватара\"],\"M/WIer\":[\"Отправить сообщение\"],\"M8er/5\":[\"Имя:\"],\"MHk+7g\":[\"Предыдущее изображение\"],\"MRorGe\":[\"Написать в личку\"],\"MVbSGP\":[\"Временное окно (секунды)\"],\"MkpcsT\":[\"Ваши сообщения и настройки хранятся локально на вашем устройстве\"],\"MzPdC2\":[\"Пароль сервера (PASS)\"],\"N/hDSy\":[\"Пометить как бота — обычно «on» или пусто\"],\"N6j2JH\":[\"Изменить \",[\"0\"]],\"N7TQbE\":[\"Пригласить пользователя в \",[\"channelName\"]],\"NCca/o\":[\"Введите ник по умолчанию...\"],\"Nqs6B9\":[\"Показывает весь внешний медиаконтент. Любой URL может вызвать запрос к неизвестному серверу.\"],\"Nt+9O7\":[\"Использовать WebSocket вместо обычного TCP\"],\"NxIHzc\":[\"Отключить пользователя\"],\"O+v/cL\":[\"Просмотреть все каналы на сервере\"],\"OCGpR4\":[\"(наследовать)\"],\"ODwSCk\":[\"Отправить GIF\"],\"OGQ5kK\":[\"Настройка звуков уведомлений и выделений\"],\"OIPt1Z\":[\"Показать или скрыть боковую панель списка участников\"],\"OKSNq/\":[\"Очень строгий\"],\"ONWvwQ\":[\"Загрузить\"],\"OVKoQO\":[\"Пароль вашего аккаунта для аутентификации\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Перенося IRC в будущее\"],\"OhCpra\":[\"Задать тему…\"],\"OkltoQ\":[\"Забанить \",[\"username\"],\" по никнейму (запрещает переподключение с тем же ником)\"],\"P+t/Te\":[\"Нет дополнительных данных\"],\"P42Wcc\":[\"Безопасно\"],\"PD38l0\":[\"Предпросмотр аватара канала\"],\"PD9mEt\":[\"Введите сообщение...\"],\"PPqfdA\":[\"Открыть настройки конфигурации канала\"],\"PSCjfZ\":[\"Тема, которая будет отображаться для этого канала. Тему видят все пользователи.\"],\"PZCecv\":[\"Предпросмотр PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 раз\"],\"few\":[[\"c\"],\" раза\"],\"many\":[[\"c\"],\" раз\"],\"other\":[[\"c\"],\" раза\"]}]],\"PguS2C\":[\"Добавить маску исключения (например: nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Показано \",[\"displayedChannelsCount\"],\" из \",[\"0\"],\" каналов\"],\"PqhVlJ\":[\"Забанить пользователя (по hostmask)\"],\"Q+chwU\":[\"Имя пользователя:\"],\"Q2QY4/\":[\"УдалиÑÑ ÑÑо пÑиглаÑение\"],\"Q3v9Wc\":[\"Да, удалить\"],\"Q6hhn8\":[\"Настройки\"],\"QF4a34\":[\"Введите имя пользователя\"],\"QGqSZ2\":[\"Цвет и форматирование\"],\"QJQd1J\":[\"Редактировать профиль\"],\"QSzGDE\":[\"Не активен\"],\"QUlny5\":[\"Добро пожаловать на \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Читать далее\"],\"QuSkCF\":[\"Фильтр каналов...\"],\"QwUrDZ\":[\"изменил тему на: \",[\"topic\"]],\"R0UH07\":[\"Изображение \",[\"0\"],\" из \",[\"1\"]],\"R7SsBE\":[\"Выкл. звук\"],\"R8rf1X\":[\"Нажмите, чтобы задать тему\"],\"RArB3D\":[\"был кикнут из \",[\"channelName\"],\" пользователем \",[\"username\"]],\"RI3cWd\":[\"Откройте мир IRC вместе с ObsidianIRC\"],\"RIfHS5\":[\"СоздаÑÑ Ð½Ð¾Ð²ÑÑ Ð¿ÑиглаÑиÑелÑнÑÑ ÑÑÑлкÑ\"],\"RMMaN5\":[\"Модерируемый (+m)\"],\"RWw9Lg\":[\"Закрыть диалог\"],\"RZ2BuZ\":[\"Регистрация аккаунта \",[\"account\"],\" требует подтверждения: \",[\"message\"]],\"RySp6q\":[\"Скрыть комментарии\"],\"S5Togi\":[\"Загрузка сетей с вашего баунсера…\"],\"SPKQTd\":[\"Необходимо указать никнейм\"],\"SPVjfj\":[\"Если оставить пустым, будет использоваться «без причины»\"],\"SQKPvQ\":[\"Пригласить пользователя\"],\"STmlpb\":[\"К списку сетей\"],\"SkZcl+\":[\"Выберите заранее заданный профиль защиты от флуда. Эти профили предоставляют сбалансированные настройки защиты для различных сценариев использования.\"],\"Slr+3C\":[\"Мин. пользователей\"],\"Spnlre\":[\"Вы пригласили \",[\"target\"],\" присоединиться к \",[\"channel\"]],\"T/ckN5\":[\"Открыть в просмотрщике\"],\"T91vKp\":[\"Воспроизвести\"],\"TV2Wdu\":[\"Узнайте, как мы обрабатываем ваши данные и защищаем вашу конфиденциальность.\"],\"TgFpwD\":[\"Применяется...\"],\"TkzSFB\":[\"Нет изменений\"],\"TtserG\":[\"Введите настоящее имя\"],\"Ttz9J1\":[\"Введите пароль...\"],\"Tz0i8g\":[\"Настройки\"],\"U3pytU\":[\"Администратор\"],\"UDb2YD\":[\"Реакция\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"ÐÑ ÐµÑÑ Ð½Ðµ Ñоздали ни одной пÑиглаÑиÑелÑной ÑÑÑлки. ÐÑполÑзÑйÑе ÑоÑÐ¼Ñ Ð²ÑÑе, ÑÑÐ¾Ð±Ñ ÑоздаÑÑ Ð¿ÐµÑвÑÑ.\"],\"UGT5vp\":[\"Сохранить настройки\"],\"UV5hLB\":[\"Баны не найдены\"],\"Uaj3Nd\":[\"Статусные сообщения\"],\"Ue3uny\":[\"По умолчанию (без профиля)\"],\"UkARhe\":[\"Обычный — стандартная защита\"],\"Umn7Cj\":[\"Комментариев пока нет. Будьте первым!\"],\"UtUIRh\":[[\"0\"],\" старых сообщений\"],\"UwzP+U\":[\"Защищённое соединение\"],\"V0/A4O\":[\"Владелец канала\"],\"V0zZWc\":[\"Также будет закрыта связанная сеть ниже.\"],\"V4qgxE\":[\"Создан до (мин назад)\"],\"V8yTm6\":[\"Очистить поиск\"],\"VJMMyz\":[\"ObsidianIRC — IRC будущего\"],\"VJScHU\":[\"Причина\"],\"VLsmVV\":[\"Отключить уведомления\"],\"VbyRUy\":[\"Комментарии\"],\"Vmx0mQ\":[\"Установлено:\"],\"VqnIZz\":[\"Ознакомьтесь с нашей политикой конфиденциальности и практикой обработки данных\"],\"VrMygG\":[\"Минимальная длина: \",[\"0\"]],\"VrnTui\":[\"Ваши местоимения, отображаемые в профиле\"],\"W8E3qn\":[\"Аутентифицированный аккаунт\"],\"WAakm9\":[\"Удалить канал\"],\"WFxTHC\":[\"Добавить маску бана (например: nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Необходимо указать хост сервера\"],\"WRYdXW\":[\"Позиция в аудио\"],\"WUOH5B\":[\"Игнорировать пользователя\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Показать ещё 1 элемент\"],\"few\":[\"Показать ещё \",[\"1\"],\" элемента\"],\"many\":[\"Показать ещё \",[\"1\"],\" элементов\"],\"other\":[\"Показать ещё \",[\"1\"],\" элемента\"]}]],\"WYxRzo\":[\"Создание и ÑпÑавление ваÑими пÑиглаÑиÑелÑнÑми ÑÑÑлками\"],\"Wd38W1\":[\"ÐÑÑавÑÑе поле канала пÑÑÑÑм Ð´Ð»Ñ Ð¾Ð±Ñего пÑиглаÑÐµÐ½Ð¸Ñ Ð² ÑеÑÑ. ÐпиÑание нÑжно ÑолÑко Ð´Ð»Ñ Ð²Ð°ÑиÑ
запиÑей â оно видно ÑолÑко вам в ÑÑом ÑпиÑке.\"],\"Weq9zb\":[\"Основное\"],\"Wfj7Sk\":[\"Включить или отключить звуки уведомлений\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Профиль пользователя\"],\"X6S3lt\":[\"Поиск настроек, каналов, серверов...\"],\"XEHan5\":[\"Всё равно продолжить\"],\"XI1+wb\":[\"Неверный формат\"],\"XIXeuC\":[\"Сообщение @\",[\"0\"]],\"XMS+k4\":[\"Начать личный чат\"],\"XWgxXq\":[\"Альбом\"],\"Xd7+IT\":[\"Открепить личный чат\"],\"Xm/s+u\":[\"Отображение\"],\"Xp2n93\":[\"Показывает медиафайлы с доверенного файлового хоста вашего сервера. Запросы к внешним сервисам не выполняются.\"],\"XvjC4F\":[\"Сохранение...\"],\"Y/qryO\":[\"Пользователи по вашему запросу не найдены\"],\"YAqRpI\":[\"Регистрация аккаунта \",[\"account\"],\" успешна: \",[\"message\"]],\"YEfzvP\":[\"Защищённая тема (+t)\"],\"YQOn6a\":[\"Свернуть список участников\"],\"YRCoE9\":[\"Оператор канала\"],\"YURQaF\":[\"Просмотреть профиль\"],\"YdBSvr\":[\"Управление отображением медиа и внешнего контента\"],\"Yj6U3V\":[\"Нет центрального сервера:\"],\"YjvpGx\":[\"Местоимения\"],\"YqH4l4\":[\"Без ключа\"],\"YyUPpV\":[\"Аккаунт:\"],\"ZJSWfw\":[\"Сообщение, отображаемое при отключении от сервера\"],\"ZR1dJ4\":[\"Приглашения\"],\"ZdWg0V\":[\"Открыть в браузере\"],\"ZhRBbl\":[\"Поиск сообщений…\"],\"Zmcu3y\":[\"Расширенные фильтры\"],\"a0bHay\":[\"Отключить <0>\",[\"0\"],\"0>?\"],\"a2/8e5\":[\"Тема установлена после (мин назад)\"],\"aHKcKc\":[\"Предыдущая страница\"],\"aJTbXX\":[\"Пароль оператора\"],\"aQryQv\":[\"Такой шаблон уже существует\"],\"aW9pLN\":[\"Максимальное количество пользователей в канале. Оставьте пустым для снятия ограничения.\"],\"ah4fmZ\":[\"Также показывает превью с YouTube, Vimeo, SoundCloud и других известных сервисов.\"],\"aifXak\":[\"В этом канале нет медиафайлов\"],\"ap2zBz\":[\"Мягкий\"],\"az8lvo\":[\"Выкл.\"],\"azXSNo\":[\"Развернуть список участников\"],\"azdliB\":[\"Войти в аккаунт\"],\"b26wlF\":[\"она/её\"],\"bD/+Ei\":[\"Строгий\"],\"bQ6BJn\":[\"Настройте подробные правила защиты от флуда. Каждое правило задаёт тип активности для мониторинга и действие при превышении порога.\"],\"beV7+y\":[\"Пользователь получит приглашение вступить в \",[\"channelName\"],\".\"],\"bk84cH\":[\"Сообщение об отсутствии\"],\"bkHdLj\":[\"Добавить IRC-сервер\"],\"bmQLn5\":[\"Добавить правило\"],\"bv4cFj\":[\"Транспорт\"],\"bwRvnp\":[\"Действие\"],\"c8+EVZ\":[\"Верифицированный аккаунт\"],\"cGYUlD\":[\"Предпросмотр медиа не загружается.\"],\"cLF98o\":[\"Показать комментарии (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Нет доступных пользователей\"],\"cSgpoS\":[\"Закрепить личный чат\"],\"cXeEKu\":[\"Также будут закрыты \",[\"0\"],\" связанных сетей ниже.\"],\"cde3ce\":[\"Написать <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Копировать форматированный вывод\"],\"cl/A5J\":[\"Добро пожаловать на \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Удалить\"],\"coPLXT\":[\"Мы не храним ваши IRC-переписки на наших серверах\"],\"crYH/6\":[\"Плеер SoundCloud\"],\"cv5DQb\":[\"хост не задан\"],\"d3sis4\":[\"Добавить сервер\"],\"d9aN5k\":[\"Удалить \",[\"username\"],\" из канала\"],\"dEgA5A\":[\"Отмена\"],\"dGi1We\":[\"Открепить эту личную переписку\"],\"dJVuyC\":[\"покинул \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"кому\"],\"dXqxlh\":[\"<0>⚠️ Угроза безопасности!0> Это соединение может быть уязвимо для перехвата или атак типа «человек посередине».\"],\"da9Q/R\":[\"Изменил режимы канала\"],\"dhJN3N\":[\"Показать комментарии\"],\"dj2xTE\":[\"Закрыть уведомление\"],\"dpCzmC\":[\"Настройки защиты от флуда\"],\"e9dQpT\":[\"Открыть эту ссылку в новой вкладке?\"],\"ePK91l\":[\"Изменить\"],\"eYBDuB\":[\"Загрузите изображение или укажите URL с необязательной подстановкой \",[\"size\"],\" для динамического масштабирования\"],\"edBbee\":[\"Забанить \",[\"username\"],\" по hostmask (запрещает переподключение с того же IP/хоста)\"],\"ekfzWq\":[\"Настройки пользователя\"],\"elPDWs\":[\"Настройте IRC-клиент под себя\"],\"eu2osY\":[\"<0>💡 Рекомендация:0> Продолжайте только если вы доверяете этому серверу и понимаете риски. Избегайте передачи конфиденциальной информации или паролей через это соединение.\"],\"euEhbr\":[\"Нажмите, чтобы войти в \",[\"channel\"]],\"ez3vLd\":[\"Включить многострочный ввод\"],\"f0J5Ki\":[\"Межсерверное взаимодействие может использовать незашифрованные соединения\"],\"f9BHJk\":[\"Предупредить пользователя\"],\"fDOLLd\":[\"Каналы не найдены.\"],\"ffzDkB\":[\"Анонимная аналитика:\"],\"fq1GF9\":[\"Показывать, когда пользователи отключаются от сервера\"],\"gCldcN\":[\"Изменить цвет акцента\"],\"gEF57C\":[\"Этот сервер поддерживает только один тип подключения\"],\"gJuLUI\":[\"Список игнорирования\"],\"gNzMrk\":[\"Текущий аватар\"],\"gjPWyO\":[\"Введите ник...\"],\"gz6UQ3\":[\"Развернуть\"],\"h6/IMX\":[\"Добавьте вашу первую сеть\"],\"h6razj\":[\"Исключить маску имени канала\"],\"hG6jnw\":[\"Тема не задана\"],\"hG89Ed\":[\"Изображение\"],\"hYgDIe\":[\"СоздаÑÑ\"],\"hZ6znB\":[\"Порт\"],\"ha+Bz5\":[\"например: 100:1440\"],\"he3ygx\":[\"ÐопиÑоваÑÑ\"],\"hehnjM\":[\"Количество\"],\"hzdLuQ\":[\"Говорить могут только пользователи с голосом или выше\"],\"i0qMbr\":[\"Главная\"],\"iDNBZe\":[\"Уведомления\"],\"iH8pgl\":[\"Назад\"],\"iL9SZg\":[\"Забанить пользователя (по никнейму)\"],\"iNt+3c\":[\"Вернуться к изображению\"],\"iQvi+a\":[\"Не предупреждать меня о низком уровне безопасности соединений для этого сервера\"],\"iSLIjg\":[\"Подключиться\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Хост сервера\"],\"idD8Ev\":[\"Сохранено\"],\"iivqkW\":[\"В сети с\"],\"ij+Elv\":[\"Предпросмотр изображения\"],\"ilIWp7\":[\"Включить/выключить уведомления\"],\"iuaqvB\":[\"Используйте * в качестве маски. Примеры: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Бот\"],\"j2DGR0\":[\"Забанить по hostmask\"],\"jA4uoI\":[\"Тема:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Причина (необязательно)\"],\"jUV7CU\":[\"Загрузить аватар\"],\"jW5Uwh\":[\"Управление загрузкой внешних медиафайлов. Выкл / Безопасно / Доверенные источники / Весь контент.\"],\"jXzms5\":[\"Параметры вложения\"],\"jZlrte\":[\"Цвет\"],\"jfC/xh\":[\"Контакт\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"Загрузить старые сообщения\"],\"k3ID0F\":[\"Фильтр участников…\"],\"k65gsE\":[\"Подробнее\"],\"k7Zgob\":[\"Отменить подключение\"],\"kAVx5h\":[\"Приглашения не найдены\"],\"kCLEPU\":[\"Подключён к\"],\"kF5LKb\":[\"Игнорируемые шаблоны:\"],\"kGeOx/\":[\"Присоединиться к \",[\"0\"]],\"kITKr8\":[\"Загрузка режимов канала...\"],\"kPpPsw\":[\"Вы являетесь IRC-оператором\"],\"kWJmRL\":[\"ты\"],\"kfcRb0\":[\"Аватар\"],\"kjMqSj\":[\"Копировать JSON\"],\"krViRy\":[\"Нажмите для копирования как JSON\"],\"ks71ra\":[\"Исключения\"],\"kw4lRv\":[\"Полуоператор канала\"],\"kxgIRq\":[\"Выберите или добавьте канал для начала.\"],\"ky6dWe\":[\"Предпросмотр аватара\"],\"l+GxCv\":[\"Загрузка каналов...\"],\"l+IUVW\":[\"Верификация аккаунта \",[\"account\"],\" успешна: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"переподключился\"],\"few\":[\"переподключился \",[\"reconnectCount\"],\" раза\"],\"many\":[\"переподключился \",[\"reconnectCount\"],\" раз\"],\"other\":[\"переподключился \",[\"reconnectCount\"],\" раза\"]}]],\"l1l8sj\":[[\"0\"],\" дн. назад\"],\"l5NhnV\":[\"#канал (необÑзаÑелÑно)\"],\"l5jmzx\":[[\"0\"],\" и \",[\"1\"],\" печатают...\"],\"lCF0wC\":[\"ÐбновиÑÑ\"],\"lHy8N5\":[\"Загрузка дополнительных каналов...\"],\"lasgrr\":[\"иÑполÑзовано\"],\"lbpf14\":[\"Войти в \",[\"value\"]],\"lfFsZ4\":[\"Каналы\"],\"lkNdiH\":[\"Имя аккаунта\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Загрузить изображение\"],\"loQxaJ\":[\"Я вернулся\"],\"lvfaxv\":[\"ГЛАВНАЯ\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Добавить\"],\"m8flAk\":[\"Предпросмотр (ещё не загружено)\"],\"mEPxTp\":[\"<0>⚠️ Будьте осторожны!0> Открывайте ссылки только из доверенных источников. Вредоносные ссылки могут угрожать вашей безопасности или конфиденциальности.\"],\"mHGdhG\":[\"Информация о сервере\"],\"mHS8lb\":[\"Сообщение #\",[\"0\"]],\"mMYBD9\":[\"Широкий — более широкая область защиты\"],\"mTGsPd\":[\"Тема канала\"],\"mU8j6O\":[\"Без внешних сообщений (+n)\"],\"mZp8FL\":[\"Автоматический возврат к однострочному режиму\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"Комментарии (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Пользователь аутентифицирован\"],\"mwtcGl\":[\"Закрыть комментарии\"],\"myL0MR\":[\"Удалить эту сеть?\"],\"mzI/c+\":[\"Скачать\"],\"n3fGRk\":[\"установил \",[\"0\"]],\"n5+j9l\":[\"напр. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Мягкий — менее строгая защита\"],\"nNflMD\":[\"Покинуть канал\"],\"nPXkBi\":[\"Загрузка данных WHOIS...\"],\"nQnxxF\":[\"Сообщение #\",[\"0\"],\" (Shift+Enter — новая строка)\"],\"nWMRxa\":[\"Открепить\"],\"nkC032\":[\"Без профиля флуда\"],\"o69z4d\":[\"Отправить предупреждение пользователю \",[\"username\"]],\"o9ylQi\":[\"Найдите GIF для начала\"],\"oFGkER\":[\"Уведомления сервера\"],\"oOi11l\":[\"Прокрутить вниз\"],\"oPYIL5\":[\"ÑеÑÑ\"],\"oQEzQR\":[\"Новое DM\"],\"oXOSPE\":[\"В сети\"],\"oal760\":[\"Возможны атаки типа «человек посередине» на межсерверные соединения\"],\"oeqmmJ\":[\"Доверенные источники\"],\"optX0N\":[[\"0\"],\" Ñ. назад\"],\"ovBPCi\":[\"По умолчанию\"],\"p0Z69r\":[\"Шаблон не может быть пустым\"],\"p1KgtK\":[\"Не удалось загрузить аудио\"],\"p59pEv\":[\"Подробности\"],\"p7sRI6\":[\"Сообщать другим, когда вы печатаете\"],\"pBm1od\":[\"Секретный канал\"],\"pNmiXx\":[\"Ваш никнейм по умолчанию для всех серверов\"],\"pUUo9G\":[\"Хост:\"],\"pVGPmz\":[\"Пароль аккаунта\"],\"peNE68\":[\"Навсегда\"],\"plhHQt\":[\"Нет данных\"],\"pm6+q5\":[\"Предупреждение безопасности\"],\"pn5qSs\":[\"Дополнительная информация\"],\"q0cR4S\":[\"теперь известен как **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Канал не будет отображаться в командах LIST и NAMES\"],\"qLpTm/\":[\"Убрать реакцию \",[\"emoji\"]],\"qVkGWK\":[\"Закрепить\"],\"qY8wNa\":[\"Сайт\"],\"qb0xJ7\":[\"Используйте маски: * соответствует любой последовательности, ? — любому одному символу. Примеры: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Ключ канала (+k)\"],\"qtoOYG\":[\"Без ограничений\"],\"r1W2AS\":[\"Изображение с файлового хостинга\"],\"rIPR2O\":[\"Тема установлена до (мин назад)\"],\"rMMSYo\":[\"Максимальная длина: \",[\"0\"]],\"rWtzQe\":[\"Сеть разделилась и воссоединилась. ✅\"],\"rYG2u6\":[\"Пожалуйста, подождите...\"],\"rdUucN\":[\"Предпросмотр\"],\"rjGI/Q\":[\"Конфиденциальность\"],\"rk8iDX\":[\"Загрузка GIF...\"],\"rn6SBY\":[\"Вкл. звук\"],\"s/UKqq\":[\"Был исключён из канала\"],\"s7oqXR\":[\"Выберите сеть\"],\"s8cATI\":[\"присоединился к \",[\"channelName\"]],\"sCO9ue\":[\"Соединение с <0>\",[\"serverName\"],\"0> имеет следующие проблемы безопасности:\"],\"sGH11W\":[\"Сервер\"],\"sHI1H+\":[\"теперь известен как **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" пригласил вас присоединиться к \",[\"channel\"]],\"sUBSbK\":[\"Пока нет вышестоящих сетей.\"],\"sby+1/\":[\"Нажмите, чтобы скопировать\"],\"sfN25C\":[\"Ваше настоящее или полное имя\"],\"sliuzR\":[\"Открыть ссылку\"],\"sqrO9R\":[\"Пользовательские упоминания\"],\"sr6RdJ\":[\"Многострочный режим по Shift+Enter\"],\"swrCpB\":[\"Канал был переименован с \",[\"oldName\"],\" на \",[\"newName\"],\" пользователем \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Дополнительно\"],\"t/YqKh\":[\"Удалить\"],\"t47eHD\":[\"Ваш уникальный идентификатор на этом сервере\"],\"tAkAh0\":[\"URL с необязательной подстановкой \",[\"size\"],\" для динамического масштабирования. Пример: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Показать или скрыть боковую панель списка каналов\"],\"tfDRzk\":[\"Сохранить\"],\"tiBsJk\":[\"покинул \",[\"channelName\"]],\"tt4/UD\":[\"вышел (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Ник {nick} уже используется, повторная попытка с {newNick}\"],\"u0a8B4\":[\"Аутентифицироваться как IRC-оператор для административного доступа\"],\"u0rWFU\":[\"Создан после (мин назад)\"],\"u72w3t\":[\"Пользователи и шаблоны для игнорирования\"],\"u7jc2L\":[\"вышел\"],\"uAQUqI\":[\"Статус\"],\"uB85T3\":[\"Ошибка сохранения: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC-серверы:\"],\"ukyW4o\":[\"ÐаÑи пÑиглаÑиÑелÑнÑе ÑÑÑлки\"],\"usSSr/\":[\"Масштаб\"],\"v7uvcf\":[\"Программа:\"],\"vE8kb+\":[\"Shift+Enter для новой строки (Enter отправляет)\"],\"vERlcd\":[\"Профиль\"],\"vK0RL8\":[\"Без темы\"],\"vSJd18\":[\"Видео\"],\"vXIe7J\":[\"Язык\"],\"vaHYxN\":[\"Настоящее имя\"],\"vhjbKr\":[\"Отсутствую\"],\"w/nogd\":[[\"0\"],\" сеть\",[\"1\"],\" — выберите одну для подключения\"],\"w4NYox\":[\"клиент \",[\"title\"]],\"w8xQRx\":[\"Неверное значение\"],\"wFjjxZ\":[\"был кикнут из \",[\"channelName\"],\" пользователем \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Исключения из банов не найдены\"],\"wPrGnM\":[\"Администратор канала\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Показывать, когда пользователи входят в каналы или покидают их\"],\"whqZ9r\":[\"Дополнительные слова или фразы для выделения\"],\"wm7RV4\":[\"Звук уведомления\"],\"wz/Yoq\":[\"Ваши сообщения могут быть перехвачены при передаче между серверами\"],\"x3+y8b\":[\"СÑолÑко Ñеловек заÑегиÑÑÑиÑовалоÑÑ Ð¿Ð¾ ÑÑой ÑÑÑлке\"],\"xCJdfg\":[\"Очистить\"],\"xOTzt5\":[\"ÑолÑко ÑÑо\"],\"xUHRTR\":[\"Автоматически аутентифицироваться как оператор при подключении\"],\"xWHwwQ\":[\"Баны\"],\"xYilR2\":[\"Медиа\"],\"xbi8D6\":[\"ÐÑÐ¾Ñ ÑеÑÐ²ÐµÑ Ð½Ðµ поддеÑÐ¶Ð¸Ð²Ð°ÐµÑ Ð¿ÑиглаÑиÑелÑнÑе ÑÑÑлки (возможноÑÑÑ<0>obby.world/invitation0>не обÑÑвлена). ÐÑ Ð¿Ð¾-пÑÐµÐ¶Ð½ÐµÐ¼Ñ Ð¼Ð¾Ð¶ÐµÑе обÑаÑÑÑÑ Ð² ÑаÑе как обÑÑно; ÑÑа Ð¿Ð°Ð½ÐµÐ»Ñ Ð¿ÑедназнаÑена Ð´Ð»Ñ ÑеÑей на базе obbyircd.\"],\"xceQrO\":[\"Поддерживаются только защищённые WebSocket-соединения\"],\"xdtXa+\":[\"имя-канала\"],\"xfXC7q\":[\"Текстовые каналы\"],\"xlCYOE\":[\"Загрузка сообщений...\"],\"xlhswE\":[\"Минимальное значение: \",[\"0\"]],\"xq97Ci\":[\"Добавить слово или фразу...\"],\"xuRqRq\":[\"Лимит пользователей (+l)\"],\"xwF+7J\":[[\"0\"],\" печатает...\"],\"y1eoq1\":[\"ÐопиÑоваÑÑ ÑÑÑлкÑ\"],\"yJztBY\":[\"Удалить сеть\"],\"yNeucF\":[\"Этот сервер не поддерживает расширенные метаданные профиля (расширение IRCv3 METADATA). Дополнительные поля, такие как аватар, отображаемое имя и статус, недоступны.\"],\"yPlrca\":[\"Аватар канала\"],\"yQE2r9\":[\"Загрузка\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Имя пользователя оператора\"],\"yYOzWD\":[\"логи\"],\"yfx9Re\":[\"Пароль IRC-оператора\"],\"ygCKqB\":[\"Стоп\"],\"ymDxJx\":[\"Имя пользователя IRC-оператора\"],\"yrpRsQ\":[\"Сортировать по имени\"],\"yz7wBu\":[\"Закрыть\"],\"zJw+jA\":[\"устанавливает режим: \",[\"0\"]],\"zbymaY\":[[\"0\"],\" мин. назад\"],\"zebeLu\":[\"Введите имя пользователя оператора\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/ru/messages.po b/src/locales/ru/messages.po
index 7db929a0..28052d94 100644
--- a/src/locales/ru/messages.po
+++ b/src/locales/ru/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - Перенося IRC в будущее"
msgid "— open in viewer"
msgstr "— открыть в просмотрщике"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(наследовать)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(без изменений)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} и {1} печатают..."
msgid "{0} is typing..."
msgstr "{0} печатает..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} сеть{1} — выберите одну для подключения"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -69,17 +85,17 @@ msgstr "{0}, {1}, {2} и ещё {3} печатают..."
#. placeholder {0}: Math.floor(secs / 86400)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}d ago"
-msgstr "{0} дн. назад"
+msgstr "{0} дн. назад"
#. placeholder {0}: Math.floor(secs / 3600)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}h ago"
-msgstr "{0} ч. назад"
+msgstr "{0} Ñ. назад"
#. placeholder {0}: Math.floor(secs / 60)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}m ago"
-msgstr "{0} мин. назад"
+msgstr "{0} мин. назад"
#: src/lib/eventGrouping.ts
msgid "{c, plural, one {1 time} other {{c} times}}"
@@ -115,7 +131,7 @@ msgstr "*spam*"
#: src/components/ui/InvitationsPanel.tsx
msgid "#channel (optional)"
-msgstr "#канал (необязательно)"
+msgstr "#канал (необÑзаÑелÑно)"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "#new-channel-name"
@@ -205,6 +221,12 @@ msgstr "Добавить маску приглашения (например: ni
msgid "Add IRC Server"
msgstr "Добавить IRC-сервер"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Добавить сеть"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Добавить правило"
msgid "Add Server"
msgstr "Добавить сервер"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Добавьте вашу первую сеть"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Подробности"
@@ -377,6 +403,10 @@ msgstr "Назад"
msgid "Back to image"
msgstr "Вернуться к изображению"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "К списку сетей"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "Забанить {username} по hostmask (запрещает переподключение с того же IP/хоста)"
@@ -424,6 +454,10 @@ msgstr "Просмотреть все каналы на сервере"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Отменить подключение"
msgid "Cancel reply"
msgstr "Отменить ответ"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Изменить цвет акцента"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Изменить имя канала (только для операторов)"
@@ -564,7 +603,7 @@ msgstr "Очистить поиск"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "Нажмите ещё раз для подтверждения"
+msgstr "ÐажмиÑе еÑÑ Ñаз Ð´Ð»Ñ Ð¿Ð¾Ð´ÑвеÑждениÑ"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "Лимит пользователей (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -666,9 +707,10 @@ msgstr "Настройка звуков уведомлений и выделен
#: src/components/ui/InvitationsPanel.tsx
msgid "Confirm?"
-msgstr "Подтвердить?"
+msgstr "ÐодÑвеÑдиÑÑ?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Подключиться"
@@ -711,11 +753,11 @@ msgstr "Скопировано"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy"
-msgstr "Копировать"
+msgstr "ÐопиÑоваÑÑ"
#: src/components/ui/RawLogViewer.tsx
msgid "Copy all"
-msgstr "Копировать всё"
+msgstr "ÐопиÑоваÑÑ Ð²ÑÑ"
#: src/components/message/JsonLogMessage.tsx
msgid "Copy entire JSON"
@@ -731,7 +773,7 @@ msgstr "Копировать JSON"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy link"
-msgstr "Копировать ссылку"
+msgstr "ÐопиÑоваÑÑ ÑÑÑлкÑ"
#: src/components/ui/ExternalLinkWarningModal.tsx
msgid "Copy URL"
@@ -739,15 +781,15 @@ msgstr "Копировать URL"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create"
-msgstr "Создать"
+msgstr "СоздаÑÑ"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "Создать новую пригласительную ссылку"
+msgstr "СоздаÑÑ Ð½Ð¾Ð²ÑÑ Ð¿ÑиглаÑиÑелÑнÑÑ ÑÑÑлкÑ"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "Создание и управление вашими пригласительными ссылками"
+msgstr "Создание и ÑпÑавление ваÑими пÑиглаÑиÑелÑнÑми ÑÑÑлками"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,27 +856,52 @@ msgstr "Удалить канал"
msgid "Delete message"
msgstr "Удалить сообщение"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Удалить сеть"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Удалить личную переписку"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "Удалить это приглашение"
+msgstr "УдалиÑÑ ÑÑо пÑиглаÑение"
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "Удалить это сообщение? Это действие нельзя отменить."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Удалить эту сеть?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "Описание (необязательно, например «Бета-тестеры Q3»)"
+msgstr "ÐпиÑание (необÑзаÑелÑно, напÑÐ¸Ð¼ÐµÑ Â«ÐеÑа-ÑеÑÑеÑÑ Q3»)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Отключиться"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "Отключить <0>{0}0>?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Отключиться от bouncer soju?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Отключить сеть?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Обзор"
@@ -891,20 +958,31 @@ msgstr "Скачать"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "Перетащите файлы для загрузки"
+msgstr "ÐеÑеÑаÑиÑе ÑÐ°Ð¹Ð»Ñ Ð´Ð»Ñ Ð·Ð°Ð³ÑÑзки"
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "напр. <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "например: 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Изменить"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "Изменить {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Редактировать профиль"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "ГЛАВНАЯ"
msgid "Homepage"
msgstr "Сайт"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Хост"
@@ -1285,7 +1364,7 @@ msgstr "Присоединился к каналу"
#: src/components/ui/InvitationsPanel.tsx
msgid "just now"
-msgstr "только что"
+msgstr "ÑолÑко ÑÑо"
#: src/components/ui/ModerationModal.tsx
#: src/components/ui/UserContextMenu.tsx
@@ -1323,7 +1402,7 @@ msgstr "Покинуть канал"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Оставьте поле канала пустым для общего приглашения в сеть. Описание нужно только для ваших записей — оно видно только вам в этом списке."
+msgstr "ÐÑÑавÑÑе поле канала пÑÑÑÑм Ð´Ð»Ñ Ð¾Ð±Ñего пÑиглаÑÐµÐ½Ð¸Ñ Ð² ÑеÑÑ. ÐпиÑание нÑжно ÑолÑко Ð´Ð»Ñ Ð²Ð°ÑиÑ
запиÑей â оно видно ÑолÑко вам в ÑÑом ÑпиÑке."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "Покинул канал"
msgid "Let others know when you are typing"
msgstr "Сообщать другим, когда вы печатаете"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Предпросмотр ссылки"
@@ -1375,6 +1458,10 @@ msgstr "Загрузка GIF..."
msgid "Loading more channels..."
msgstr "Загрузка дополнительных каналов..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Загрузка сетей с вашего баунсера…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "Загрузка данных WHOIS..."
@@ -1563,12 +1650,23 @@ msgstr "Имя:"
#: src/components/ui/InvitationsPanel.tsx
msgid "network"
-msgstr "сеть"
+msgstr "ÑеÑÑ"
+
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Сеть, связанная через bouncer soju"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Название сети"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Сети на {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Новое DM"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (например: spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Файл не выбран"
msgid "No flood profile"
msgstr "Без профиля флуда"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "хост не задан"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Приглашения не найдены"
@@ -1672,7 +1775,7 @@ msgstr "Предпросмотр медиа не загружается."
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "Необработанный IRC-трафик пока не зафиксирован. Попробуйте подключиться или отправить сообщение."
+msgstr "ÐеобÑабоÑаннÑй IRC-ÑÑаÑик пока не заÑикÑиÑован. ÐопÑобÑйÑе подклÑÑиÑÑÑÑ Ð¸Ð»Ð¸ оÑпÑавиÑÑ ÑообÑение."
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "Результаты не найдены"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "Сервер не выбран. Сначала выберите сервер на боковой панели; пригласительные ссылки управляются отдельно для каждого сервера."
+msgstr "СеÑÐ²ÐµÑ Ð½Ðµ вÑбÑан. СнаÑала вÑбеÑиÑе ÑеÑÐ²ÐµÑ Ð½Ð° боковой панели; пÑиглаÑиÑелÑнÑе ÑÑÑлки ÑпÑавлÑÑÑÑÑ Ð¾ÑделÑно Ð´Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾ ÑеÑвеÑа."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "Тема не задана"
msgid "No unread mentions or messages"
msgstr "Нет непрочитанных упоминаний или сообщений"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Пока нет вышестоящих сетей."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Нет доступных пользователей"
@@ -1784,6 +1891,10 @@ msgstr "Упс! Разрыв сети! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Открыть"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Открыть настройки конфигурации канала"
@@ -1887,6 +1998,10 @@ msgstr "Закрепить личный чат"
msgid "Pin this private message conversation"
msgstr "Закрепить эту личную переписку"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Открытый текст"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "Написать в личку"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Порт"
@@ -2006,6 +2122,7 @@ msgstr "отреагировал на это сообщение"
msgid "Read more"
msgstr "Читать далее"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Причина"
msgid "Reason (optional)"
msgstr "Причина (необязательно)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Переподключиться к серверу"
@@ -2031,7 +2149,7 @@ msgstr "Переподключиться к серверу"
#: src/components/ui/InvitationsPanel.tsx
#: src/components/ui/InvitationsPanel.tsx
msgid "Refresh"
-msgstr "Обновить"
+msgstr "ÐбновиÑÑ"
#: src/components/ui/AddServerModal.tsx
msgid "Register for an account"
@@ -2095,6 +2213,7 @@ msgstr "Правила"
msgid "Safe"
msgstr "Безопасно"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Перемотка"
msgid "Select a channel"
msgstr "Выберите канал"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Выберите сеть"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Выбрать участника"
@@ -2276,6 +2399,10 @@ msgstr "Операторы серверов сети потенциально м
msgid "Server Password"
msgstr "Пароль сервера"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Пароль сервера (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Межсерверное взаимодействие может использовать незашифрованные соединения"
@@ -2380,6 +2507,12 @@ msgstr "В сети с"
msgid "Software:"
msgstr "Программа:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "bouncer soju (управление)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Сортировать по имени"
@@ -2457,7 +2590,11 @@ msgstr "Срок действия этого изображения истёк"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "Столько человек зарегистрировалось по этой ссылке"
+msgstr "СÑолÑко Ñеловек заÑегиÑÑÑиÑовалоÑÑ Ð¿Ð¾ ÑÑой ÑÑÑлке"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Это удалит сеть из вашего bouncer soju. Чтобы использовать её снова, вам нужно будет добавить её обратно."
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "Этот сервер не поддерживает расширенны
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Этот сервер не поддерживает пригласительные ссылки (возможность<0>obby.world/invitation0>не объявлена). Вы по-прежнему можете общаться в чате как обычно; эта панель предназначена для сетей на базе obbyircd."
+msgstr "ÐÑÐ¾Ñ ÑеÑÐ²ÐµÑ Ð½Ðµ поддеÑÐ¶Ð¸Ð²Ð°ÐµÑ Ð¿ÑиглаÑиÑелÑнÑе ÑÑÑлки (возможноÑÑÑ<0>obby.world/invitation0>не обÑÑвлена). ÐÑ Ð¿Ð¾-пÑÐµÐ¶Ð½ÐµÐ¼Ñ Ð¼Ð¾Ð¶ÐµÑе обÑаÑÑÑÑ Ð² ÑаÑе как обÑÑно; ÑÑа Ð¿Ð°Ð½ÐµÐ»Ñ Ð¿ÑедназнаÑена Ð´Ð»Ñ ÑеÑей на базе obbyircd."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Этот сервер поддерживает только один тип подключения"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Также будут закрыты {0} связанных сетей ниже."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Также будет закрыта связанная сеть ниже."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Время (мин)"
@@ -2479,6 +2625,10 @@ msgstr "Время (мин)"
msgid "Time Window (seconds)"
msgstr "Временное окно (секунды)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Тема:"
msgid "Total: {0}"
msgstr "Всего: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Транспорт"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Доверенные источники"
@@ -2615,7 +2769,7 @@ msgstr "Используйте маски: * соответствует любо
#: src/components/ui/InvitationsPanel.tsx
msgid "used"
-msgstr "использовано"
+msgstr "иÑполÑзовано"
#: src/components/message/JsonLogMessage.tsx
#: src/components/ui/UserProfileModal.tsx
@@ -2641,6 +2795,7 @@ msgstr "Профиль пользователя"
msgid "User Settings"
msgstr "Настройки пользователя"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Широкий — более широкая область защиты
msgid "Will default to 'no reason' if left empty"
msgstr "Если оставить пустым, будет использоваться «без причины»"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Да, удалить"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "У вас есть несохранённые изменения. Вы
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "Вы ещё не создали ни одной пригласительной ссылки. Используйте форму выше, чтобы создать первую."
+msgstr "ÐÑ ÐµÑÑ Ð½Ðµ Ñоздали ни одной пÑиглаÑиÑелÑной ÑÑÑлки. ÐÑполÑзÑйÑе ÑоÑÐ¼Ñ Ð²ÑÑе, ÑÑÐ¾Ð±Ñ ÑоздаÑÑ Ð¿ÐµÑвÑÑ."
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "Вы пригласили {target} присоединиться к {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Вы подключены к <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Пароль вашего аккаунта для аутентификации"
@@ -2822,13 +2986,17 @@ msgstr "Пароль вашего аккаунта для аутентифика
msgid "Your account username for authentication"
msgstr "Имя пользователя вашего аккаунта для аутентификации"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "У вашего баунсера ещё нет сетей. Добавьте одну, чтобы начать."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Ваш никнейм по умолчанию для всех серверов"
#: src/components/ui/InvitationsPanel.tsx
msgid "Your invite links"
-msgstr "Ваши пригласительные ссылки"
+msgstr "ÐаÑи пÑиглаÑиÑелÑнÑе ÑÑÑлки"
#: src/components/ui/UserSettings.tsx
msgid "Your messages and settings are stored locally on your device"
diff --git a/src/locales/sv/messages.mjs b/src/locales/sv/messages.mjs
index f51da7e2..1a120229 100644
--- a/src/locales/sv/messages.mjs
+++ b/src/locales/sv/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Ogiltigt mönsterformat. Använd formatet nick!user@host (jokertecken * tillåts)\"],\"+6NQQA\":[\"Allmän supportkanal\"],\"+6NyRG\":[\"Klient\"],\"+K0AvT\":[\"Koppla från\"],\"+cyFdH\":[\"Standardmeddelande när du markerar dig som borta\"],\"+mVPqU\":[\"Rendera markdown-formatering i meddelanden\"],\"+vqCJH\":[\"Ditt kontoanvändarnamn för autentisering\"],\"+yPBXI\":[\"Välj fil\"],\"+zy2Nq\":[\"Typ\"],\"/09cao\":[\"Låg länksäkerhet (nivå \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Användare utanför kanalen kan inte skicka meddelanden till den\"],\"/4C8U0\":[\"Kopiera alla\"],\"/6BzZF\":[\"Växla medlemslista\"],\"/AkXyp\":[\"Bekräfta?\"],\"/TNOPk\":[\"Användaren är borta\"],\"/XQgft\":[\"Utforska\"],\"/cF7Rs\":[\"Volym\"],\"/dqduX\":[\"Nästa sida\"],\"/fc3q4\":[\"Allt innehåll\"],\"/kISDh\":[\"Aktivera aviseringsljud\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Ljud\"],\"/rfkZe\":[\"Spela upp ljud för omnämnanden och meddelanden\"],\"0/0ZGA\":[\"Kanalnamns-mask\"],\"0D6j7U\":[\"Läs mer om anpassade regler →\"],\"0XsHcR\":[\"Sparka ut användare\"],\"0ZpE//\":[\"Sortera efter användare\"],\"0bEPwz\":[\"Ange borta\"],\"0dGkPt\":[\"Expandera kanallistan\"],\"0gS7M5\":[\"Visningsnamn\"],\"0kS+M8\":[\"ExempelNÄT\"],\"0rgoY7\":[\"Anslut bara till servrar du väljer\"],\"0wdd7X\":[\"Gå med\"],\"0wkVYx\":[\"Privata meddelanden\"],\"111uHX\":[\"Länkförhandsvisning\"],\"196EG4\":[\"Ta bort privatchatt\"],\"1DSr1i\":[\"Registrera ett konto\"],\"1O/24y\":[\"Växla kanallista\"],\"1VPJJ2\":[\"Varning för extern länk\"],\"1ZC/dv\":[\"Inga olästa omnämnanden eller meddelanden\"],\"1pO1zi\":[\"Servernamn krävs\"],\"1uwfzQ\":[\"Visa kanalämne\"],\"268g7c\":[\"Ange visningsnamn\"],\"2F9+AZ\":[\"Ingen rå IRC-trafik har fångats än. Försök att ansluta eller skicka ett meddelande.\"],\"2FOFq1\":[\"Serveroperatörer på nätverket kan potentiellt läsa dina meddelanden\"],\"2FYpfJ\":[\"Mer\"],\"2HF1Y2\":[[\"inviter\"],\" bjöd in \",[\"target\"],\" att gå med i \",[\"channel\"]],\"2I70QL\":[\"Visa användarprofilinformation\"],\"2QYdmE\":[\"Användare:\"],\"2QpEjG\":[\"lämnade\"],\"2YE223\":[\"Meddelande #\",[\"0\"],\" (Enter för ny rad, Shift+Enter för att skicka)\"],\"2bimFY\":[\"Använd serverlösenord\"],\"2iTmdZ\":[\"Lokal lagring:\"],\"2odkwe\":[\"Strikt – mer aggressivt skydd\"],\"2uDhbA\":[\"Ange användarnamn att bjuda in\"],\"2ygf/L\":[\"← Tillbaka\"],\"2zEgxj\":[\"Sök GIF:ar...\"],\"3RdPhl\":[\"Byt namn på kanal\"],\"3THokf\":[\"Voice-användare\"],\"3TSz9S\":[\"Minimera\"],\"3jBDvM\":[\"Kanalens visningsnamn\"],\"3ryuFU\":[\"Valfria kraschrapporter för att förbättra appen\"],\"3uBF/8\":[\"Stäng visaren\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Ange kontonamn...\"],\"4/Rr0R\":[\"Bjud in en användare till den aktuella kanalen\"],\"4EZrJN\":[\"Regler\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Flödesprofil (+F)\"],\"4RZQRK\":[\"Vad håller du på med?\"],\"4hfTrB\":[\"Nick\"],\"4n99LO\":[\"Redan i \",[\"0\"]],\"4t6vMV\":[\"Växla automatiskt till enkel rad för korta meddelanden\"],\"4vsHmf\":[\"Tid (min)\"],\"5+INAX\":[\"Markera meddelanden som nämner dig\"],\"5R5Pv/\":[\"Oper-namn\"],\"678PKt\":[\"Nätverksnamn\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Lösenord krävs för att gå med i kanalen. Lämna tomt för att ta bort nyckeln.\"],\"6HhMs3\":[\"Quit-meddelande\"],\"6V3Ea3\":[\"Kopierat\"],\"6lGV3K\":[\"Visa mindre\"],\"6yFOEi\":[\"Ange oper-lösenord...\"],\"7+IHTZ\":[\"Ingen fil vald\"],\"73hrRi\":[\"nick!user@host (t.ex. spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Skicka privat meddelande\"],\"7U1W7c\":[\"Mycket avslappnat\"],\"7Y1YQj\":[\"Riktigt namn:\"],\"7YHArF\":[\"— öppna i visaren\"],\"7fjnVl\":[\"Sök användare...\"],\"7jL88x\":[\"Ta bort det här meddelandet? Det kan inte ångras.\"],\"7nGhhM\":[\"Vad tänker du på?\"],\"7sEpu1\":[\"Medlemmar — \",[\"0\"]],\"7sNhEz\":[\"Användarnamn\"],\"8H0Q+x\":[\"Läs mer om profiler →\"],\"8Phu0A\":[\"Visa när användare byter nick\"],\"8XTG9e\":[\"Ange oper-lösenord\"],\"8XsV2J\":[\"Försök skicka igen\"],\"8ZsakT\":[\"Lösenord\"],\"8kR84m\":[\"Du håller på att öppna en extern länk:\"],\"8lCgih\":[\"Ta bort regel\"],\"8o3dPc\":[\"Släpp filer för att ladda upp\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"gick med\"],\"other\":[\"gick med \",[\"joinCount\"],\" gånger\"]}]],\"9BMLnJ\":[\"Återanslut till server\"],\"9OEgyT\":[\"Lägg till reaktion\"],\"9PQ8m2\":[\"G-Line (globalt ban)\"],\"9Qs99X\":[\"E-post:\"],\"9QupBP\":[\"Ta bort mönster\"],\"9bG48P\":[\"Skickar\"],\"9f5f0u\":[\"Frågor om integritet? Kontakta oss:\"],\"9unqs3\":[\"Frånvarande:\"],\"9v3hwv\":[\"Inga servrar hittades.\"],\"9zb2WA\":[\"Ansluter\"],\"A1taO8\":[\"Sök\"],\"A2adVi\":[\"Skicka skriv-aviseringar\"],\"A9Rhec\":[\"Kanalnamn\"],\"AWOSPo\":[\"Zooma in\"],\"AXSpEQ\":[\"Oper vid anslutning\"],\"AeXO77\":[\"Konto\"],\"AhNP40\":[\"Sök position\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Bytte nick\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Avbryt svar\"],\"ApSx0O\":[\"Hittade \",[\"0\"],\" meddelanden som matchar \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Inga resultat hittades\"],\"AyNqAB\":[\"Visa alla serverhändelser i chatten\"],\"B/QqGw\":[\"Borta från tangentbordet\"],\"B8AaMI\":[\"Det här fältet krävs\"],\"BA2c49\":[\"Servern stöder inte avancerad LIST-filtrering\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" och \",[\"3\"],\" andra skriver...\"],\"BGul2A\":[\"Du har osparade ändringar. Är du säker på att du vill stänga utan att spara?\"],\"BIf9fi\":[\"Ditt statusmeddelande\"],\"BPm98R\":[\"Ingen server är vald. Välj en server från sidofältet först; inbjudningslänkar hanteras per server.\"],\"BZz3md\":[\"Din personliga webbplats\"],\"Bgm/H7\":[\"Tillåt att skriva flera rader text\"],\"BiQIl1\":[\"Fäst den här privata meddelandekonversationen\"],\"BlNZZ2\":[\"Klicka för att hoppa till meddelandet\"],\"Bowq3c\":[\"Endast operatörer kan ändra kanalämnet\"],\"Btozzp\":[\"Den här bilden har gått ut\"],\"Bycfjm\":[\"Totalt: \",[\"0\"]],\"C6IBQc\":[\"Kopiera hela JSON\"],\"C9L9wL\":[\"Datainsamling\"],\"CDq4wC\":[\"Moderera användare\"],\"CHVRxG\":[\"Meddelande @\",[\"0\"],\" (Shift+Enter för ny rad)\"],\"CN9zdR\":[\"Oper-namn och lösenord krävs\"],\"CW3sYa\":[\"Lägg till reaktion \",[\"emoji\"]],\"CaAkqd\":[\"Visa quit-meddelanden\"],\"CbvaYj\":[\"Banna via nick\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Välj en kanal\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"System\"],\"D28t6+\":[\"gick med och lämnade\"],\"DB8zMK\":[\"Tillämpa\"],\"DBcWHr\":[\"Anpassad aviseringsljudfil\"],\"DTy9Xw\":[\"Medieförhandsvisningar\"],\"Dj4pSr\":[\"Välj ett säkert lösenord\"],\"Du+zn+\":[\"Söker...\"],\"Du2T2f\":[\"Inställningen hittades inte\"],\"DwsSVQ\":[\"Tillämpa filter och uppdatera\"],\"E3W/zd\":[\"Standard-nick\"],\"E6nRW7\":[\"Kopiera URL\"],\"E703RG\":[\"Lägen:\"],\"EAeu1Z\":[\"Skicka inbjudan\"],\"EFKJQT\":[\"Inställning\"],\"EGPQBv\":[\"Anpassade flödesregler (+f)\"],\"ELik0r\":[\"Visa fullständig integritetspolicy\"],\"EPbeC2\":[\"Visa eller redigera kanalämnet\"],\"EQCDNT\":[\"Ange oper-användarnamn...\"],\"EUvulZ\":[\"Hittade 1 meddelande som matchar \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Nästa bild\"],\"EdQY6l\":[\"Ingen\"],\"EnqLYU\":[\"Sök servrar...\"],\"F0OKMc\":[\"Redigera server\"],\"F6Int2\":[\"Aktivera markeringar\"],\"FDoLyE\":[\"Max användare\"],\"FUU/hZ\":[\"Styr hur mycket externt media som laddas i chatten.\"],\"Fdp03t\":[\"på\"],\"FfPWR0\":[\"Dialog\"],\"FjkaiT\":[\"Zooma ut\"],\"FlqOE9\":[\"Vad detta innebär:\"],\"FolHNl\":[\"Hantera ditt konto och autentisering\"],\"Fp2Dif\":[\"Lämnade servern\"],\"G5KmCc\":[\"GZ-Line (global Z-Line)\"],\"GDs0lz\":[\"<0>Risk:0> Känslig information (meddelanden, privata konversationer, autentiseringsuppgifter) kan exponeras för nätverksadministratörer eller angripare som befinner sig mellan IRC-servrar.\"],\"GR+2I3\":[\"Lägg till inbjudningsmask (t.ex. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Stäng utfällt servermeddelanden-fönster\"],\"GdhD7H\":[\"Klicka igen för att bekräfta\"],\"GlHnXw\":[\"Namnbyte misslyckades: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Förhandsvisning:\"],\"GtmO8/\":[\"från\"],\"GtuHUQ\":[\"Byt namn på den här kanalen på servern. Alla användare ser det nya namnet.\"],\"GuGfFX\":[\"Växla sökning\"],\"GxkJXS\":[\"Laddar upp...\"],\"GzbwnK\":[\"Gick med i kanalen\"],\"GzsUDB\":[\"Utökad profil\"],\"H/PnT8\":[\"Infoga emoji\"],\"H6Izzl\":[\"Din föredragna färgkod\"],\"H9jIv+\":[\"Visa join/part\"],\"HAKBY9\":[\"Ladda upp filer\"],\"HdE1If\":[\"Kanal\"],\"Hk4AW9\":[\"Ditt föredragna visningsnamn\"],\"HmHDk7\":[\"Välj medlem\"],\"HrQzPU\":[\"Kanaler på \",[\"networkName\"]],\"I2tXQ5\":[\"Meddelande @\",[\"0\"],\" (Enter för ny rad, Shift+Enter för att skicka)\"],\"I6bw/h\":[\"Banna användare\"],\"I92Z+b\":[\"Aktivera aviseringar\"],\"I9D72S\":[\"Är du säker på att du vill ta bort det här meddelandet? Den här åtgärden kan inte ångras.\"],\"IA+1wo\":[\"Visa när användare sparkats ut från kanaler\"],\"IDwkJx\":[\"IRC-operatör\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Spara ändringar\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" och \",[\"2\"],\" skriver...\"],\"IgrLD/\":[\"Pausa\"],\"Im6JED\":[\"VISKNING\"],\"ImOQa9\":[\"Svara\"],\"IoHMnl\":[\"Maximalt värde är \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Ansluter...\"],\"J5T9NW\":[\"Användarinformation\"],\"J8Y5+z\":[\"Hoppsan! Nätverksuppdelning! ⚠️\"],\"JBHkBA\":[\"Lämnade kanalen\"],\"JCwL0Q\":[\"Ange anledning (valfritt)\"],\"JFciKP\":[\"Växla\"],\"JXGkhG\":[\"Ändra kanalnamnet (endast operatörer)\"],\"JcD7qf\":[\"Fler åtgärder\"],\"JdkA+c\":[\"Hemlig (+s)\"],\"Jmu12l\":[\"Serverkanaler\"],\"JvQ++s\":[\"Aktivera Markdown\"],\"K2jwh/\":[\"Ingen WHOIS-data tillgänglig\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Ta bort meddelande\"],\"KKBlUU\":[\"Inbäddning\"],\"KM0pLb\":[\"Välkommen till kanalen!\"],\"KR6W2h\":[\"Sluta ignorera användare\"],\"KV+Bi1\":[\"Endast inbjudna (+i)\"],\"KdCtwE\":[\"Hur många sekunder flödaktivitet ska övervakas innan räknarna återställs\"],\"Kkezga\":[\"Serverlösenord\"],\"KsiQ/8\":[\"Användare måste bjudas in för att gå med i kanalen\"],\"L+gB/D\":[\"Kanalinformation\"],\"LC1a7n\":[\"IRC-servern har rapporterat att dess server-till-server-länkar har en låg säkerhetsnivå. Det innebär att när dina meddelanden vidarebefordras mellan IRC-servrar i nätverket kanske de inte krypteras ordentligt eller att SSL/TLS-certifikaten inte valideras korrekt.\"],\"LNfLR5\":[\"Visa utsparkningar\"],\"LQb0W/\":[\"Visa alla händelser\"],\"LU7/yA\":[\"Alternativt namn för visning i gränssnittet. Får innehålla mellanslag, emoji och specialtecken. Det riktiga kanalnamnet (\",[\"channelName\"],\") används fortfarande för IRC-kommandon.\"],\"LUb9O7\":[\"Giltig serverport krävs\"],\"LV4fT6\":[\"Beskrivning (valfri, t.ex. \\\"Betatestare Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Integritetspolicy\"],\"LcuSDR\":[\"Hantera din profilinformation och metadata\"],\"LqLS9B\":[\"Visa nickbyten\"],\"LsDQt2\":[\"Kanalinställningar\"],\"LtI9AS\":[\"Ägare\"],\"LuNhhL\":[\"reagerade på det här meddelandet\"],\"M/AZNG\":[\"URL till din avatarbild\"],\"M/WIer\":[\"Skicka meddelande\"],\"M8er/5\":[\"Namn:\"],\"MHk+7g\":[\"Föregående bild\"],\"MRorGe\":[\"PM-användare\"],\"MVbSGP\":[\"Tidsfönster (sekunder)\"],\"MkpcsT\":[\"Dina meddelanden och inställningar lagras lokalt på din enhet\"],\"N/hDSy\":[\"Markera som bot – vanligtvis 'on' eller tomt\"],\"N7TQbE\":[\"Bjud in användare till \",[\"channelName\"]],\"NCca/o\":[\"Ange standardsmeknamn...\"],\"Nqs6B9\":[\"Visar allt externt media. Valfri URL kan orsaka en begäran till en okänd server.\"],\"Nt+9O7\":[\"Använd WebSocket istället för råa TCP\"],\"NxIHzc\":[\"Koppla från användare\"],\"O+v/cL\":[\"Bläddra bland alla kanaler på servern\"],\"ODwSCk\":[\"Skicka en GIF\"],\"OGQ5kK\":[\"Konfigurera aviseringsljud och markeringar\"],\"OIPt1Z\":[\"Visa eller dölj medlemslistans sidopanel\"],\"OKSNq/\":[\"Mycket strikt\"],\"ONWvwQ\":[\"Ladda upp\"],\"OVKoQO\":[\"Ditt kontolösenord för autentisering\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - För IRC in i framtiden\"],\"OhCpra\":[\"Ange ett ämne…\"],\"OkltoQ\":[\"Banna \",[\"username\"],\" via nick (förhindrar återanslutning med samma nick)\"],\"P+t/Te\":[\"Inga ytterligare data\"],\"P42Wcc\":[\"Säkert\"],\"PD38l0\":[\"Förhandsvisning av kanalavatar\"],\"PD9mEt\":[\"Skriv ett meddelande...\"],\"PPqfdA\":[\"Öppna kanalens konfigurationsinställningar\"],\"PSCjfZ\":[\"Ämnet som visas för den här kanalen. Alla användare kan se ämnet.\"],\"PZCecv\":[\"PDF-förhandsgranskning\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 gång\"],\"other\":[[\"c\"],\" gånger\"]}]],\"PguS2C\":[\"Lägg till undantagsmask (t.ex. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Visar \",[\"displayedChannelsCount\"],\" av \",[\"0\"],\" kanaler\"],\"PqhVlJ\":[\"Banna användare (via hostmask)\"],\"Q+chwU\":[\"Användarnamn:\"],\"Q2QY4/\":[\"Ta bort den här inbjudan\"],\"Q6hhn8\":[\"Inställningar\"],\"QF4a34\":[\"Ange ett användarnamn\"],\"QGqSZ2\":[\"Färg och formatering\"],\"QJQd1J\":[\"Redigera profil\"],\"QSzGDE\":[\"Inaktiv\"],\"QUlny5\":[\"Välkommen till \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Läs mer\"],\"QuSkCF\":[\"Filtrera kanaler...\"],\"QwUrDZ\":[\"ändrade ämnet till: \",[\"topic\"]],\"R0UH07\":[\"Bild \",[\"0\"],\" av \",[\"1\"]],\"R7SsBE\":[\"Tysta\"],\"R8rf1X\":[\"Klicka för att ange ämne\"],\"RArB3D\":[\"kastades ut från \",[\"channelName\"],\" av \",[\"username\"]],\"RI3cWd\":[\"Utforska IRC-världen med ObsidianIRC\"],\"RIfHS5\":[\"Skapa en ny inbjudningslänk\"],\"RMMaN5\":[\"Modererad (+m)\"],\"RWw9Lg\":[\"Stäng dialog\"],\"RZ2BuZ\":[\"Kontoregistrering för \",[\"account\"],\" kräver verifiering: \",[\"message\"]],\"RySp6q\":[\"Dölj kommentarer\"],\"SPKQTd\":[\"Nick krävs\"],\"SPVjfj\":[\"Standardvärdet är 'ingen anledning' om det lämnas tomt\"],\"SQKPvQ\":[\"Bjud in användare\"],\"SkZcl+\":[\"Välj en fördefinierad flödeskyddsprofil. Dessa profiler ger balanserade skyddsinställningar för olika användningsfall.\"],\"Slr+3C\":[\"Min användare\"],\"Spnlre\":[\"Du bjöd in \",[\"target\"],\" att gå med i \",[\"channel\"]],\"T/ckN5\":[\"Öppna i visaren\"],\"T91vKp\":[\"Spela upp\"],\"TV2Wdu\":[\"Läs om hur vi hanterar dina uppgifter och skyddar din integritet.\"],\"TgFpwD\":[\"Tillämpar...\"],\"TkzSFB\":[\"Inga ändringar\"],\"TtserG\":[\"Ange riktigt namn\"],\"Ttz9J1\":[\"Ange lösenord...\"],\"Tz0i8g\":[\"Inställningar\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reagera\"],\"UE4KO5\":[\"*kanal*\"],\"UETAwW\":[\"Du har inte skapat några inbjudningslänkar än. Använd formuläret ovan för att skapa din första.\"],\"UGT5vp\":[\"Spara inställningar\"],\"UV5hLB\":[\"Inga banningar hittades\"],\"Uaj3Nd\":[\"Statusmeddelanden\"],\"Ue3uny\":[\"Standard (ingen profil)\"],\"UkARhe\":[\"Normal – standardskydd\"],\"Umn7Cj\":[\"Inga kommentarer ännu. Var den första!\"],\"UtUIRh\":[[\"0\"],\" äldre meddelanden\"],\"UwzP+U\":[\"Säker anslutning\"],\"V0/A4O\":[\"Kanalägare\"],\"V4qgxE\":[\"Skapad före (min sedan)\"],\"V8yTm6\":[\"Rensa sökning\"],\"VJMMyz\":[\"ObsidianIRC - För IRC in i framtiden\"],\"VJScHU\":[\"Anledning\"],\"VLsmVV\":[\"Tysta aviseringar\"],\"VbyRUy\":[\"Kommentarer\"],\"Vmx0mQ\":[\"Satt av:\"],\"VqnIZz\":[\"Visa vår integritetspolicy och datapraxis\"],\"VrMygG\":[\"Minimal längd är \",[\"0\"]],\"VrnTui\":[\"Dina pronomen, visas i din profil\"],\"W8E3qn\":[\"Autentiserat konto\"],\"WAakm9\":[\"Ta bort kanal\"],\"WFxTHC\":[\"Lägg till banmask (t.ex. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Serveradress krävs\"],\"WRYdXW\":[\"Ljudposition\"],\"WUOH5B\":[\"Ignorera användare\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Visa 1 till\"],\"other\":[\"Visa \",[\"1\"],\" till\"]}]],\"WYxRzo\":[\"Skapa och hantera dina inbjudningslänkar\"],\"Wd38W1\":[\"Lämna kanal tomt för en allmän nätverksinbjudan. Beskrivningen är bara för dina anteckningar — syns bara för dig i den här listan.\"],\"Weq9zb\":[\"Allmänt\"],\"Wfj7Sk\":[\"Tysta eller aktivera aviseringsljud\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*skräp*\"],\"WzMCru\":[\"Användarprofil\"],\"X6S3lt\":[\"Sök inställningar, kanaler, servrar...\"],\"XEHan5\":[\"Fortsätt ändå\"],\"XI1+wb\":[\"Ogiltigt format\"],\"XIXeuC\":[\"Meddelande @\",[\"0\"]],\"XMS+k4\":[\"Starta privat meddelande\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Lossa privatchatt\"],\"Xm/s+u\":[\"Visning\"],\"Xp2n93\":[\"Visar media från serverns betrodda filvärd. Inga begäranden görs till externa tjänster.\"],\"XvjC4F\":[\"Sparar...\"],\"Y/qryO\":[\"Inga användare hittades som matchar din sökning\"],\"YAqRpI\":[\"Kontoregistrering för \",[\"account\"],\" lyckades: \",[\"message\"]],\"YEfzvP\":[\"Skyddat ämne (+t)\"],\"YQOn6a\":[\"Dölj medlemslistan\"],\"YRCoE9\":[\"Kanaloperatör\"],\"YURQaF\":[\"Visa profil\"],\"YdBSvr\":[\"Styr medievisning och externt innehåll\"],\"Yj6U3V\":[\"Ingen central server:\"],\"YjvpGx\":[\"Pronomen\"],\"YqH4l4\":[\"Ingen nyckel\"],\"YyUPpV\":[\"Konto:\"],\"ZJSWfw\":[\"Meddelande som visas när du kopplar från servern\"],\"ZR1dJ4\":[\"Inbjudningar\"],\"ZdWg0V\":[\"Öppna i webbläsare\"],\"ZhRBbl\":[\"Sök meddelanden…\"],\"Zmcu3y\":[\"Avancerade filter\"],\"a2/8e5\":[\"Ämne angivet efter (min sedan)\"],\"aHKcKc\":[\"Föregående sida\"],\"aJTbXX\":[\"Oper-lösenord\"],\"aQryQv\":[\"Mönstret finns redan\"],\"aW9pLN\":[\"Maximalt antal användare som tillåts i kanalen. Lämna tomt för ingen gräns.\"],\"ah4fmZ\":[\"Visar också förhandsvisningar från YouTube, Vimeo, SoundCloud och liknande kända tjänster.\"],\"aifXak\":[\"Inget media i den här kanalen\"],\"ap2zBz\":[\"Avslappnat\"],\"az8lvo\":[\"Av\"],\"azXSNo\":[\"Expandera medlemslistan\"],\"azdliB\":[\"Logga in på ett konto\"],\"b26wlF\":[\"hon/hennes\"],\"bD/+Ei\":[\"Strikt\"],\"bQ6BJn\":[\"Konfigurera detaljerade flödeskyddsregler. Varje regel anger vilken typ av aktivitet som ska övervakas och vilken åtgärd som ska vidtas när gränser överskrids.\"],\"beV7+y\":[\"Användaren får en inbjudan att gå med i \",[\"channelName\"],\".\"],\"bk84cH\":[\"Borta-meddelande\"],\"bkHdLj\":[\"Lägg till IRC-server\"],\"bmQLn5\":[\"Lägg till regel\"],\"bwRvnp\":[\"Åtgärd\"],\"c8+EVZ\":[\"Verifierat konto\"],\"cGYUlD\":[\"Inga medieförhandsvisningar laddas.\"],\"cLF98o\":[\"Visa kommentarer (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Inga användare tillgängliga\"],\"cSgpoS\":[\"Fäst privatchatt\"],\"cde3ce\":[\"Meddelande <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Kopiera formaterad utdata\"],\"cl/A5J\":[\"Välkommen till \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Ta bort\"],\"coPLXT\":[\"Vi lagrar inte dina IRC-kommunikationer på våra servrar\"],\"crYH/6\":[\"SoundCloud-spelare\"],\"d3sis4\":[\"Lägg till server\"],\"d9aN5k\":[\"Ta bort \",[\"username\"],\" från kanalen\"],\"dEgA5A\":[\"Avbryt\"],\"dGi1We\":[\"Lossa den här privata meddelandekonversationen\"],\"dJVuyC\":[\"lämnade \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"till\"],\"dXqxlh\":[\"<0>⚠️ Säkerhetsrisk!0> Den här anslutningen kan vara sårbar för avlyssning eller man-in-the-middle-attacker.\"],\"da9Q/R\":[\"Ändrade kanallägen\"],\"dhJN3N\":[\"Visa kommentarer\"],\"dj2xTE\":[\"Stäng avisering\"],\"dpCzmC\":[\"Inställningar för flödeskydd\"],\"e9dQpT\":[\"Vill du öppna den här länken i en ny flik?\"],\"ePK91l\":[\"Redigera\"],\"eYBDuB\":[\"Ladda upp en bild eller ange en URL med valfri \",[\"size\"],\"-ersättning för dynamisk storleksanpassning\"],\"edBbee\":[\"Banna \",[\"username\"],\" via hostmask (förhindrar återanslutning från samma IP/host)\"],\"ekfzWq\":[\"Användarinställningar\"],\"elPDWs\":[\"Anpassa din IRC-klientupplevelse\"],\"eu2osY\":[\"<0>💡 Rekommendation:0> Fortsätt bara om du litar på den här servern och förstår riskerna. Undvik att dela känslig information eller lösenord via den här anslutningen.\"],\"euEhbr\":[\"Klicka för att gå med i \",[\"channel\"]],\"ez3vLd\":[\"Aktivera flerrads-inmatning\"],\"f0J5Ki\":[\"Server-till-server-kommunikation kan använda okrypterade anslutningar\"],\"f9BHJk\":[\"Varna användare\"],\"fDOLLd\":[\"Inga kanaler hittades.\"],\"ffzDkB\":[\"Anonym analys:\"],\"fq1GF9\":[\"Visa när användare kopplar från servern\"],\"gEF57C\":[\"Den här servern stöder bara en anslutningstyp\"],\"gJuLUI\":[\"Ignorera-lista\"],\"gNzMrk\":[\"Nuvarande avatar\"],\"gjPWyO\":[\"Ange smeknamn...\"],\"gz6UQ3\":[\"Maximera\"],\"h6razj\":[\"Uteslut kanalnamns-mask\"],\"hG6jnw\":[\"Inget ämne angivet\"],\"hG89Ed\":[\"Bild\"],\"hYgDIe\":[\"Skapa\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"t.ex. 100:1440\"],\"he3ygx\":[\"Kopiera\"],\"hehnjM\":[\"Mängd\"],\"hzdLuQ\":[\"Endast användare med Voice eller högre kan tala\"],\"i0qMbr\":[\"Hem\"],\"iDNBZe\":[\"Aviseringar\"],\"iH8pgl\":[\"Tillbaka\"],\"iL9SZg\":[\"Banna användare (via nick)\"],\"iNt+3c\":[\"Tillbaka till bild\"],\"iQvi+a\":[\"Varna mig inte om låg länksäkerhet för den här servern\"],\"iSLIjg\":[\"Anslut\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Serveradress\"],\"idD8Ev\":[\"Sparat\"],\"iivqkW\":[\"Inloggad sedan\"],\"ij+Elv\":[\"Bildförhandsvisning\"],\"ilIWp7\":[\"Växla aviseringar\"],\"iuaqvB\":[\"Använd * som jokertecken. Exempel: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Banna via hostmask\"],\"jA4uoI\":[\"Ämne:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Anledning (valfritt)\"],\"jUV7CU\":[\"Ladda upp avatar\"],\"jW5Uwh\":[\"Styr hur mycket externt media som laddas. Av / Säkert / Betrodda källor / Allt innehåll.\"],\"jXzms5\":[\"Bilagealternativ\"],\"jZlrte\":[\"Färg\"],\"jfC/xh\":[\"Kontakt\"],\"jywMpv\":[\"#nytt-kanalnamn\"],\"k112DD\":[\"Ladda äldre meddelanden\"],\"k3ID0F\":[\"Filtrera medlemmar…\"],\"k65gsE\":[\"Fördjupning\"],\"k7Zgob\":[\"Avbryt anslutning\"],\"kAVx5h\":[\"Inga inbjudningar hittades\"],\"kCLEPU\":[\"Ansluten till\"],\"kF5LKb\":[\"Ignorerade mönster:\"],\"kGeOx/\":[\"Gå med i \",[\"0\"]],\"kITKr8\":[\"Laddar kanallägen...\"],\"kPpPsw\":[\"Du är en IRC-operatör\"],\"kWJmRL\":[\"Du\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Kopiera JSON\"],\"krViRy\":[\"Klicka för att kopiera som JSON\"],\"ks71ra\":[\"Undantag\"],\"kw4lRv\":[\"Kanal-halvoperatör\"],\"kxgIRq\":[\"Välj eller lägg till en kanal för att komma igång.\"],\"ky6dWe\":[\"Förhandsvisning av avatar\"],\"l+GxCv\":[\"Laddar kanaler...\"],\"l+IUVW\":[\"Kontoverifiering för \",[\"account\"],\" lyckades: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"återanslöt\"],\"other\":[\"återanslöt \",[\"reconnectCount\"],\" gånger\"]}]],\"l1l8sj\":[[\"0\"],\"d sedan\"],\"l5NhnV\":[\"#kanal (valfritt)\"],\"l5jmzx\":[[\"0\"],\" och \",[\"1\"],\" skriver...\"],\"lCF0wC\":[\"Uppdatera\"],\"lHy8N5\":[\"Laddar fler kanaler...\"],\"lasgrr\":[\"använd\"],\"lbpf14\":[\"Gå med i \",[\"value\"]],\"lfFsZ4\":[\"Kanaler\"],\"lkNdiH\":[\"Kontonamn\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Ladda upp bild\"],\"loQxaJ\":[\"Jag är tillbaka\"],\"lvfaxv\":[\"HEM\"],\"m16xKo\":[\"Lägg till\"],\"m8flAk\":[\"Förhandsvisning (ej uppladdad ännu)\"],\"mEPxTp\":[\"<0>⚠️ Var försiktig!0> Öppna bara länkar från betrodda källor. Skadliga länkar kan äventyra din säkerhet eller integritet.\"],\"mHGdhG\":[\"Serverinformation\"],\"mHS8lb\":[\"Meddelande #\",[\"0\"]],\"mMYBD9\":[\"Brett – bredare skyddsomfång\"],\"mTGsPd\":[\"Kanalämne\"],\"mU8j6O\":[\"Inga externa meddelanden (+n)\"],\"mZp8FL\":[\"Automatisk återgång till enkel rad\"],\"mdQu8G\":[\"DittNick\"],\"miSSBQ\":[\"Kommentarer (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Användaren är autentiserad\"],\"mwtcGl\":[\"Stäng kommentarer\"],\"mzI/c+\":[\"Ladda ned\"],\"n3fGRk\":[\"angett av \",[\"0\"]],\"nE9jsU\":[\"Avslappnat – mindre aggressivt skydd\"],\"nNflMD\":[\"Lämna kanal\"],\"nPXkBi\":[\"Laddar WHOIS-data...\"],\"nQnxxF\":[\"Meddelande #\",[\"0\"],\" (Shift+Enter för ny rad)\"],\"nWMRxa\":[\"Lossa\"],\"nkC032\":[\"Ingen flödesprofil\"],\"o69z4d\":[\"Skicka ett varningsmeddelande till \",[\"username\"]],\"o9ylQi\":[\"Sök efter GIF:ar för att komma igång\"],\"oFGkER\":[\"Servermeddelanden\"],\"oOi11l\":[\"Scrolla till botten\"],\"oPYIL5\":[\"nätverk\"],\"oQEzQR\":[\"Nytt DM\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Man-in-the-middle-attacker på serverlänkar är möjliga\"],\"oeqmmJ\":[\"Betrodda källor\"],\"optX0N\":[[\"0\"],\"h sedan\"],\"ovBPCi\":[\"Standard\"],\"p0Z69r\":[\"Mönstret kan inte vara tomt\"],\"p1KgtK\":[\"Det gick inte att läsa in ljud\"],\"p59pEv\":[\"Ytterligare detaljer\"],\"p7sRI6\":[\"Låt andra se när du skriver\"],\"pBm1od\":[\"Hemlig kanal\"],\"pNmiXx\":[\"Ditt standard-nick för alla servrar\"],\"pUUo9G\":[\"Värdnamn:\"],\"pVGPmz\":[\"Kontolösenord\"],\"peNE68\":[\"Permanent\"],\"plhHQt\":[\"Inga data\"],\"pm6+q5\":[\"Säkerhetsvarning\"],\"pn5qSs\":[\"Ytterligare information\"],\"q0cR4S\":[\"är nu känd som **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanalen visas inte i LIST- eller NAMES-kommandon\"],\"qLpTm/\":[\"Ta bort reaktion \",[\"emoji\"]],\"qVkGWK\":[\"Fäst\"],\"qY8wNa\":[\"Hemsida\"],\"qb0xJ7\":[\"Använd jokertecken: * matchar valfri sekvens, ? matchar valfritt enskilt tecken. Exempel: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Kanalnyckel (+k)\"],\"qtoOYG\":[\"Ingen gräns\"],\"r1W2AS\":[\"Filserverbild\"],\"rIPR2O\":[\"Ämne angivet före (min sedan)\"],\"rMMSYo\":[\"Maximal längd är \",[\"0\"]],\"rWtzQe\":[\"Nätverket splittrades och återanslöts. ✅\"],\"rYG2u6\":[\"Vänta...\"],\"rdUucN\":[\"Förhandsvisning\"],\"rjGI/Q\":[\"Integritet\"],\"rk8iDX\":[\"Laddar GIF:ar...\"],\"rn6SBY\":[\"Sluta tysta\"],\"s/UKqq\":[\"Sparkades ut från kanalen\"],\"s8cATI\":[\"gick med i \",[\"channelName\"]],\"sCO9ue\":[\"Anslutningen till <0>\",[\"serverName\"],\"0> har följande säkerhetsproblem:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"är nu känd som **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" bjöd in dig att gå med i \",[\"channel\"]],\"sby+1/\":[\"Klicka för att kopiera\"],\"sfN25C\":[\"Ditt riktiga eller fullständiga namn\"],\"sliuzR\":[\"Öppna länk\"],\"sqrO9R\":[\"Anpassade omnämnanden\"],\"sr6RdJ\":[\"Flerrad med Shift+Enter\"],\"swrCpB\":[\"Kanalen har döpts om från \",[\"oldName\"],\" till \",[\"newName\"],\" av \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Avancerat\"],\"t/YqKh\":[\"Ta bort\"],\"t47eHD\":[\"Din unika identifierare på den här servern\"],\"tAkAh0\":[\"URL med valfri \",[\"size\"],\"-ersättning för dynamisk storleksanpassning. Exempel: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Visa eller dölj kanallistans sidopanel\"],\"tfDRzk\":[\"Spara\"],\"tiBsJk\":[\"lämnade \",[\"channelName\"]],\"tt4/UD\":[\"lämnade (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Smeknamnet {nick} används redan, försöker med {newNick}\"],\"u0a8B4\":[\"Autentisera som IRC-operatör för administrativ åtkomst\"],\"u0rWFU\":[\"Skapad efter (min sedan)\"],\"u72w3t\":[\"Användare och mönster att ignorera\"],\"u7jc2L\":[\"lämnade\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Sparning misslyckades: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC-servrar:\"],\"ukyW4o\":[\"Dina inbjudningslänkar\"],\"usSSr/\":[\"Zoomnivå\"],\"v7uvcf\":[\"Programvara:\"],\"vE8kb+\":[\"Använd Shift+Enter för nya rader (Enter skickar)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Inget ämne\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Språk\"],\"vaHYxN\":[\"Riktigt namn\"],\"vhjbKr\":[\"Borta\"],\"w4NYox\":[[\"title\"],\" klient\"],\"w8xQRx\":[\"Ogiltigt värde\"],\"wFjjxZ\":[\"kastades ut från \",[\"channelName\"],\" av \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Inga banundantag hittades\"],\"wPrGnM\":[\"Kanaladmin\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Visa när användare går med i eller lämnar kanaler\"],\"whqZ9r\":[\"Ytterligare ord eller fraser att markera\"],\"wm7RV4\":[\"Aviseringsljud\"],\"wz/Yoq\":[\"Dina meddelanden kan avlyssnas när de vidarebefordras mellan servrar\"],\"x3+y8b\":[\"Så här många registrerade sig via den här länken\"],\"xCJdfg\":[\"Rensa\"],\"xOTzt5\":[\"just nu\"],\"xUHRTR\":[\"Autentisera automatiskt som operatör vid anslutning\"],\"xWHwwQ\":[\"Banningar\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"Den här servern stöder inte inbjudningslänkar (kapabiliteten<0>obby.world/invitation0>annonseras inte). Du kan fortfarande chatta normalt; den här panelen är för nätverk som drivs av obbyircd.\"],\"xceQrO\":[\"Endast säkra websockets stöds\"],\"xdtXa+\":[\"kanalnamn\"],\"xfXC7q\":[\"Textkanaler\"],\"xlCYOE\":[\"Hämtar fler meddelanden...\"],\"xlhswE\":[\"Minimalt värde är \",[\"0\"]],\"xq97Ci\":[\"Lägg till ett ord eller en fras...\"],\"xuRqRq\":[\"Klientgräns (+l)\"],\"xwF+7J\":[[\"0\"],\" skriver...\"],\"y1eoq1\":[\"Kopiera länk\"],\"yNeucF\":[\"Den här servern stöder inte utökad profilmetadata (IRCv3 METADATA-tillägget). Ytterligare fält som avatar, visningsnamn och status är inte tillgängliga.\"],\"yPlrca\":[\"Kanalavatar\"],\"yQE2r9\":[\"Laddar\"],\"ySU+JY\":[\"din@epost.se\"],\"yTX1Rt\":[\"Oper-användarnamn\"],\"yYOzWD\":[\"loggar\"],\"yfx9Re\":[\"IRC-operatörslösenord\"],\"ygCKqB\":[\"Stoppa\"],\"ymDxJx\":[\"IRC-operatörens användarnamn\"],\"yrpRsQ\":[\"Sortera efter namn\"],\"yz7wBu\":[\"Stäng\"],\"zJw+jA\":[\"anger läge: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"m sedan\"],\"zebeLu\":[\"Ange oper-användarnamn\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Ogiltigt mönsterformat. Använd formatet nick!user@host (jokertecken * tillåts)\"],\"+6NQQA\":[\"Allmän supportkanal\"],\"+6NyRG\":[\"Klient\"],\"+K0AvT\":[\"Koppla från\"],\"+cyFdH\":[\"Standardmeddelande när du markerar dig som borta\"],\"+mVPqU\":[\"Rendera markdown-formatering i meddelanden\"],\"+vqCJH\":[\"Ditt kontoanvändarnamn för autentisering\"],\"+yPBXI\":[\"Välj fil\"],\"+zy2Nq\":[\"Typ\"],\"/09cao\":[\"Låg länksäkerhet (nivå \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Användare utanför kanalen kan inte skicka meddelanden till den\"],\"/4C8U0\":[\"Kopiera alla\"],\"/6BzZF\":[\"Växla medlemslista\"],\"/AkXyp\":[\"Bekräfta?\"],\"/TNOPk\":[\"Användaren är borta\"],\"/XQgft\":[\"Utforska\"],\"/cF7Rs\":[\"Volym\"],\"/dqduX\":[\"Nästa sida\"],\"/fc3q4\":[\"Allt innehåll\"],\"/kISDh\":[\"Aktivera aviseringsljud\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Ljud\"],\"/rfkZe\":[\"Spela upp ljud för omnämnanden och meddelanden\"],\"0/0ZGA\":[\"Kanalnamns-mask\"],\"0D6j7U\":[\"Läs mer om anpassade regler →\"],\"0XsHcR\":[\"Sparka ut användare\"],\"0ZpE//\":[\"Sortera efter användare\"],\"0bEPwz\":[\"Ange borta\"],\"0dGkPt\":[\"Expandera kanallistan\"],\"0gS7M5\":[\"Visningsnamn\"],\"0kS+M8\":[\"ExempelNÄT\"],\"0rgoY7\":[\"Anslut bara till servrar du väljer\"],\"0wdd7X\":[\"Gå med\"],\"0wkVYx\":[\"Privata meddelanden\"],\"111uHX\":[\"Länkförhandsvisning\"],\"196EG4\":[\"Ta bort privatchatt\"],\"1DSr1i\":[\"Registrera ett konto\"],\"1O/24y\":[\"Växla kanallista\"],\"1TNIig\":[\"Öppna\"],\"1VPJJ2\":[\"Varning för extern länk\"],\"1ZC/dv\":[\"Inga olästa omnämnanden eller meddelanden\"],\"1pO1zi\":[\"Servernamn krävs\"],\"1uwfzQ\":[\"Visa kanalämne\"],\"268g7c\":[\"Ange visningsnamn\"],\"2CEOW6\":[\"Nätverk bundet via soju-bouncer\"],\"2F9+AZ\":[\"Ingen rÃ¥ IRC-trafik har fÃ¥ngats än. Försök att ansluta eller skicka ett meddelande.\"],\"2FOFq1\":[\"Serveroperatörer på nätverket kan potentiellt läsa dina meddelanden\"],\"2FYpfJ\":[\"Mer\"],\"2HF1Y2\":[[\"inviter\"],\" bjöd in \",[\"target\"],\" att gå med i \",[\"channel\"]],\"2I70QL\":[\"Visa användarprofilinformation\"],\"2QYdmE\":[\"Användare:\"],\"2QpEjG\":[\"lämnade\"],\"2YE223\":[\"Meddelande #\",[\"0\"],\" (Enter för ny rad, Shift+Enter för att skicka)\"],\"2bimFY\":[\"Använd serverlösenord\"],\"2iTmdZ\":[\"Lokal lagring:\"],\"2odkwe\":[\"Strikt – mer aggressivt skydd\"],\"2uDhbA\":[\"Ange användarnamn att bjuda in\"],\"2ygf/L\":[\"← Tillbaka\"],\"2zEgxj\":[\"Sök GIF:ar...\"],\"3RdPhl\":[\"Byt namn på kanal\"],\"3THokf\":[\"Voice-användare\"],\"3TSz9S\":[\"Minimera\"],\"3jBDvM\":[\"Kanalens visningsnamn\"],\"3ryuFU\":[\"Valfria kraschrapporter för att förbättra appen\"],\"3uBF/8\":[\"Stäng visaren\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Ange kontonamn...\"],\"4/Rr0R\":[\"Bjud in en användare till den aktuella kanalen\"],\"4EZrJN\":[\"Regler\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Flödesprofil (+F)\"],\"4RZQRK\":[\"Vad håller du på med?\"],\"4hfTrB\":[\"Nick\"],\"4n99LO\":[\"Redan i \",[\"0\"]],\"4t6vMV\":[\"Växla automatiskt till enkel rad för korta meddelanden\"],\"4vsHmf\":[\"Tid (min)\"],\"4x/Axu\":[\"Din bouncer har inga nätverk ännu. Lägg till ett för att komma igång.\"],\"5+INAX\":[\"Markera meddelanden som nämner dig\"],\"5R5Pv/\":[\"Oper-namn\"],\"678PKt\":[\"Nätverksnamn\"],\"6Aih4U\":[\"Offline\"],\"6CO3WE\":[\"Lösenord krävs för att gå med i kanalen. Lämna tomt för att ta bort nyckeln.\"],\"6HhMs3\":[\"Quit-meddelande\"],\"6V3Ea3\":[\"Kopierat\"],\"6lGV3K\":[\"Visa mindre\"],\"6yFOEi\":[\"Ange oper-lösenord...\"],\"7+IHTZ\":[\"Ingen fil vald\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (t.ex. spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Skicka privat meddelande\"],\"7U1W7c\":[\"Mycket avslappnat\"],\"7Y1YQj\":[\"Riktigt namn:\"],\"7YHArF\":[\"— öppna i visaren\"],\"7fjnVl\":[\"Sök användare...\"],\"7jL88x\":[\"Ta bort det här meddelandet? Det kan inte ångras.\"],\"7nGhhM\":[\"Vad tänker du på?\"],\"7sEpu1\":[\"Medlemmar — \",[\"0\"]],\"7sNhEz\":[\"Användarnamn\"],\"8H0Q+x\":[\"Läs mer om profiler →\"],\"8Phu0A\":[\"Visa när användare byter nick\"],\"8XTG9e\":[\"Ange oper-lösenord\"],\"8XsV2J\":[\"Försök skicka igen\"],\"8ZsakT\":[\"Lösenord\"],\"8kR84m\":[\"Du håller på att öppna en extern länk:\"],\"8lCgih\":[\"Ta bort regel\"],\"8o3dPc\":[\"Släpp filer för att ladda upp\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"gick med\"],\"other\":[\"gick med \",[\"joinCount\"],\" gånger\"]}]],\"9BMLnJ\":[\"Återanslut till server\"],\"9OEgyT\":[\"Lägg till reaktion\"],\"9PQ8m2\":[\"G-Line (globalt ban)\"],\"9Qs99X\":[\"E-post:\"],\"9QupBP\":[\"Ta bort mönster\"],\"9W7tl5\":[\"(oförändrad)\"],\"9bG48P\":[\"Skickar\"],\"9f5f0u\":[\"Frågor om integritet? Kontakta oss:\"],\"9iweoP\":[\"Nätverk på \",[\"0\"]],\"9unqs3\":[\"Frånvarande:\"],\"9v3hwv\":[\"Inga servrar hittades.\"],\"9zb2WA\":[\"Ansluter\"],\"A1taO8\":[\"Sök\"],\"A2adVi\":[\"Skicka skriv-aviseringar\"],\"A9Rhec\":[\"Kanalnamn\"],\"AWOSPo\":[\"Zooma in\"],\"AXSpEQ\":[\"Oper vid anslutning\"],\"AdKRCX\":[\"Du är ansluten till <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Konto\"],\"AhNP40\":[\"Sök position\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Bytte nick\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Avbryt svar\"],\"ApSx0O\":[\"Hittade \",[\"0\"],\" meddelanden som matchar \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Inga resultat hittades\"],\"AyNqAB\":[\"Visa alla serverhändelser i chatten\"],\"B/QqGw\":[\"Borta från tangentbordet\"],\"B0sB2k\":[\"Klartext\"],\"B8AaMI\":[\"Det här fältet krävs\"],\"BA2c49\":[\"Servern stöder inte avancerad LIST-filtrering\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" och \",[\"3\"],\" andra skriver...\"],\"BGul2A\":[\"Du har osparade ändringar. Är du säker på att du vill stänga utan att spara?\"],\"BIf9fi\":[\"Ditt statusmeddelande\"],\"BOJWfb\":[\"Koppla från soju-bouncern?\"],\"BPm98R\":[\"Ingen server är vald. Välj en server frÃ¥n sidofältet först; inbjudningslänkar hanteras per server.\"],\"BZz3md\":[\"Din personliga webbplats\"],\"Bgm/H7\":[\"Tillåt att skriva flera rader text\"],\"BiQIl1\":[\"Fäst den här privata meddelandekonversationen\"],\"BlNZZ2\":[\"Klicka för att hoppa till meddelandet\"],\"Bowq3c\":[\"Endast operatörer kan ändra kanalämnet\"],\"Btozzp\":[\"Den här bilden har gått ut\"],\"Bycfjm\":[\"Totalt: \",[\"0\"]],\"C6IBQc\":[\"Kopiera hela JSON\"],\"C9L9wL\":[\"Datainsamling\"],\"CDq4wC\":[\"Moderera användare\"],\"CHVRxG\":[\"Meddelande @\",[\"0\"],\" (Shift+Enter för ny rad)\"],\"CN9zdR\":[\"Oper-namn och lösenord krävs\"],\"CW3sYa\":[\"Lägg till reaktion \",[\"emoji\"]],\"CaAkqd\":[\"Visa quit-meddelanden\"],\"CbvaYj\":[\"Banna via nick\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Välj en kanal\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"System\"],\"D28t6+\":[\"gick med och lämnade\"],\"DB8zMK\":[\"Tillämpa\"],\"DBcWHr\":[\"Anpassad aviseringsljudfil\"],\"DTy9Xw\":[\"Medieförhandsvisningar\"],\"Dj4pSr\":[\"Välj ett säkert lösenord\"],\"Du+zn+\":[\"Söker...\"],\"Du2T2f\":[\"Inställningen hittades inte\"],\"DwsSVQ\":[\"Tillämpa filter och uppdatera\"],\"E3W/zd\":[\"Standard-nick\"],\"E6nRW7\":[\"Kopiera URL\"],\"E703RG\":[\"Lägen:\"],\"EAeu1Z\":[\"Skicka inbjudan\"],\"EFKJQT\":[\"Inställning\"],\"EGPQBv\":[\"Anpassade flödesregler (+f)\"],\"ELik0r\":[\"Visa fullständig integritetspolicy\"],\"EPbeC2\":[\"Visa eller redigera kanalämnet\"],\"EQCDNT\":[\"Ange oper-användarnamn...\"],\"EUvulZ\":[\"Hittade 1 meddelande som matchar \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Nästa bild\"],\"EdQY6l\":[\"Ingen\"],\"EnqLYU\":[\"Sök servrar...\"],\"F0OKMc\":[\"Redigera server\"],\"F6Int2\":[\"Aktivera markeringar\"],\"FDoLyE\":[\"Max användare\"],\"FUU/hZ\":[\"Styr hur mycket externt media som laddas i chatten.\"],\"Fdp03t\":[\"på\"],\"FfPWR0\":[\"Dialog\"],\"FjkaiT\":[\"Zooma ut\"],\"FlqOE9\":[\"Vad detta innebär:\"],\"FolHNl\":[\"Hantera ditt konto och autentisering\"],\"Fp2Dif\":[\"Lämnade servern\"],\"G5KmCc\":[\"GZ-Line (global Z-Line)\"],\"GDs0lz\":[\"<0>Risk:0> Känslig information (meddelanden, privata konversationer, autentiseringsuppgifter) kan exponeras för nätverksadministratörer eller angripare som befinner sig mellan IRC-servrar.\"],\"GR+2I3\":[\"Lägg till inbjudningsmask (t.ex. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Stäng utfällt servermeddelanden-fönster\"],\"GdhD7H\":[\"Klicka igen för att bekräfta\"],\"GjRZex\":[\"Koppla från nätverket?\"],\"GlHnXw\":[\"Namnbyte misslyckades: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Förhandsvisning:\"],\"GtmO8/\":[\"från\"],\"GtuHUQ\":[\"Byt namn på den här kanalen på servern. Alla användare ser det nya namnet.\"],\"GuGfFX\":[\"Växla sökning\"],\"GxkJXS\":[\"Laddar upp...\"],\"GzbwnK\":[\"Gick med i kanalen\"],\"GzsUDB\":[\"Utökad profil\"],\"H/PnT8\":[\"Infoga emoji\"],\"H6Izzl\":[\"Din föredragna färgkod\"],\"H9jIv+\":[\"Visa join/part\"],\"HAKBY9\":[\"Ladda upp filer\"],\"HdE1If\":[\"Kanal\"],\"Hk4AW9\":[\"Ditt föredragna visningsnamn\"],\"HmHDk7\":[\"Välj medlem\"],\"HrQzPU\":[\"Kanaler på \",[\"networkName\"]],\"I2tXQ5\":[\"Meddelande @\",[\"0\"],\" (Enter för ny rad, Shift+Enter för att skicka)\"],\"I6bw/h\":[\"Banna användare\"],\"I92Z+b\":[\"Aktivera aviseringar\"],\"I9D72S\":[\"Är du säker på att du vill ta bort det här meddelandet? Den här åtgärden kan inte ångras.\"],\"IA+1wo\":[\"Visa när användare sparkats ut från kanaler\"],\"IDwkJx\":[\"IRC-operatör\"],\"ILlU+s\":[\"Info:\"],\"IUwGEM\":[\"Spara ändringar\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" och \",[\"2\"],\" skriver...\"],\"IgrLD/\":[\"Pausa\"],\"Im6JED\":[\"VISKNING\"],\"ImOQa9\":[\"Svara\"],\"IoHMnl\":[\"Maximalt värde är \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Ansluter...\"],\"J5T9NW\":[\"Användarinformation\"],\"J8Y5+z\":[\"Hoppsan! Nätverksuppdelning! ⚠️\"],\"JBHkBA\":[\"Lämnade kanalen\"],\"JCwL0Q\":[\"Ange anledning (valfritt)\"],\"JFciKP\":[\"Växla\"],\"JXGkhG\":[\"Ändra kanalnamnet (endast operatörer)\"],\"JcD7qf\":[\"Fler åtgärder\"],\"JdkA+c\":[\"Hemlig (+s)\"],\"Jmu12l\":[\"Serverkanaler\"],\"JoQY+E\":[\"Detta tar bort nätverket från din soju-bouncer. För att använda det igen måste du lägga till det på nytt.\"],\"JvQ++s\":[\"Aktivera Markdown\"],\"K2jwh/\":[\"Ingen WHOIS-data tillgänglig\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Ta bort meddelande\"],\"KKBlUU\":[\"Inbäddning\"],\"KM0pLb\":[\"Välkommen till kanalen!\"],\"KR6W2h\":[\"Sluta ignorera användare\"],\"KV+Bi1\":[\"Endast inbjudna (+i)\"],\"KdCtwE\":[\"Hur många sekunder flödaktivitet ska övervakas innan räknarna återställs\"],\"Kkezga\":[\"Serverlösenord\"],\"KsiQ/8\":[\"Användare måste bjudas in för att gå med i kanalen\"],\"L+gB/D\":[\"Kanalinformation\"],\"LC1a7n\":[\"IRC-servern har rapporterat att dess server-till-server-länkar har en låg säkerhetsnivå. Det innebär att när dina meddelanden vidarebefordras mellan IRC-servrar i nätverket kanske de inte krypteras ordentligt eller att SSL/TLS-certifikaten inte valideras korrekt.\"],\"LEwpeL\":[\"soju-bouncer (kontroll)\"],\"LNfLR5\":[\"Visa utsparkningar\"],\"LP+1Z7\":[\"Lägg till nätverk\"],\"LQb0W/\":[\"Visa alla händelser\"],\"LU7/yA\":[\"Alternativt namn för visning i gränssnittet. Får innehålla mellanslag, emoji och specialtecken. Det riktiga kanalnamnet (\",[\"channelName\"],\") används fortfarande för IRC-kommandon.\"],\"LUb9O7\":[\"Giltig serverport krävs\"],\"LV4fT6\":[\"Beskrivning (valfri, t.ex. \\\"Betatestare Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Integritetspolicy\"],\"LcuSDR\":[\"Hantera din profilinformation och metadata\"],\"LqLS9B\":[\"Visa nickbyten\"],\"LsDQt2\":[\"Kanalinställningar\"],\"LtI9AS\":[\"Ägare\"],\"LuNhhL\":[\"reagerade på det här meddelandet\"],\"M/AZNG\":[\"URL till din avatarbild\"],\"M/WIer\":[\"Skicka meddelande\"],\"M8er/5\":[\"Namn:\"],\"MHk+7g\":[\"Föregående bild\"],\"MRorGe\":[\"PM-användare\"],\"MVbSGP\":[\"Tidsfönster (sekunder)\"],\"MkpcsT\":[\"Dina meddelanden och inställningar lagras lokalt på din enhet\"],\"MzPdC2\":[\"Serverlösenord (PASS)\"],\"N/hDSy\":[\"Markera som bot – vanligtvis 'on' eller tomt\"],\"N6j2JH\":[\"Redigera \",[\"0\"]],\"N7TQbE\":[\"Bjud in användare till \",[\"channelName\"]],\"NCca/o\":[\"Ange standardsmeknamn...\"],\"Nqs6B9\":[\"Visar allt externt media. Valfri URL kan orsaka en begäran till en okänd server.\"],\"Nt+9O7\":[\"Använd WebSocket istället för råa TCP\"],\"NxIHzc\":[\"Koppla från användare\"],\"O+v/cL\":[\"Bläddra bland alla kanaler på servern\"],\"OCGpR4\":[\"(ärv)\"],\"ODwSCk\":[\"Skicka en GIF\"],\"OGQ5kK\":[\"Konfigurera aviseringsljud och markeringar\"],\"OIPt1Z\":[\"Visa eller dölj medlemslistans sidopanel\"],\"OKSNq/\":[\"Mycket strikt\"],\"ONWvwQ\":[\"Ladda upp\"],\"OVKoQO\":[\"Ditt kontolösenord för autentisering\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - För IRC in i framtiden\"],\"OhCpra\":[\"Ange ett ämne…\"],\"OkltoQ\":[\"Banna \",[\"username\"],\" via nick (förhindrar återanslutning med samma nick)\"],\"P+t/Te\":[\"Inga ytterligare data\"],\"P42Wcc\":[\"Säkert\"],\"PD38l0\":[\"Förhandsvisning av kanalavatar\"],\"PD9mEt\":[\"Skriv ett meddelande...\"],\"PPqfdA\":[\"Öppna kanalens konfigurationsinställningar\"],\"PSCjfZ\":[\"Ämnet som visas för den här kanalen. Alla användare kan se ämnet.\"],\"PZCecv\":[\"PDF-förhandsgranskning\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 gång\"],\"other\":[[\"c\"],\" gånger\"]}]],\"PguS2C\":[\"Lägg till undantagsmask (t.ex. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Visar \",[\"displayedChannelsCount\"],\" av \",[\"0\"],\" kanaler\"],\"PqhVlJ\":[\"Banna användare (via hostmask)\"],\"Q+chwU\":[\"Användarnamn:\"],\"Q2QY4/\":[\"Ta bort den här inbjudan\"],\"Q3v9Wc\":[\"Ja, ta bort\"],\"Q6hhn8\":[\"Inställningar\"],\"QF4a34\":[\"Ange ett användarnamn\"],\"QGqSZ2\":[\"Färg och formatering\"],\"QJQd1J\":[\"Redigera profil\"],\"QSzGDE\":[\"Inaktiv\"],\"QUlny5\":[\"Välkommen till \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Läs mer\"],\"QuSkCF\":[\"Filtrera kanaler...\"],\"QwUrDZ\":[\"ändrade ämnet till: \",[\"topic\"]],\"R0UH07\":[\"Bild \",[\"0\"],\" av \",[\"1\"]],\"R7SsBE\":[\"Tysta\"],\"R8rf1X\":[\"Klicka för att ange ämne\"],\"RArB3D\":[\"kastades ut från \",[\"channelName\"],\" av \",[\"username\"]],\"RI3cWd\":[\"Utforska IRC-världen med ObsidianIRC\"],\"RIfHS5\":[\"Skapa en ny inbjudningslänk\"],\"RMMaN5\":[\"Modererad (+m)\"],\"RWw9Lg\":[\"Stäng dialog\"],\"RZ2BuZ\":[\"Kontoregistrering för \",[\"account\"],\" kräver verifiering: \",[\"message\"]],\"RySp6q\":[\"Dölj kommentarer\"],\"S5Togi\":[\"Laddar nätverk från din bouncer…\"],\"SPKQTd\":[\"Nick krävs\"],\"SPVjfj\":[\"Standardvärdet är 'ingen anledning' om det lämnas tomt\"],\"SQKPvQ\":[\"Bjud in användare\"],\"STmlpb\":[\"Tillbaka till nätverkslistan\"],\"SkZcl+\":[\"Välj en fördefinierad flödeskyddsprofil. Dessa profiler ger balanserade skyddsinställningar för olika användningsfall.\"],\"Slr+3C\":[\"Min användare\"],\"Spnlre\":[\"Du bjöd in \",[\"target\"],\" att gå med i \",[\"channel\"]],\"T/ckN5\":[\"Öppna i visaren\"],\"T91vKp\":[\"Spela upp\"],\"TV2Wdu\":[\"Läs om hur vi hanterar dina uppgifter och skyddar din integritet.\"],\"TgFpwD\":[\"Tillämpar...\"],\"TkzSFB\":[\"Inga ändringar\"],\"TtserG\":[\"Ange riktigt namn\"],\"Ttz9J1\":[\"Ange lösenord...\"],\"Tz0i8g\":[\"Inställningar\"],\"U3pytU\":[\"Admin\"],\"UDb2YD\":[\"Reagera\"],\"UE4KO5\":[\"*kanal*\"],\"UETAwW\":[\"Du har inte skapat nÃ¥gra inbjudningslänkar än. Använd formuläret ovan för att skapa din första.\"],\"UGT5vp\":[\"Spara inställningar\"],\"UV5hLB\":[\"Inga banningar hittades\"],\"Uaj3Nd\":[\"Statusmeddelanden\"],\"Ue3uny\":[\"Standard (ingen profil)\"],\"UkARhe\":[\"Normal – standardskydd\"],\"Umn7Cj\":[\"Inga kommentarer ännu. Var den första!\"],\"UtUIRh\":[[\"0\"],\" äldre meddelanden\"],\"UwzP+U\":[\"Säker anslutning\"],\"V0/A4O\":[\"Kanalägare\"],\"V0zZWc\":[\"Detta stänger även det bundna nätverket nedan.\"],\"V4qgxE\":[\"Skapad före (min sedan)\"],\"V8yTm6\":[\"Rensa sökning\"],\"VJMMyz\":[\"ObsidianIRC - För IRC in i framtiden\"],\"VJScHU\":[\"Anledning\"],\"VLsmVV\":[\"Tysta aviseringar\"],\"VbyRUy\":[\"Kommentarer\"],\"Vmx0mQ\":[\"Satt av:\"],\"VqnIZz\":[\"Visa vår integritetspolicy och datapraxis\"],\"VrMygG\":[\"Minimal längd är \",[\"0\"]],\"VrnTui\":[\"Dina pronomen, visas i din profil\"],\"W8E3qn\":[\"Autentiserat konto\"],\"WAakm9\":[\"Ta bort kanal\"],\"WFxTHC\":[\"Lägg till banmask (t.ex. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Serveradress krävs\"],\"WRYdXW\":[\"Ljudposition\"],\"WUOH5B\":[\"Ignorera användare\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Visa 1 till\"],\"other\":[\"Visa \",[\"1\"],\" till\"]}]],\"WYxRzo\":[\"Skapa och hantera dina inbjudningslänkar\"],\"Wd38W1\":[\"Lämna kanal tomt för en allmän nätverksinbjudan. Beskrivningen är bara för dina anteckningar â syns bara för dig i den här listan.\"],\"Weq9zb\":[\"Allmänt\"],\"Wfj7Sk\":[\"Tysta eller aktivera aviseringsljud\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*skräp*\"],\"WzMCru\":[\"Användarprofil\"],\"X6S3lt\":[\"Sök inställningar, kanaler, servrar...\"],\"XEHan5\":[\"Fortsätt ändå\"],\"XI1+wb\":[\"Ogiltigt format\"],\"XIXeuC\":[\"Meddelande @\",[\"0\"]],\"XMS+k4\":[\"Starta privat meddelande\"],\"XWgxXq\":[\"Album\"],\"Xd7+IT\":[\"Lossa privatchatt\"],\"Xm/s+u\":[\"Visning\"],\"Xp2n93\":[\"Visar media från serverns betrodda filvärd. Inga begäranden görs till externa tjänster.\"],\"XvjC4F\":[\"Sparar...\"],\"Y/qryO\":[\"Inga användare hittades som matchar din sökning\"],\"YAqRpI\":[\"Kontoregistrering för \",[\"account\"],\" lyckades: \",[\"message\"]],\"YEfzvP\":[\"Skyddat ämne (+t)\"],\"YQOn6a\":[\"Dölj medlemslistan\"],\"YRCoE9\":[\"Kanaloperatör\"],\"YURQaF\":[\"Visa profil\"],\"YdBSvr\":[\"Styr medievisning och externt innehåll\"],\"Yj6U3V\":[\"Ingen central server:\"],\"YjvpGx\":[\"Pronomen\"],\"YqH4l4\":[\"Ingen nyckel\"],\"YyUPpV\":[\"Konto:\"],\"ZJSWfw\":[\"Meddelande som visas när du kopplar från servern\"],\"ZR1dJ4\":[\"Inbjudningar\"],\"ZdWg0V\":[\"Öppna i webbläsare\"],\"ZhRBbl\":[\"Sök meddelanden…\"],\"Zmcu3y\":[\"Avancerade filter\"],\"a0bHay\":[\"Koppla från <0>\",[\"0\"],\"0>?\"],\"a2/8e5\":[\"Ämne angivet efter (min sedan)\"],\"aHKcKc\":[\"Föregående sida\"],\"aJTbXX\":[\"Oper-lösenord\"],\"aQryQv\":[\"Mönstret finns redan\"],\"aW9pLN\":[\"Maximalt antal användare som tillåts i kanalen. Lämna tomt för ingen gräns.\"],\"ah4fmZ\":[\"Visar också förhandsvisningar från YouTube, Vimeo, SoundCloud och liknande kända tjänster.\"],\"aifXak\":[\"Inget media i den här kanalen\"],\"ap2zBz\":[\"Avslappnat\"],\"az8lvo\":[\"Av\"],\"azXSNo\":[\"Expandera medlemslistan\"],\"azdliB\":[\"Logga in på ett konto\"],\"b26wlF\":[\"hon/hennes\"],\"bD/+Ei\":[\"Strikt\"],\"bQ6BJn\":[\"Konfigurera detaljerade flödeskyddsregler. Varje regel anger vilken typ av aktivitet som ska övervakas och vilken åtgärd som ska vidtas när gränser överskrids.\"],\"beV7+y\":[\"Användaren får en inbjudan att gå med i \",[\"channelName\"],\".\"],\"bk84cH\":[\"Borta-meddelande\"],\"bkHdLj\":[\"Lägg till IRC-server\"],\"bmQLn5\":[\"Lägg till regel\"],\"bv4cFj\":[\"Transport\"],\"bwRvnp\":[\"Åtgärd\"],\"c8+EVZ\":[\"Verifierat konto\"],\"cGYUlD\":[\"Inga medieförhandsvisningar laddas.\"],\"cLF98o\":[\"Visa kommentarer (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Inga användare tillgängliga\"],\"cSgpoS\":[\"Fäst privatchatt\"],\"cXeEKu\":[\"Detta stänger även de \",[\"0\"],\" bundna nätverken nedan.\"],\"cde3ce\":[\"Meddelande <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Kopiera formaterad utdata\"],\"cl/A5J\":[\"Välkommen till \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Ta bort\"],\"coPLXT\":[\"Vi lagrar inte dina IRC-kommunikationer på våra servrar\"],\"crYH/6\":[\"SoundCloud-spelare\"],\"cv5DQb\":[\"ingen värd angiven\"],\"d3sis4\":[\"Lägg till server\"],\"d9aN5k\":[\"Ta bort \",[\"username\"],\" från kanalen\"],\"dEgA5A\":[\"Avbryt\"],\"dGi1We\":[\"Lossa den här privata meddelandekonversationen\"],\"dJVuyC\":[\"lämnade \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"till\"],\"dXqxlh\":[\"<0>⚠️ Säkerhetsrisk!0> Den här anslutningen kan vara sårbar för avlyssning eller man-in-the-middle-attacker.\"],\"da9Q/R\":[\"Ändrade kanallägen\"],\"dhJN3N\":[\"Visa kommentarer\"],\"dj2xTE\":[\"Stäng avisering\"],\"dpCzmC\":[\"Inställningar för flödeskydd\"],\"e9dQpT\":[\"Vill du öppna den här länken i en ny flik?\"],\"ePK91l\":[\"Redigera\"],\"eYBDuB\":[\"Ladda upp en bild eller ange en URL med valfri \",[\"size\"],\"-ersättning för dynamisk storleksanpassning\"],\"edBbee\":[\"Banna \",[\"username\"],\" via hostmask (förhindrar återanslutning från samma IP/host)\"],\"ekfzWq\":[\"Användarinställningar\"],\"elPDWs\":[\"Anpassa din IRC-klientupplevelse\"],\"eu2osY\":[\"<0>💡 Rekommendation:0> Fortsätt bara om du litar på den här servern och förstår riskerna. Undvik att dela känslig information eller lösenord via den här anslutningen.\"],\"euEhbr\":[\"Klicka för att gå med i \",[\"channel\"]],\"ez3vLd\":[\"Aktivera flerrads-inmatning\"],\"f0J5Ki\":[\"Server-till-server-kommunikation kan använda okrypterade anslutningar\"],\"f9BHJk\":[\"Varna användare\"],\"fDOLLd\":[\"Inga kanaler hittades.\"],\"ffzDkB\":[\"Anonym analys:\"],\"fq1GF9\":[\"Visa när användare kopplar från servern\"],\"gCldcN\":[\"Ändra accentfärg\"],\"gEF57C\":[\"Den här servern stöder bara en anslutningstyp\"],\"gJuLUI\":[\"Ignorera-lista\"],\"gNzMrk\":[\"Nuvarande avatar\"],\"gjPWyO\":[\"Ange smeknamn...\"],\"gz6UQ3\":[\"Maximera\"],\"h6/IMX\":[\"Lägg till ditt första nätverk\"],\"h6razj\":[\"Uteslut kanalnamns-mask\"],\"hG6jnw\":[\"Inget ämne angivet\"],\"hG89Ed\":[\"Bild\"],\"hYgDIe\":[\"Skapa\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"t.ex. 100:1440\"],\"he3ygx\":[\"Kopiera\"],\"hehnjM\":[\"Mängd\"],\"hzdLuQ\":[\"Endast användare med Voice eller högre kan tala\"],\"i0qMbr\":[\"Hem\"],\"iDNBZe\":[\"Aviseringar\"],\"iH8pgl\":[\"Tillbaka\"],\"iL9SZg\":[\"Banna användare (via nick)\"],\"iNt+3c\":[\"Tillbaka till bild\"],\"iQvi+a\":[\"Varna mig inte om låg länksäkerhet för den här servern\"],\"iSLIjg\":[\"Anslut\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Serveradress\"],\"idD8Ev\":[\"Sparat\"],\"iivqkW\":[\"Inloggad sedan\"],\"ij+Elv\":[\"Bildförhandsvisning\"],\"ilIWp7\":[\"Växla aviseringar\"],\"iuaqvB\":[\"Använd * som jokertecken. Exempel: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Banna via hostmask\"],\"jA4uoI\":[\"Ämne:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Anledning (valfritt)\"],\"jUV7CU\":[\"Ladda upp avatar\"],\"jW5Uwh\":[\"Styr hur mycket externt media som laddas. Av / Säkert / Betrodda källor / Allt innehåll.\"],\"jXzms5\":[\"Bilagealternativ\"],\"jZlrte\":[\"Färg\"],\"jfC/xh\":[\"Kontakt\"],\"jywMpv\":[\"#nytt-kanalnamn\"],\"k112DD\":[\"Ladda äldre meddelanden\"],\"k3ID0F\":[\"Filtrera medlemmar…\"],\"k65gsE\":[\"Fördjupning\"],\"k7Zgob\":[\"Avbryt anslutning\"],\"kAVx5h\":[\"Inga inbjudningar hittades\"],\"kCLEPU\":[\"Ansluten till\"],\"kF5LKb\":[\"Ignorerade mönster:\"],\"kGeOx/\":[\"Gå med i \",[\"0\"]],\"kITKr8\":[\"Laddar kanallägen...\"],\"kPpPsw\":[\"Du är en IRC-operatör\"],\"kWJmRL\":[\"Du\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"Kopiera JSON\"],\"krViRy\":[\"Klicka för att kopiera som JSON\"],\"ks71ra\":[\"Undantag\"],\"kw4lRv\":[\"Kanal-halvoperatör\"],\"kxgIRq\":[\"Välj eller lägg till en kanal för att komma igång.\"],\"ky6dWe\":[\"Förhandsvisning av avatar\"],\"l+GxCv\":[\"Laddar kanaler...\"],\"l+IUVW\":[\"Kontoverifiering för \",[\"account\"],\" lyckades: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"återanslöt\"],\"other\":[\"återanslöt \",[\"reconnectCount\"],\" gånger\"]}]],\"l1l8sj\":[[\"0\"],\"d sedan\"],\"l5NhnV\":[\"#kanal (valfritt)\"],\"l5jmzx\":[[\"0\"],\" och \",[\"1\"],\" skriver...\"],\"lCF0wC\":[\"Uppdatera\"],\"lHy8N5\":[\"Laddar fler kanaler...\"],\"lasgrr\":[\"använd\"],\"lbpf14\":[\"Gå med i \",[\"value\"]],\"lfFsZ4\":[\"Kanaler\"],\"lkNdiH\":[\"Kontonamn\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Ladda upp bild\"],\"loQxaJ\":[\"Jag är tillbaka\"],\"lvfaxv\":[\"HEM\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Lägg till\"],\"m8flAk\":[\"Förhandsvisning (ej uppladdad ännu)\"],\"mEPxTp\":[\"<0>⚠️ Var försiktig!0> Öppna bara länkar från betrodda källor. Skadliga länkar kan äventyra din säkerhet eller integritet.\"],\"mHGdhG\":[\"Serverinformation\"],\"mHS8lb\":[\"Meddelande #\",[\"0\"]],\"mMYBD9\":[\"Brett – bredare skyddsomfång\"],\"mTGsPd\":[\"Kanalämne\"],\"mU8j6O\":[\"Inga externa meddelanden (+n)\"],\"mZp8FL\":[\"Automatisk återgång till enkel rad\"],\"mdQu8G\":[\"DittNick\"],\"miSSBQ\":[\"Kommentarer (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Användaren är autentiserad\"],\"mwtcGl\":[\"Stäng kommentarer\"],\"myL0MR\":[\"Ta bort det här nätverket?\"],\"mzI/c+\":[\"Ladda ned\"],\"n3fGRk\":[\"angett av \",[\"0\"]],\"n5+j9l\":[\"t.ex. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Avslappnat – mindre aggressivt skydd\"],\"nNflMD\":[\"Lämna kanal\"],\"nPXkBi\":[\"Laddar WHOIS-data...\"],\"nQnxxF\":[\"Meddelande #\",[\"0\"],\" (Shift+Enter för ny rad)\"],\"nWMRxa\":[\"Lossa\"],\"nkC032\":[\"Ingen flödesprofil\"],\"o69z4d\":[\"Skicka ett varningsmeddelande till \",[\"username\"]],\"o9ylQi\":[\"Sök efter GIF:ar för att komma igång\"],\"oFGkER\":[\"Servermeddelanden\"],\"oOi11l\":[\"Scrolla till botten\"],\"oPYIL5\":[\"nätverk\"],\"oQEzQR\":[\"Nytt DM\"],\"oXOSPE\":[\"Online\"],\"oal760\":[\"Man-in-the-middle-attacker på serverlänkar är möjliga\"],\"oeqmmJ\":[\"Betrodda källor\"],\"optX0N\":[[\"0\"],\"h sedan\"],\"ovBPCi\":[\"Standard\"],\"p0Z69r\":[\"Mönstret kan inte vara tomt\"],\"p1KgtK\":[\"Det gick inte att läsa in ljud\"],\"p59pEv\":[\"Ytterligare detaljer\"],\"p7sRI6\":[\"Låt andra se när du skriver\"],\"pBm1od\":[\"Hemlig kanal\"],\"pNmiXx\":[\"Ditt standard-nick för alla servrar\"],\"pUUo9G\":[\"Värdnamn:\"],\"pVGPmz\":[\"Kontolösenord\"],\"peNE68\":[\"Permanent\"],\"plhHQt\":[\"Inga data\"],\"pm6+q5\":[\"Säkerhetsvarning\"],\"pn5qSs\":[\"Ytterligare information\"],\"q0cR4S\":[\"är nu känd som **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Kanalen visas inte i LIST- eller NAMES-kommandon\"],\"qLpTm/\":[\"Ta bort reaktion \",[\"emoji\"]],\"qVkGWK\":[\"Fäst\"],\"qY8wNa\":[\"Hemsida\"],\"qb0xJ7\":[\"Använd jokertecken: * matchar valfri sekvens, ? matchar valfritt enskilt tecken. Exempel: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Kanalnyckel (+k)\"],\"qtoOYG\":[\"Ingen gräns\"],\"r1W2AS\":[\"Filserverbild\"],\"rIPR2O\":[\"Ämne angivet före (min sedan)\"],\"rMMSYo\":[\"Maximal längd är \",[\"0\"]],\"rWtzQe\":[\"Nätverket splittrades och återanslöts. ✅\"],\"rYG2u6\":[\"Vänta...\"],\"rdUucN\":[\"Förhandsvisning\"],\"rjGI/Q\":[\"Integritet\"],\"rk8iDX\":[\"Laddar GIF:ar...\"],\"rn6SBY\":[\"Sluta tysta\"],\"s/UKqq\":[\"Sparkades ut från kanalen\"],\"s7oqXR\":[\"Välj ett nätverk\"],\"s8cATI\":[\"gick med i \",[\"channelName\"]],\"sCO9ue\":[\"Anslutningen till <0>\",[\"serverName\"],\"0> har följande säkerhetsproblem:\"],\"sGH11W\":[\"Server\"],\"sHI1H+\":[\"är nu känd som **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" bjöd in dig att gå med i \",[\"channel\"]],\"sUBSbK\":[\"Inga uppströmsnätverk ännu.\"],\"sby+1/\":[\"Klicka för att kopiera\"],\"sfN25C\":[\"Ditt riktiga eller fullständiga namn\"],\"sliuzR\":[\"Öppna länk\"],\"sqrO9R\":[\"Anpassade omnämnanden\"],\"sr6RdJ\":[\"Flerrad med Shift+Enter\"],\"swrCpB\":[\"Kanalen har döpts om från \",[\"oldName\"],\" till \",[\"newName\"],\" av \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Avancerat\"],\"t/YqKh\":[\"Ta bort\"],\"t47eHD\":[\"Din unika identifierare på den här servern\"],\"tAkAh0\":[\"URL med valfri \",[\"size\"],\"-ersättning för dynamisk storleksanpassning. Exempel: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Visa eller dölj kanallistans sidopanel\"],\"tfDRzk\":[\"Spara\"],\"tiBsJk\":[\"lämnade \",[\"channelName\"]],\"tt4/UD\":[\"lämnade (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Smeknamnet {nick} används redan, försöker med {newNick}\"],\"u0a8B4\":[\"Autentisera som IRC-operatör för administrativ åtkomst\"],\"u0rWFU\":[\"Skapad efter (min sedan)\"],\"u72w3t\":[\"Användare och mönster att ignorera\"],\"u7jc2L\":[\"lämnade\"],\"uAQUqI\":[\"Status\"],\"uB85T3\":[\"Sparning misslyckades: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC-servrar:\"],\"ukyW4o\":[\"Dina inbjudningslänkar\"],\"usSSr/\":[\"Zoomnivå\"],\"v7uvcf\":[\"Programvara:\"],\"vE8kb+\":[\"Använd Shift+Enter för nya rader (Enter skickar)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Inget ämne\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Språk\"],\"vaHYxN\":[\"Riktigt namn\"],\"vhjbKr\":[\"Borta\"],\"w/nogd\":[[\"0\"],\" nätverk\",[\"1\"],\" — välj ett att gå med i\"],\"w4NYox\":[[\"title\"],\" klient\"],\"w8xQRx\":[\"Ogiltigt värde\"],\"wFjjxZ\":[\"kastades ut från \",[\"channelName\"],\" av \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Inga banundantag hittades\"],\"wPrGnM\":[\"Kanaladmin\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Visa när användare går med i eller lämnar kanaler\"],\"whqZ9r\":[\"Ytterligare ord eller fraser att markera\"],\"wm7RV4\":[\"Aviseringsljud\"],\"wz/Yoq\":[\"Dina meddelanden kan avlyssnas när de vidarebefordras mellan servrar\"],\"x3+y8b\":[\"SÃ¥ här mÃ¥nga registrerade sig via den här länken\"],\"xCJdfg\":[\"Rensa\"],\"xOTzt5\":[\"just nu\"],\"xUHRTR\":[\"Autentisera automatiskt som operatör vid anslutning\"],\"xWHwwQ\":[\"Banningar\"],\"xYilR2\":[\"Media\"],\"xbi8D6\":[\"Den här servern stöder inte inbjudningslänkar (kapabiliteten<0>obby.world/invitation0>annonseras inte). Du kan fortfarande chatta normalt; den här panelen är för nätverk som drivs av obbyircd.\"],\"xceQrO\":[\"Endast säkra websockets stöds\"],\"xdtXa+\":[\"kanalnamn\"],\"xfXC7q\":[\"Textkanaler\"],\"xlCYOE\":[\"Hämtar fler meddelanden...\"],\"xlhswE\":[\"Minimalt värde är \",[\"0\"]],\"xq97Ci\":[\"Lägg till ett ord eller en fras...\"],\"xuRqRq\":[\"Klientgräns (+l)\"],\"xwF+7J\":[[\"0\"],\" skriver...\"],\"y1eoq1\":[\"Kopiera länk\"],\"yJztBY\":[\"Ta bort nätverk\"],\"yNeucF\":[\"Den här servern stöder inte utökad profilmetadata (IRCv3 METADATA-tillägget). Ytterligare fält som avatar, visningsnamn och status är inte tillgängliga.\"],\"yPlrca\":[\"Kanalavatar\"],\"yQE2r9\":[\"Laddar\"],\"ySU+JY\":[\"din@epost.se\"],\"yTX1Rt\":[\"Oper-användarnamn\"],\"yYOzWD\":[\"loggar\"],\"yfx9Re\":[\"IRC-operatörslösenord\"],\"ygCKqB\":[\"Stoppa\"],\"ymDxJx\":[\"IRC-operatörens användarnamn\"],\"yrpRsQ\":[\"Sortera efter namn\"],\"yz7wBu\":[\"Stäng\"],\"zJw+jA\":[\"anger läge: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"m sedan\"],\"zebeLu\":[\"Ange oper-användarnamn\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/sv/messages.po b/src/locales/sv/messages.po
index 22a95ef3..ea43e162 100644
--- a/src/locales/sv/messages.po
+++ b/src/locales/sv/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - För IRC in i framtiden"
msgid "— open in viewer"
msgstr "— öppna i visaren"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(ärv)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(oförändrad)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} och {1} skriver..."
msgid "{0} is typing..."
msgstr "{0} skriver..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} nätverk{1} — välj ett att gå med i"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -205,6 +221,12 @@ msgstr "Lägg till inbjudningsmask (t.ex. nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "Lägg till IRC-server"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Lägg till nätverk"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Lägg till regel"
msgid "Add Server"
msgstr "Lägg till server"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Lägg till ditt första nätverk"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Ytterligare detaljer"
@@ -377,6 +403,10 @@ msgstr "Tillbaka"
msgid "Back to image"
msgstr "Tillbaka till bild"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Tillbaka till nätverkslistan"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "Banna {username} via hostmask (förhindrar återanslutning från samma IP/host)"
@@ -424,6 +454,10 @@ msgstr "Bläddra bland alla kanaler på servern"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Avbryt anslutning"
msgid "Cancel reply"
msgstr "Avbryt svar"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Ändra accentfärg"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Ändra kanalnamnet (endast operatörer)"
@@ -564,7 +603,7 @@ msgstr "Rensa sökning"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "Klicka igen för att bekräfta"
+msgstr "Klicka igen för att bekräfta"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "Klientgräns (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -666,9 +707,10 @@ msgstr "Konfigurera aviseringsljud och markeringar"
#: src/components/ui/InvitationsPanel.tsx
msgid "Confirm?"
-msgstr "Bekräfta?"
+msgstr "Bekräfta?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Anslut"
@@ -731,7 +773,7 @@ msgstr "Kopiera JSON"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy link"
-msgstr "Kopiera länk"
+msgstr "Kopiera länk"
#: src/components/ui/ExternalLinkWarningModal.tsx
msgid "Copy URL"
@@ -743,11 +785,11 @@ msgstr "Skapa"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "Skapa en ny inbjudningslänk"
+msgstr "Skapa en ny inbjudningslänk"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "Skapa och hantera dina inbjudningslänkar"
+msgstr "Skapa och hantera dina inbjudningslänkar"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,27 +856,52 @@ msgstr "Ta bort kanal"
msgid "Delete message"
msgstr "Ta bort meddelande"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Ta bort nätverk"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Ta bort privatchatt"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "Ta bort den här inbjudan"
+msgstr "Ta bort den här inbjudan"
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "Ta bort det här meddelandet? Det kan inte ångras."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Ta bort det här nätverket?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
msgstr "Beskrivning (valfri, t.ex. \"Betatestare Q3\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Koppla från"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "Koppla från <0>{0}0>?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Koppla från soju-bouncern?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Koppla från nätverket?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Utforska"
@@ -891,20 +958,31 @@ msgstr "Ladda ned"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "Släpp filer för att ladda upp"
+msgstr "Släpp filer för att ladda upp"
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "t.ex. <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "t.ex. 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Redigera"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "Redigera {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Redigera profil"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "HEM"
msgid "Homepage"
msgstr "Hemsida"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Host"
@@ -1323,7 +1402,7 @@ msgstr "Lämna kanal"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Lämna kanal tomt för en allmän nätverksinbjudan. Beskrivningen är bara för dina anteckningar — syns bara för dig i den här listan."
+msgstr "Lämna kanal tomt för en allmän nätverksinbjudan. Beskrivningen är bara för dina anteckningar â syns bara för dig i den här listan."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "Lämnade kanalen"
msgid "Let others know when you are typing"
msgstr "Låt andra se när du skriver"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Länkförhandsvisning"
@@ -1375,6 +1458,10 @@ msgstr "Laddar GIF:ar..."
msgid "Loading more channels..."
msgstr "Laddar fler kanaler..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Laddar nätverk från din bouncer…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "Laddar WHOIS-data..."
@@ -1563,12 +1650,23 @@ msgstr "Namn:"
#: src/components/ui/InvitationsPanel.tsx
msgid "network"
-msgstr "nätverk"
+msgstr "nätverk"
+
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Nätverk bundet via soju-bouncer"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Nätverksnamn"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Nätverk på {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Nytt DM"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (t.ex. spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Ingen fil vald"
msgid "No flood profile"
msgstr "Ingen flödesprofil"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "ingen värd angiven"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Inga inbjudningar hittades"
@@ -1672,7 +1775,7 @@ msgstr "Inga medieförhandsvisningar laddas."
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "Ingen rå IRC-trafik har fångats än. Försök att ansluta eller skicka ett meddelande."
+msgstr "Ingen rå IRC-trafik har fångats än. Försök att ansluta eller skicka ett meddelande."
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "Inga resultat hittades"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "Ingen server är vald. Välj en server från sidofältet först; inbjudningslänkar hanteras per server."
+msgstr "Ingen server är vald. Välj en server från sidofältet först; inbjudningslänkar hanteras per server."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "Inget ämne angivet"
msgid "No unread mentions or messages"
msgstr "Inga olästa omnämnanden eller meddelanden"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Inga uppströmsnätverk ännu."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Inga användare tillgängliga"
@@ -1784,6 +1891,10 @@ msgstr "Hoppsan! Nätverksuppdelning! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Öppna"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Öppna kanalens konfigurationsinställningar"
@@ -1887,6 +1998,10 @@ msgstr "Fäst privatchatt"
msgid "Pin this private message conversation"
msgstr "Fäst den här privata meddelandekonversationen"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Klartext"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "PM-användare"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Port"
@@ -2006,6 +2122,7 @@ msgstr "reagerade på det här meddelandet"
msgid "Read more"
msgstr "Läs mer"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Anledning"
msgid "Reason (optional)"
msgstr "Anledning (valfritt)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Återanslut till server"
@@ -2095,6 +2213,7 @@ msgstr "Regler"
msgid "Safe"
msgstr "Säkert"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Sök position"
msgid "Select a channel"
msgstr "Välj en kanal"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Välj ett nätverk"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Välj medlem"
@@ -2276,6 +2399,10 @@ msgstr "Serveroperatörer på nätverket kan potentiellt läsa dina meddelanden"
msgid "Server Password"
msgstr "Serverlösenord"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Serverlösenord (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Server-till-server-kommunikation kan använda okrypterade anslutningar"
@@ -2380,6 +2507,12 @@ msgstr "Inloggad sedan"
msgid "Software:"
msgstr "Programvara:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "soju-bouncer (kontroll)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Sortera efter namn"
@@ -2457,7 +2590,11 @@ msgstr "Den här bilden har gått ut"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "Så här många registrerade sig via den här länken"
+msgstr "Så här många registrerade sig via den här länken"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Detta tar bort nätverket från din soju-bouncer. För att använda det igen måste du lägga till det på nytt."
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "Den här servern stöder inte utökad profilmetadata (IRCv3 METADATA-til
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Den här servern stöder inte inbjudningslänkar (kapabiliteten<0>obby.world/invitation0>annonseras inte). Du kan fortfarande chatta normalt; den här panelen är för nätverk som drivs av obbyircd."
+msgstr "Den här servern stöder inte inbjudningslänkar (kapabiliteten<0>obby.world/invitation0>annonseras inte). Du kan fortfarande chatta normalt; den här panelen är för nätverk som drivs av obbyircd."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Den här servern stöder bara en anslutningstyp"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Detta stänger även de {0} bundna nätverken nedan."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Detta stänger även det bundna nätverket nedan."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Tid (min)"
@@ -2479,6 +2625,10 @@ msgstr "Tid (min)"
msgid "Time Window (seconds)"
msgstr "Tidsfönster (sekunder)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Ämne:"
msgid "Total: {0}"
msgstr "Totalt: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Transport"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Betrodda källor"
@@ -2615,7 +2769,7 @@ msgstr "Använd jokertecken: * matchar valfri sekvens, ? matchar valfritt enskil
#: src/components/ui/InvitationsPanel.tsx
msgid "used"
-msgstr "använd"
+msgstr "använd"
#: src/components/message/JsonLogMessage.tsx
#: src/components/ui/UserProfileModal.tsx
@@ -2641,6 +2795,7 @@ msgstr "Användarprofil"
msgid "User Settings"
msgstr "Användarinställningar"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Brett – bredare skyddsomfång"
msgid "Will default to 'no reason' if left empty"
msgstr "Standardvärdet är 'ingen anledning' om det lämnas tomt"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Ja, ta bort"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "Du har osparade ändringar. Är du säker på att du vill stänga utan a
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "Du har inte skapat några inbjudningslänkar än. Använd formuläret ovan för att skapa din första."
+msgstr "Du har inte skapat några inbjudningslänkar än. Använd formuläret ovan för att skapa din första."
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "Du bjöd in {target} att gå med i {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Du är ansluten till <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Ditt kontolösenord för autentisering"
@@ -2822,13 +2986,17 @@ msgstr "Ditt kontolösenord för autentisering"
msgid "Your account username for authentication"
msgstr "Ditt kontoanvändarnamn för autentisering"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Din bouncer har inga nätverk ännu. Lägg till ett för att komma igång."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Ditt standard-nick för alla servrar"
#: src/components/ui/InvitationsPanel.tsx
msgid "Your invite links"
-msgstr "Dina inbjudningslänkar"
+msgstr "Dina inbjudningslänkar"
#: src/components/ui/UserSettings.tsx
msgid "Your messages and settings are stored locally on your device"
diff --git a/src/locales/tr/messages.mjs b/src/locales/tr/messages.mjs
index 99836297..e66ea714 100644
--- a/src/locales/tr/messages.mjs
+++ b/src/locales/tr/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Geçersiz desen biçimi. nick!user@host biçimini kullanın (joker karakter * kullanılabilir)\"],\"+6NQQA\":[\"Genel Destek Kanalı\"],\"+6NyRG\":[\"İstemci\"],\"+K0AvT\":[\"Bağlantıyı Kes\"],\"+cyFdH\":[\"Uzakta olarak işaretlendiğinde gösterilecek varsayılan mesaj\"],\"+mVPqU\":[\"Mesajlarda markdown biçimlendirmesini işle\"],\"+vqCJH\":[\"Kimlik doğrulama için hesap kullanıcı adınız\"],\"+yPBXI\":[\"Dosya seç\"],\"+zy2Nq\":[\"Tür\"],\"/09cao\":[\"Düşük Bağlantı Güvenliği (Seviye \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Kanal dışındaki kullanıcılar kanala mesaj gönderemez\"],\"/4C8U0\":[\"Tümünü kopyala\"],\"/6BzZF\":[\"Üye Listesini Aç/Kapat\"],\"/AkXyp\":[\"Onaylıyor musunuz?\"],\"/TNOPk\":[\"Kullanıcı uzakta\"],\"/XQgft\":[\"Keşfet\"],\"/cF7Rs\":[\"Ses Seviyesi\"],\"/dqduX\":[\"Sonraki sayfa\"],\"/fc3q4\":[\"Tüm İçerik\"],\"/kISDh\":[\"Bildirim Seslerini Etkinleştir\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Ses\"],\"/rfkZe\":[\"Bahisler ve mesajlar için ses çal\"],\"0/0ZGA\":[\"Kanal Adı Maskesi\"],\"0D6j7U\":[\"Özel kurallar hakkında daha fazla bilgi →\"],\"0XsHcR\":[\"Kullanıcıyı At\"],\"0ZpE//\":[\"Kullanıcıya Göre Sırala\"],\"0bEPwz\":[\"Uzakta Olarak İşaretle\"],\"0dGkPt\":[\"Kanal listesini genişlet\"],\"0gS7M5\":[\"Görünen Ad\"],\"0kS+M8\":[\"ÖrnekAĞ\"],\"0rgoY7\":[\"Yalnızca seçtiğiniz sunuculara bağlanın\"],\"0wdd7X\":[\"Katıl\"],\"0wkVYx\":[\"Özel Mesajlar\"],\"111uHX\":[\"Bağlantı önizlemesi\"],\"196EG4\":[\"Özel Sohbeti Sil\"],\"1DSr1i\":[\"Hesap kaydı oluştur\"],\"1O/24y\":[\"Kanal Listesini Aç/Kapat\"],\"1VPJJ2\":[\"Harici Bağlantı Uyarısı\"],\"1ZC/dv\":[\"Okunmamış bahis veya mesaj yok\"],\"1pO1zi\":[\"Sunucu adı gereklidir\"],\"1uwfzQ\":[\"Kanal Konusunu Görüntüle\"],\"268g7c\":[\"Görünen adı girin\"],\"2F9+AZ\":[\"Henüz ham IRC trafiği yakalanmadı. Bağlanmayı veya bir mesaj göndermeyi deneyin.\"],\"2FOFq1\":[\"Ağdaki sunucu operatörleri mesajlarınızı okuyabilir\"],\"2FYpfJ\":[\"Daha fazla\"],\"2HF1Y2\":[[\"inviter\"],\", \",[\"target\"],\" kişisini \",[\"channel\"],\" kanalına davet etti\"],\"2I70QL\":[\"Kullanıcı profil bilgilerini görüntüle\"],\"2QYdmE\":[\"Kullanıcılar:\"],\"2QpEjG\":[\"ayrıldı\"],\"2YE223\":[\"#\",[\"0\"],\" kanalına mesaj (yeni satır için Enter, göndermek için Shift+Enter)\"],\"2bimFY\":[\"Sunucu şifresini kullan\"],\"2iTmdZ\":[\"Yerel Depolama:\"],\"2odkwe\":[\"Katı - Daha agresif koruma\"],\"2uDhbA\":[\"Davet edilecek kullanıcı adını girin\"],\"2ygf/L\":[\"← Geri\"],\"2zEgxj\":[\"GIF ara...\"],\"3RdPhl\":[\"Kanalı Yeniden Adlandır\"],\"3THokf\":[\"Sesli Kullanıcı\"],\"3TSz9S\":[\"Küçült\"],\"3jBDvM\":[\"Kanal Görünen Adı\"],\"3ryuFU\":[\"Uygulamayı geliştirmek için isteğe bağlı çökme raporları\"],\"3uBF/8\":[\"Görüntüleyiciyi kapat\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Hesap adını girin...\"],\"4/Rr0R\":[\"Mevcut kanala bir kullanıcı davet et\"],\"4EZrJN\":[\"Kurallar\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Flood Profili (+F)\"],\"4RZQRK\":[\"Ne yapıyorsun?\"],\"4hfTrB\":[\"Takma Ad\"],\"4n99LO\":[[\"0\"],\" kanalında zaten var\"],\"4t6vMV\":[\"Kısa mesajlar için otomatik olarak tek satıra geç\"],\"4vsHmf\":[\"Süre (dk)\"],\"5+INAX\":[\"Sizi bahseden mesajları vurgula\"],\"5R5Pv/\":[\"Oper Adı\"],\"678PKt\":[\"Ağ Adı\"],\"6Aih4U\":[\"Çevrimdışı\"],\"6CO3WE\":[\"Kanala katılmak için şifre gerekli. Anahtarı kaldırmak için boş bırakın.\"],\"6HhMs3\":[\"Ayrılma Mesajı\"],\"6V3Ea3\":[\"Kopyalandı\"],\"6lGV3K\":[\"Daha az göster\"],\"6yFOEi\":[\"Oper şifresini girin...\"],\"7+IHTZ\":[\"Dosya seçilmedi\"],\"73hrRi\":[\"nick!user@host (örn. spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Özel mesaj gönder\"],\"7U1W7c\":[\"Çok Rahat\"],\"7Y1YQj\":[\"Gerçek ad:\"],\"7YHArF\":[\"— görüntüleyicide aç\"],\"7fjnVl\":[\"Kullanıcı ara...\"],\"7jL88x\":[\"Bu mesaj silinsin mi? Bu işlem geri alınamaz.\"],\"7nGhhM\":[\"Aklınızda ne var?\"],\"7sEpu1\":[\"Üyeler — \",[\"0\"]],\"7sNhEz\":[\"Kullanıcı Adı\"],\"8H0Q+x\":[\"Profiller hakkında daha fazla bilgi →\"],\"8Phu0A\":[\"Kullanıcılar takma adını değiştirdiğinde göster\"],\"8XTG9e\":[\"Oper şifresini girin\"],\"8XsV2J\":[\"Yeniden gönder\"],\"8ZsakT\":[\"Şifre\"],\"8kR84m\":[\"Harici bir bağlantı açmak üzeresiniz:\"],\"8lCgih\":[\"Kuralı Kaldır\"],\"8o3dPc\":[\"Yüklemek için dosyaları bırakın\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"katıldı\"],\"other\":[[\"joinCount\"],\" kez katıldı\"]}]],\"9BMLnJ\":[\"Sunucuya yeniden bağlan\"],\"9OEgyT\":[\"Tepki ekle\"],\"9PQ8m2\":[\"G-Line (global yasak)\"],\"9Qs99X\":[\"E-posta:\"],\"9QupBP\":[\"Deseni kaldır\"],\"9bG48P\":[\"Gönderiliyor\"],\"9f5f0u\":[\"Gizlilik hakkında sorularınız mı var? Bize ulaşın:\"],\"9unqs3\":[\"Uzakta:\"],\"9v3hwv\":[\"Sunucu bulunamadı.\"],\"9zb2WA\":[\"Bağlanıyor\"],\"A1taO8\":[\"Ara\"],\"A2adVi\":[\"Yazıyor Bildirimleri Gönder\"],\"A9Rhec\":[\"Kanal Adı\"],\"AWOSPo\":[\"Yakınlaştır\"],\"AXSpEQ\":[\"Bağlanırken Oper Ol\"],\"AeXO77\":[\"Hesap\"],\"AhNP40\":[\"Konuma git\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Takma ad değiştirildi\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Yanıtı iptal et\"],\"ApSx0O\":[\"\\\"\",[\"searchQuery\"],\"\\\" ile eşleşen \",[\"0\"],\" mesaj bulundu\"],\"AxPAXW\":[\"Sonuç bulunamadı\"],\"AyNqAB\":[\"Tüm sunucu olaylarını sohbette göster\"],\"B/QqGw\":[\"Klavyeden uzakta\"],\"B8AaMI\":[\"Bu alan zorunludur\"],\"BA2c49\":[\"Sunucu gelişmiş LIST filtrelemesini desteklemiyor\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" ve \",[\"3\"],\" kişi daha yazıyor...\"],\"BGul2A\":[\"Kaydedilmemiş değişiklikleriniz var. Kaydetmeden kapatmak istediğinizden emin misiniz?\"],\"BIf9fi\":[\"Durum mesajınız\"],\"BPm98R\":[\"Hiçbir sunucu seçilmedi. Önce kenar çubuğundan bir sunucu seçin; davet bağlantıları sunucu başına yönetilir.\"],\"BZz3md\":[\"Kişisel web siteniz\"],\"Bgm/H7\":[\"Çok satırlı metin girişine izin ver\"],\"BiQIl1\":[\"Bu özel mesaj konuşmasını sabitle\"],\"BlNZZ2\":[\"Mesaja gitmek için tıklayın\"],\"Bowq3c\":[\"Yalnızca operatörler kanal konusunu değiştirebilir\"],\"Btozzp\":[\"Bu görüntünün süresi doldu\"],\"Bycfjm\":[\"Toplam: \",[\"0\"]],\"C6IBQc\":[\"Tüm JSON'u kopyala\"],\"C9L9wL\":[\"Veri Toplama\"],\"CDq4wC\":[\"Kullanıcıyı Yönet\"],\"CHVRxG\":[\"@\",[\"0\"],\"'a mesaj (yeni satır için Shift+Enter)\"],\"CN9zdR\":[\"Oper adı ve şifresi gereklidir\"],\"CW3sYa\":[[\"emoji\"],\" tepkisi ekle\"],\"CaAkqd\":[\"Ayrılmaları Göster\"],\"CbvaYj\":[\"Takma Adıyla Yasakla\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Bir kanal seçin\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"Sistem\"],\"D28t6+\":[\"katıldı ve ayrıldı\"],\"DB8zMK\":[\"Uygula\"],\"DBcWHr\":[\"Özel bildirim sesi dosyası\"],\"DTy9Xw\":[\"Medya Önizlemeleri\"],\"Dj4pSr\":[\"Güvenli bir şifre seçin\"],\"Du+zn+\":[\"Aranıyor...\"],\"Du2T2f\":[\"Ayar bulunamadı\"],\"DwsSVQ\":[\"Filtreleri Uygula ve Yenile\"],\"E3W/zd\":[\"Varsayılan Takma Ad\"],\"E6nRW7\":[\"URL'yi Kopyala\"],\"E703RG\":[\"Modlar:\"],\"EAeu1Z\":[\"Davet Gönder\"],\"EFKJQT\":[\"Ayar\"],\"EGPQBv\":[\"Özel Flood Kuralları (+f)\"],\"ELik0r\":[\"Tam Gizlilik Politikasını Görüntüle\"],\"EPbeC2\":[\"Kanal konusunu görüntüle veya düzenle\"],\"EQCDNT\":[\"Oper kullanıcı adını girin...\"],\"EUvulZ\":[\"\\\"\",[\"searchQuery\"],\"\\\" ile eşleşen 1 mesaj bulundu\"],\"EatZYJ\":[\"Sonraki görüntü\"],\"EdQY6l\":[\"Yok\"],\"EnqLYU\":[\"Sunucu ara...\"],\"F0OKMc\":[\"Sunucuyu Düzenle\"],\"F6Int2\":[\"Vurgulamayı Etkinleştir\"],\"FDoLyE\":[\"Maks. Kullanıcı\"],\"FUU/hZ\":[\"Sohbette ne kadar harici medyanın yükleneceğini denetleyin.\"],\"Fdp03t\":[\"açık\"],\"FfPWR0\":[\"Pencere\"],\"FjkaiT\":[\"Uzaklaştır\"],\"FlqOE9\":[\"Bu ne anlama geliyor:\"],\"FolHNl\":[\"Hesabınızı ve kimlik doğrulamayı yönetin\"],\"Fp2Dif\":[\"Sunucudan ayrıldı\"],\"G5KmCc\":[\"GZ-Line (global Z-Line)\"],\"GDs0lz\":[\"<0>Risk:0> Hassas bilgiler (mesajlar, özel konuşmalar, kimlik doğrulama bilgileri) ağ yöneticilerine veya IRC sunucuları arasına konumlanmış saldırganlara açık olabilir.\"],\"GR+2I3\":[\"Davet maskesi ekle (örn. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Açılır sunucu bildirimlerini kapat\"],\"GdhD7H\":[\"Onaylamak için tekrar tıklayın\"],\"GlHnXw\":[\"Takma ad değişikliği başarısız: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Önizleme:\"],\"GtmO8/\":[\"kimden\"],\"GtuHUQ\":[\"Bu kanalı sunucuda yeniden adlandır. Tüm kullanıcılar yeni adı görecek.\"],\"GuGfFX\":[\"Aramayı aç/kapat\"],\"GxkJXS\":[\"Yükleniyor...\"],\"GzbwnK\":[\"Kanala katıldı\"],\"GzsUDB\":[\"Genişletilmiş Profil\"],\"H/PnT8\":[\"Emoji ekle\"],\"H6Izzl\":[\"Tercih ettiğiniz renk kodu\"],\"H9jIv+\":[\"Katılma/Ayrılma Göster\"],\"HAKBY9\":[\"Dosyaları yükle\"],\"HdE1If\":[\"Kanal\"],\"Hk4AW9\":[\"Tercih ettiğiniz görünen ad\"],\"HmHDk7\":[\"Üye Seç\"],\"HrQzPU\":[[\"networkName\"],\" üzerindeki kanallar\"],\"I2tXQ5\":[\"@\",[\"0\"],\"'a mesaj (yeni satır için Enter, göndermek için Shift+Enter)\"],\"I6bw/h\":[\"Kullanıcıyı Yasakla\"],\"I92Z+b\":[\"Bildirimleri etkinleştir\"],\"I9D72S\":[\"Bu mesajı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.\"],\"IA+1wo\":[\"Kullanıcılar kanaldan atıldığında göster\"],\"IDwkJx\":[\"IRC Operatörü\"],\"ILlU+s\":[\"Bilgi:\"],\"IUwGEM\":[\"Değişiklikleri Kaydet\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" ve \",[\"2\"],\" yazıyor...\"],\"IgrLD/\":[\"Duraklat\"],\"Im6JED\":[\"FISISALTI\"],\"ImOQa9\":[\"Yanıtla\"],\"IoHMnl\":[\"Maksimum değer \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Bağlanıyor...\"],\"J5T9NW\":[\"Kullanıcı Bilgileri\"],\"J8Y5+z\":[\"Eyvah! Ağ bölündü! ⚠️\"],\"JBHkBA\":[\"Kanaldan ayrıldı\"],\"JCwL0Q\":[\"Neden girin (isteğe bağlı)\"],\"JFciKP\":[\"Değiştir\"],\"JXGkhG\":[\"Kanal adını değiştir (yalnızca operatörler)\"],\"JcD7qf\":[\"Daha fazla işlem\"],\"JdkA+c\":[\"Gizli (+s)\"],\"Jmu12l\":[\"Sunucu Kanalları\"],\"JvQ++s\":[\"Markdown'ı Etkinleştir\"],\"K2jwh/\":[\"WHOIS verisi mevcut değil\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Mesajı sil\"],\"KKBlUU\":[\"Gömülü\"],\"KM0pLb\":[\"Kanala hoş geldiniz!\"],\"KR6W2h\":[\"Kullanıcının Engelini Kaldır\"],\"KV+Bi1\":[\"Yalnızca Davetli (+i)\"],\"KdCtwE\":[\"Sayaçları sıfırlamadan önce flood etkinliğinin kaç saniye izleneceği\"],\"Kkezga\":[\"Sunucu Şifresi\"],\"KsiQ/8\":[\"Kullanıcıların kanala katılmak için davet edilmesi gerekir\"],\"L+gB/D\":[\"Kanal bilgisi\"],\"LC1a7n\":[\"IRC sunucusu, sunucular arası bağlantılarının düşük güvenlik seviyesine sahip olduğunu bildirdi. Bu, mesajlarınız ağdaki IRC sunucuları arasında iletilirken düzgün şifrelenmeyebileceği veya SSL/TLS sertifikalarının doğru şekilde doğrulanmayabileceği anlamına gelir.\"],\"LNfLR5\":[\"Atmaları Göster\"],\"LQb0W/\":[\"Tüm Olayları Göster\"],\"LU7/yA\":[\"Arayüzde gösterilecek alternatif ad. Boşluk, emoji ve özel karakter içerebilir. IRC komutlarında gerçek kanal adı (\",[\"channelName\"],\") kullanılmaya devam eder.\"],\"LUb9O7\":[\"Geçerli bir sunucu portu gereklidir\"],\"LV4fT6\":[\"Açıklama (isteğe bağlı, örn. \\\"Beta test ekibi Ç3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Gizlilik Politikası\"],\"LcuSDR\":[\"Profil bilgilerinizi ve meta verilerinizi yönetin\"],\"LqLS9B\":[\"Takma Ad Değişikliklerini Göster\"],\"LsDQt2\":[\"Kanal Ayarları\"],\"LtI9AS\":[\"Sahip\"],\"LuNhhL\":[\"bu mesaja tepki verdi\"],\"M/AZNG\":[\"Avatar görüntünüzün URL'si\"],\"M/WIer\":[\"Mesaj Gönder\"],\"M8er/5\":[\"Ad:\"],\"MHk+7g\":[\"Önceki görüntü\"],\"MRorGe\":[\"Kullanıcıya PM Gönder\"],\"MVbSGP\":[\"Zaman Penceresi (saniye)\"],\"MkpcsT\":[\"Mesajlarınız ve ayarlarınız cihazınızda yerel olarak saklanır\"],\"N/hDSy\":[\"Bot olarak işaretle - genellikle 'on' veya boş\"],\"N7TQbE\":[[\"channelName\"],\" kanalına Kullanıcı Davet Et\"],\"NCca/o\":[\"Varsayılan takma adı girin...\"],\"Nqs6B9\":[\"Tüm harici medyayı gösterir. Herhangi bir URL bilinmeyen bir sunucuya istek gönderebilir.\"],\"Nt+9O7\":[\"Ham TCP yerine WebSocket kullan\"],\"NxIHzc\":[\"Kullanıcıyı at\"],\"O+v/cL\":[\"Sunucudaki tüm kanalları görüntüle\"],\"ODwSCk\":[\"GIF gönder\"],\"OGQ5kK\":[\"Bildirim seslerini ve vurguları yapılandır\"],\"OIPt1Z\":[\"Üye listesi kenar çubuğunu göster veya gizle\"],\"OKSNq/\":[\"Çok Katı\"],\"ONWvwQ\":[\"Yükle\"],\"OVKoQO\":[\"Kimlik doğrulama için hesap şifreniz\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - IRC'yi geleceğe taşıyor\"],\"OhCpra\":[\"Konu belirle…\"],\"OkltoQ\":[[\"username\"],\" kullanıcısını takma adıyla yasakla (aynı takma adla yeniden katılmasını engeller)\"],\"P+t/Te\":[\"Ek veri yok\"],\"P42Wcc\":[\"Güvenli\"],\"PD38l0\":[\"Kanal avatarı önizlemesi\"],\"PD9mEt\":[\"Mesaj yazın...\"],\"PPqfdA\":[\"Kanal yapılandırma ayarlarını aç\"],\"PSCjfZ\":[\"Bu kanal için görüntülenecek konu. Tüm kullanıcılar konuyu görebilir.\"],\"PZCecv\":[\"PDF önizleme\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 kez\"],\"other\":[[\"c\"],\" kez\"]}]],\"PguS2C\":[\"İstisna maskesi ekle (örn. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[[\"0\"],\" kanaldan \",[\"displayedChannelsCount\"],\" tanesi gösteriliyor\"],\"PqhVlJ\":[\"Kullanıcıyı Yasakla (Host Maskesiyle)\"],\"Q+chwU\":[\"Kullanıcı adı:\"],\"Q2QY4/\":[\"Bu daveti sil\"],\"Q6hhn8\":[\"Tercihler\"],\"QF4a34\":[\"Lütfen bir kullanıcı adı girin\"],\"QGqSZ2\":[\"Renk ve Biçimlendirme\"],\"QJQd1J\":[\"Profili Düzenle\"],\"QSzGDE\":[\"Boşta\"],\"QUlny5\":[[\"0\"],\"'a hoş geldiniz!\"],\"Qoq+GP\":[\"Devamını oku\"],\"QuSkCF\":[\"Kanalları filtrele...\"],\"QwUrDZ\":[\"konuyu şu şekilde değiştirdi: \",[\"topic\"]],\"R0UH07\":[[\"1\"],\" görselinden \",[\"0\"],\". görsel\"],\"R7SsBE\":[\"Sessize Al\"],\"R8rf1X\":[\"Konu belirlemek için tıklayın\"],\"RArB3D\":[[\"channelName\"],\" kanalından \",[\"username\"],\" tarafından atıldı\"],\"RI3cWd\":[\"ObsidianIRC ile IRC dünyasını keşfedin\"],\"RIfHS5\":[\"Yeni bir davet bağlantısı oluştur\"],\"RMMaN5\":[\"Moderasyonlu (+m)\"],\"RWw9Lg\":[\"Pencereyi kapat\"],\"RZ2BuZ\":[[\"account\"],\" hesap kaydı doğrulama gerektiriyor: \",[\"message\"]],\"RySp6q\":[\"Yorumları gizle\"],\"SPKQTd\":[\"Takma ad gereklidir\"],\"SPVjfj\":[\"Boş bırakılırsa varsayılan olarak 'neden yok' kullanılır\"],\"SQKPvQ\":[\"Kullanıcı Davet Et\"],\"SkZcl+\":[\"Önceden tanımlanmış bir flood koruma profili seçin. Bu profiller, farklı kullanım durumları için dengeli koruma ayarları sunar.\"],\"Slr+3C\":[\"Min. Kullanıcı\"],\"Spnlre\":[[\"target\"],\" kişisini \",[\"channel\"],\" kanalına davet ettiniz\"],\"T/ckN5\":[\"Görüntüleyicide aç\"],\"T91vKp\":[\"Oynat\"],\"TV2Wdu\":[\"Verilerinizi nasıl işlediğimizi ve gizliliğinizi nasıl koruduğumuzu öğrenin.\"],\"TgFpwD\":[\"Uygulanıyor...\"],\"TkzSFB\":[\"Değişiklik Yok\"],\"TtserG\":[\"Gerçek adı girin\"],\"Ttz9J1\":[\"Şifreyi girin...\"],\"Tz0i8g\":[\"Ayarlar\"],\"U3pytU\":[\"Yönetici\"],\"UDb2YD\":[\"Tepki Ver\"],\"UE4KO5\":[\"*kanal*\"],\"UETAwW\":[\"Henüz hiç davet bağlantısı oluşturmadınız. İlkini oluşturmak için yukarıdaki formu kullanın.\"],\"UGT5vp\":[\"Ayarları Kaydet\"],\"UV5hLB\":[\"Yasak bulunamadı\"],\"Uaj3Nd\":[\"Durum Mesajları\"],\"Ue3uny\":[\"Varsayılan (profil yok)\"],\"UkARhe\":[\"Normal - Standart koruma\"],\"Umn7Cj\":[\"Henüz yorum yok. İlk sen ol!\"],\"UtUIRh\":[[\"0\"],\" eski mesaj\"],\"UwzP+U\":[\"Güvenli Bağlantı\"],\"V0/A4O\":[\"Kanal Sahibi\"],\"V4qgxE\":[\"Şu kadar dakika önce önce oluşturulan\"],\"V8yTm6\":[\"Aramayı temizle\"],\"VJMMyz\":[\"ObsidianIRC - IRC'yi geleceğe taşıyor\"],\"VJScHU\":[\"Neden\"],\"VLsmVV\":[\"Bildirimleri sessize al\"],\"VbyRUy\":[\"Yorumlar\"],\"Vmx0mQ\":[\"Ayarlayan:\"],\"VqnIZz\":[\"Gizlilik politikamızı ve veri uygulamalarımızı görüntüleyin\"],\"VrMygG\":[\"Minimum uzunluk \",[\"0\"]],\"VrnTui\":[\"Profilinizde gösterilen zamirleriniz\"],\"W8E3qn\":[\"Doğrulanmış Hesap\"],\"WAakm9\":[\"Kanalı Sil\"],\"WFxTHC\":[\"Yasaklama maskesi ekle (örn. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Sunucu hostu gereklidir\"],\"WRYdXW\":[\"Ses konumu\"],\"WUOH5B\":[\"Kullanıcıyı Engelle\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"1 öğe daha göster\"],\"other\":[[\"1\"],\" öğe daha göster\"]}]],\"WYxRzo\":[\"Davet bağlantılarınızı oluşturun ve yönetin\"],\"Wd38W1\":[\"Genel bir ağ daveti için kanal alanını boş bırakın. Açıklama yalnızca sizin kayıtlarınız içindir — bu listede yalnızca sizin görebileceğiniz şekilde gösterilir.\"],\"Weq9zb\":[\"Genel\"],\"Wfj7Sk\":[\"Bildirim seslerini sessize al veya aç\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Kullanıcı Profili\"],\"X6S3lt\":[\"Ayarlar, kanallar, sunucular arayın...\"],\"XEHan5\":[\"Yine de Devam Et\"],\"XI1+wb\":[\"Geçersiz biçim\"],\"XIXeuC\":[\"@\",[\"0\"],\"'a mesaj\"],\"XMS+k4\":[\"Özel Mesaj Başlat\"],\"XWgxXq\":[\"Albüm\"],\"Xd7+IT\":[\"Özel Sohbetin Sabitlemesini Kaldır\"],\"Xm/s+u\":[\"Görünüm\"],\"Xp2n93\":[\"Sunucunuzun güvenilir dosya hostundan medya gösterir. Harici hizmetlere istek gönderilmez.\"],\"XvjC4F\":[\"Kaydediliyor...\"],\"Y/qryO\":[\"Aramanızla eşleşen kullanıcı bulunamadı\"],\"YAqRpI\":[[\"account\"],\" hesap kaydı başarılı: \",[\"message\"]],\"YEfzvP\":[\"Korumalı Konu (+t)\"],\"YQOn6a\":[\"Üye listesini daralt\"],\"YRCoE9\":[\"Kanal Operatörü\"],\"YURQaF\":[\"Profili Görüntüle\"],\"YdBSvr\":[\"Medya gösterimini ve harici içeriği denetleyin\"],\"Yj6U3V\":[\"Merkezi Sunucu Yok:\"],\"YjvpGx\":[\"Zamirler\"],\"YqH4l4\":[\"Anahtar yok\"],\"YyUPpV\":[\"Hesap:\"],\"ZJSWfw\":[\"Sunucudan ayrıldığınızda gösterilen mesaj\"],\"ZR1dJ4\":[\"Davetler\"],\"ZdWg0V\":[\"Tarayıcıda aç\"],\"ZhRBbl\":[\"Mesajlarda ara…\"],\"Zmcu3y\":[\"Gelişmiş Filtreler\"],\"a2/8e5\":[\"Şu kadar dakika önce sonra konu belirlenen\"],\"aHKcKc\":[\"Önceki sayfa\"],\"aJTbXX\":[\"Oper Şifresi\"],\"aQryQv\":[\"Desen zaten mevcut\"],\"aW9pLN\":[\"Kanalda izin verilen maksimum kullanıcı sayısı. Sınır olmaması için boş bırakın.\"],\"ah4fmZ\":[\"YouTube, Vimeo, SoundCloud ve benzeri bilinen hizmetlerden önizlemeler de gösterir.\"],\"aifXak\":[\"Bu kanalda medya yok\"],\"ap2zBz\":[\"Rahat\"],\"az8lvo\":[\"Kapalı\"],\"azXSNo\":[\"Üye listesini genişlet\"],\"azdliB\":[\"Bir hesaba giriş yap\"],\"b26wlF\":[\"o/onun\"],\"bD/+Ei\":[\"Katı\"],\"bQ6BJn\":[\"Ayrıntılı flood koruma kurallarını yapılandırın. Her kural, hangi etkinlik türünün izleneceğini ve eşikler aşıldığında hangi işlemin yapılacağını belirtir.\"],\"beV7+y\":[\"Kullanıcı \",[\"channelName\"],\" kanalına katılmak için davet alacak.\"],\"bk84cH\":[\"Uzakta Mesajı\"],\"bkHdLj\":[\"IRC Sunucusu Ekle\"],\"bmQLn5\":[\"Kural Ekle\"],\"bwRvnp\":[\"İşlem\"],\"c8+EVZ\":[\"Doğrulanmış hesap\"],\"cGYUlD\":[\"Medya önizlemesi yüklenmiyor.\"],\"cLF98o\":[\"Yorumları göster (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Kullanılabilir kullanıcı yok\"],\"cSgpoS\":[\"Özel Sohbeti Sabitle\"],\"cde3ce\":[\"<0>\",[\"0\"],\"0>'a mesaj\"],\"chQsxg\":[\"Biçimlendirilmiş çıktıyı kopyala\"],\"cl/A5J\":[[\"__DEFAULT_IRC_SERVER_NAME__\"],\"'a hoş geldiniz!\"],\"cnGeoo\":[\"Sil\"],\"coPLXT\":[\"IRC iletişimlerinizi sunucularımızda saklamıyoruz\"],\"crYH/6\":[\"SoundCloud oynatıcı\"],\"d3sis4\":[\"Sunucu Ekle\"],\"d9aN5k\":[[\"username\"],\" kullanıcısını kanaldan kaldır\"],\"dEgA5A\":[\"İptal\"],\"dGi1We\":[\"Bu özel mesaj konuşmasının sabitlemesini kaldır\"],\"dJVuyC\":[[\"channelName\"],\" kanalından ayrıldı (\",[\"reason\"],\")\"],\"dMtLDE\":[\"kime\"],\"dXqxlh\":[\"<0>⚠️ Güvenlik Riski!0> Bu bağlantı, araya girme veya ortadaki adam saldırılarına karşı savunmasız olabilir.\"],\"da9Q/R\":[\"Kanal modları değiştirildi\"],\"dhJN3N\":[\"Yorumları göster\"],\"dj2xTE\":[\"Bildirimi kapat\"],\"dpCzmC\":[\"Flood Koruma Ayarları\"],\"e9dQpT\":[\"Bu bağlantıyı yeni sekmede açmak istiyor musunuz?\"],\"ePK91l\":[\"Düzenle\"],\"eYBDuB\":[\"Bir görüntü yükleyin veya dinamik boyutlandırma için isteğe bağlı \",[\"size\"],\" değişkeni içeren bir URL sağlayın\"],\"edBbee\":[[\"username\"],\" kullanıcısını host maskesiyle yasakla (aynı IP/host'tan yeniden katılmasını engeller)\"],\"ekfzWq\":[\"Kullanıcı Ayarları\"],\"elPDWs\":[\"IRC istemci deneyiminizi özelleştirin\"],\"eu2osY\":[\"<0>💡 Öneri:0> Yalnızca bu sunucuya güveniyorsanız ve risklerin farkındaysanız devam edin. Bu bağlantı üzerinden hassas bilgi veya şifre paylaşmaktan kaçının.\"],\"euEhbr\":[[\"channel\"],\" kanalına katılmak için tıklayın\"],\"ez3vLd\":[\"Çok Satırlı Girişi Etkinleştir\"],\"f0J5Ki\":[\"Sunucular arası iletişim şifrelenmemiş bağlantılar kullanıyor olabilir\"],\"f9BHJk\":[\"Kullanıcıyı Uyar\"],\"fDOLLd\":[\"Kanal bulunamadı.\"],\"ffzDkB\":[\"Anonim Analitik:\"],\"fq1GF9\":[\"Kullanıcılar sunucudan ayrıldığında göster\"],\"gEF57C\":[\"Bu sunucu yalnızca bir bağlantı türünü destekliyor\"],\"gJuLUI\":[\"Engelleme Listesi\"],\"gNzMrk\":[\"Mevcut avatar\"],\"gjPWyO\":[\"Takma adı girin...\"],\"gz6UQ3\":[\"Büyüt\"],\"h6razj\":[\"Kanal Adı Maskesini Hariç Tut\"],\"hG6jnw\":[\"Konu belirlenmemiş\"],\"hG89Ed\":[\"Görüntü\"],\"hYgDIe\":[\"Oluştur\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"örn. 100:1440\"],\"he3ygx\":[\"Kopyala\"],\"hehnjM\":[\"Miktar\"],\"hzdLuQ\":[\"Yalnızca Voice veya daha yüksek yetkiye sahip kullanıcılar konuşabilir\"],\"i0qMbr\":[\"Ana Sayfa\"],\"iDNBZe\":[\"Bildirimler\"],\"iH8pgl\":[\"Geri\"],\"iL9SZg\":[\"Kullanıcıyı Yasakla (Takma Adıyla)\"],\"iNt+3c\":[\"Görüntüye geri dön\"],\"iQvi+a\":[\"Bu sunucu için düşük bağlantı güvenliği konusunda uyarma\"],\"iSLIjg\":[\"Bağlan\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Sunucu Hostu\"],\"idD8Ev\":[\"Kaydedildi\"],\"iivqkW\":[\"Giriş Yapıldı\"],\"ij+Elv\":[\"Görüntü önizlemesi\"],\"ilIWp7\":[\"Bildirimleri Aç/Kapat\"],\"iuaqvB\":[\"Joker karakter için * kullanın. Örnekler: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Host Maskesiyle Yasakla\"],\"jA4uoI\":[\"Konu:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Neden (isteğe bağlı)\"],\"jUV7CU\":[\"Avatar Yükle\"],\"jW5Uwh\":[\"Ne kadar harici medya yükleneceğini denetleyin. Kapalı / Güvenli / Güvenilir Kaynaklar / Tüm İçerik.\"],\"jXzms5\":[\"Ek seçenekleri\"],\"jZlrte\":[\"Renk\"],\"jfC/xh\":[\"İletişim\"],\"jywMpv\":[\"#yeni-kanal-adı\"],\"k112DD\":[\"Eski mesajları yükle\"],\"k3ID0F\":[\"Üyeleri filtrele…\"],\"k65gsE\":[\"Derinlemesine incele\"],\"k7Zgob\":[\"Bağlantıyı İptal Et\"],\"kAVx5h\":[\"Davet bulunamadı\"],\"kCLEPU\":[\"Bağlı Olduğu Sunucu\"],\"kF5LKb\":[\"Engellenen desenler:\"],\"kGeOx/\":[[\"0\"],\" kanalına katıl\"],\"kITKr8\":[\"Kanal modları yükleniyor...\"],\"kPpPsw\":[\"IRC Operatörüsünüz\"],\"kWJmRL\":[\"Siz\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"JSON kopyala\"],\"krViRy\":[\"JSON olarak kopyalamak için tıklayın\"],\"ks71ra\":[\"İstisnalar\"],\"kw4lRv\":[\"Kanal Yarı Operatörü\"],\"kxgIRq\":[\"Başlamak için bir kanal seçin veya ekleyin.\"],\"ky6dWe\":[\"Avatar önizlemesi\"],\"l+GxCv\":[\"Kanallar yükleniyor...\"],\"l+IUVW\":[[\"account\"],\" hesap doğrulaması başarılı: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"yeniden bağlandı\"],\"other\":[[\"reconnectCount\"],\" kez yeniden bağlandı\"]}]],\"l1l8sj\":[[\"0\"],\"g önce\"],\"l5NhnV\":[\"#kanal (isteğe bağlı)\"],\"l5jmzx\":[[\"0\"],\" ve \",[\"1\"],\" yazıyor...\"],\"lCF0wC\":[\"Yenile\"],\"lHy8N5\":[\"Daha fazla kanal yükleniyor...\"],\"lasgrr\":[\"kullanıldı\"],\"lbpf14\":[[\"value\"],\" kanalına katıl\"],\"lfFsZ4\":[\"Kanallar\"],\"lkNdiH\":[\"Hesap Adı\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Görüntü Yükle\"],\"loQxaJ\":[\"Geri Döndüm\"],\"lvfaxv\":[\"ANA SAYFA\"],\"m16xKo\":[\"Ekle\"],\"m8flAk\":[\"Önizleme (henüz yüklenmedi)\"],\"mEPxTp\":[\"<0>⚠️ Dikkatli olun!0> Yalnızca güvenilir kaynaklardan gelen bağlantıları açın. Kötü amaçlı bağlantılar güvenliğinizi veya gizliliğinizi tehlikeye atabilir.\"],\"mHGdhG\":[\"Sunucu bilgisi\"],\"mHS8lb\":[\"#\",[\"0\"],\" kanalına mesaj\"],\"mMYBD9\":[\"Geniş - Daha kapsamlı koruma alanı\"],\"mTGsPd\":[\"Kanal Konusu\"],\"mU8j6O\":[\"Harici Mesaj Yok (+n)\"],\"mZp8FL\":[\"Otomatik Tek Satıra Dön\"],\"mdQu8G\":[\"TakmaAdınız\"],\"miSSBQ\":[\"Yorumlar (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Kullanıcı kimliği doğrulandı\"],\"mwtcGl\":[\"Yorumları kapat\"],\"mzI/c+\":[\"İndir\"],\"n3fGRk\":[[\"0\"],\" tarafından ayarlandı\"],\"nE9jsU\":[\"Rahat - Daha az agresif koruma\"],\"nNflMD\":[\"Kanaldan ayrıl\"],\"nPXkBi\":[\"WHOIS verisi yükleniyor...\"],\"nQnxxF\":[\"#\",[\"0\"],\" kanalına mesaj (yeni satır için Shift+Enter)\"],\"nWMRxa\":[\"Sabitlemeyi Kaldır\"],\"nkC032\":[\"Flood profili yok\"],\"o69z4d\":[[\"username\"],\" kullanıcısına uyarı mesajı gönder\"],\"o9ylQi\":[\"Başlamak için GIF arayın\"],\"oFGkER\":[\"Sunucu Bildirimleri\"],\"oOi11l\":[\"En alta kaydır\"],\"oPYIL5\":[\"ağ\"],\"oQEzQR\":[\"Yeni DM\"],\"oXOSPE\":[\"Çevrimiçi\"],\"oal760\":[\"Sunucu bağlantılarında ortadaki adam saldırıları mümkün\"],\"oeqmmJ\":[\"Güvenilir Kaynaklar\"],\"optX0N\":[[\"0\"],\"s önce\"],\"ovBPCi\":[\"Varsayılan\"],\"p0Z69r\":[\"Desen boş olamaz\"],\"p1KgtK\":[\"Ses yüklenemedi\"],\"p59pEv\":[\"Ek ayrıntılar\"],\"p7sRI6\":[\"Yazarken diğerlerini bilgilendir\"],\"pBm1od\":[\"Gizli kanal\"],\"pNmiXx\":[\"Tüm sunucular için varsayılan takma adınız\"],\"pUUo9G\":[\"Ana makine adı:\"],\"pVGPmz\":[\"Hesap Şifresi\"],\"peNE68\":[\"Kalıcı\"],\"plhHQt\":[\"Veri yok\"],\"pm6+q5\":[\"Güvenlik Uyarısı\"],\"pn5qSs\":[\"Ek Bilgiler\"],\"q0cR4S\":[\"artık **\",[\"newNick\"],\"** olarak bilinmektedir\"],\"qFcunY\":[\"Kanal LIST veya NAMES komutlarında görünmeyecek\"],\"qLpTm/\":[[\"emoji\"],\" tepkisini kaldır\"],\"qVkGWK\":[\"Sabitle\"],\"qY8wNa\":[\"Ana Sayfa\"],\"qb0xJ7\":[\"Joker karakter kullanın: * herhangi bir diziyle eşleşir, ? herhangi bir tek karakterle eşleşir. Örnekler: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Kanal Anahtarı (+k)\"],\"qtoOYG\":[\"Sınır yok\"],\"r1W2AS\":[\"Dosya sunucu görseli\"],\"rIPR2O\":[\"Şu kadar dakika önce önce konu belirlenen\"],\"rMMSYo\":[\"Maksimum uzunluk \",[\"0\"]],\"rWtzQe\":[\"Ağ bölündü ve yeniden birleşti. ✅\"],\"rYG2u6\":[\"Lütfen bekleyin...\"],\"rdUucN\":[\"Önizleme\"],\"rjGI/Q\":[\"Gizlilik\"],\"rk8iDX\":[\"GIF'ler yükleniyor...\"],\"rn6SBY\":[\"Sesi Aç\"],\"s/UKqq\":[\"Kanaldan atıldı\"],\"s8cATI\":[[\"channelName\"],\" kanalına katıldı\"],\"sCO9ue\":[\"<0>\",[\"serverName\"],\"0> bağlantısında aşağıdaki güvenlik endişeleri bulunmaktadır:\"],\"sGH11W\":[\"Sunucu\"],\"sHI1H+\":[\"artık **\",[\"newNick\"],\"** olarak bilinmektedir\"],\"sJyV04\":[[\"inviter\"],\", sizi \",[\"channel\"],\" kanalına davet etti\"],\"sby+1/\":[\"Kopyalamak için tıklayın\"],\"sfN25C\":[\"Gerçek veya tam adınız\"],\"sliuzR\":[\"Bağlantıyı Aç\"],\"sqrO9R\":[\"Özel Bahsediler\"],\"sr6RdJ\":[\"Shift+Enter ile çok satır\"],\"swrCpB\":[\"Kanal, \",[\"user\"],\" tarafından \",[\"oldName\"],\" yerine \",[\"newName\"],\" olarak yeniden adlandırıldı\",[\"0\"]],\"sxkWRg\":[\"Gelişmiş\"],\"t/YqKh\":[\"Kaldır\"],\"t47eHD\":[\"Bu sunucudaki benzersiz tanımlayıcınız\"],\"tAkAh0\":[\"Dinamik boyutlandırma için isteğe bağlı \",[\"size\"],\" değişkeni içeren URL. Örnek: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Kanal listesi kenar çubuğunu göster veya gizle\"],\"tfDRzk\":[\"Kaydet\"],\"tiBsJk\":[[\"channelName\"],\" kanalından ayrıldı\"],\"tt4/UD\":[\"ayrıldı (\",[\"reason\"],\")\"],\"u0TcnO\":[\"{nick} takma adı zaten kullanımda, {newNick} ile yeniden deneniyor\"],\"u0a8B4\":[\"Yönetici erişimi için IRC Operatörü olarak kimlik doğrula\"],\"u0rWFU\":[\"Şu kadar dakika önce sonra oluşturulan\"],\"u72w3t\":[\"Engellenecek kullanıcılar ve desenler\"],\"u7jc2L\":[\"ayrıldı\"],\"uAQUqI\":[\"Durum\"],\"uB85T3\":[\"Kaydetme başarısız: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC Sunucuları:\"],\"ukyW4o\":[\"Davet bağlantılarınız\"],\"usSSr/\":[\"Yakınlaştırma seviyesi\"],\"v7uvcf\":[\"Yazılım:\"],\"vE8kb+\":[\"Yeni satır için Shift+Enter kullanın (Enter gönderir)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Konu yok\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Dil\"],\"vaHYxN\":[\"Gerçek Ad\"],\"vhjbKr\":[\"Uzakta\"],\"w4NYox\":[[\"title\"],\" istemcisi\"],\"w8xQRx\":[\"Geçersiz değer\"],\"wFjjxZ\":[[\"channelName\"],\" kanalından \",[\"username\"],\" tarafından atıldı (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Yasak istisnası bulunamadı\"],\"wPrGnM\":[\"Kanal Yöneticisi\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Kullanıcılar kanala katıldığında veya ayrıldığında göster\"],\"whqZ9r\":[\"Vurgulanacak ek kelimeler veya ifadeler\"],\"wm7RV4\":[\"Bildirim Sesi\"],\"wz/Yoq\":[\"Mesajlarınız sunucular arasında iletilirken ele geçirilebilir\"],\"x3+y8b\":[\"Bu bağlantı aracılığıyla kayıt olan kişi sayısı\"],\"xCJdfg\":[\"Temizle\"],\"xOTzt5\":[\"az önce\"],\"xUHRTR\":[\"Bağlanırken otomatik olarak operatör kimliği doğrula\"],\"xWHwwQ\":[\"Yasaklar\"],\"xYilR2\":[\"Medya\"],\"xbi8D6\":[\"Bu sunucu davet bağlantılarını desteklemiyor (<0>obby.world/invitation0> yeteneği duyurulmuyor). Yine de normal şekilde sohbet edebilirsiniz; bu panel obbyircd tabanlı ağlar içindir.\"],\"xceQrO\":[\"Yalnızca güvenli websocket'ler desteklenmektedir\"],\"xdtXa+\":[\"kanal-adı\"],\"xfXC7q\":[\"Metin Kanalları\"],\"xlCYOE\":[\"Daha fazla mesaj alınıyor...\"],\"xlhswE\":[\"Minimum değer \",[\"0\"]],\"xq97Ci\":[\"Kelime veya ifade ekle...\"],\"xuRqRq\":[\"İstemci Sınırı (+l)\"],\"xwF+7J\":[[\"0\"],\" yazıyor...\"],\"y1eoq1\":[\"Bağlantıyı kopyala\"],\"yNeucF\":[\"Bu sunucu genişletilmiş profil meta verilerini (IRCv3 METADATA uzantısı) desteklemiyor. Avatar, görünen ad ve durum gibi ek alanlar mevcut değil.\"],\"yPlrca\":[\"Kanal Avatarı\"],\"yQE2r9\":[\"Yükleniyor\"],\"ySU+JY\":[\"eposta@adresiniz.com\"],\"yTX1Rt\":[\"Oper Kullanıcı Adı\"],\"yYOzWD\":[\"günlükler\"],\"yfx9Re\":[\"IRC operatör şifresi\"],\"ygCKqB\":[\"Durdur\"],\"ymDxJx\":[\"IRC operatör kullanıcı adı\"],\"yrpRsQ\":[\"Ada Göre Sırala\"],\"yz7wBu\":[\"Kapat\"],\"zJw+jA\":[\"modu ayarlar: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"dk önce\"],\"zebeLu\":[\"Oper kullanıcı adını girin\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Geçersiz desen biçimi. nick!user@host biçimini kullanın (joker karakter * kullanılabilir)\"],\"+6NQQA\":[\"Genel Destek Kanalı\"],\"+6NyRG\":[\"İstemci\"],\"+K0AvT\":[\"Bağlantıyı Kes\"],\"+cyFdH\":[\"Uzakta olarak işaretlendiğinde gösterilecek varsayılan mesaj\"],\"+mVPqU\":[\"Mesajlarda markdown biçimlendirmesini işle\"],\"+vqCJH\":[\"Kimlik doğrulama için hesap kullanıcı adınız\"],\"+yPBXI\":[\"Dosya seç\"],\"+zy2Nq\":[\"Tür\"],\"/09cao\":[\"Düşük Bağlantı Güvenliği (Seviye \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Kanal dışındaki kullanıcılar kanala mesaj gönderemez\"],\"/4C8U0\":[\"Tümünü kopyala\"],\"/6BzZF\":[\"Üye Listesini Aç/Kapat\"],\"/AkXyp\":[\"Onaylıyor musunuz?\"],\"/TNOPk\":[\"Kullanıcı uzakta\"],\"/XQgft\":[\"Keşfet\"],\"/cF7Rs\":[\"Ses Seviyesi\"],\"/dqduX\":[\"Sonraki sayfa\"],\"/fc3q4\":[\"Tüm İçerik\"],\"/kISDh\":[\"Bildirim Seslerini Etkinleştir\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Ses\"],\"/rfkZe\":[\"Bahisler ve mesajlar için ses çal\"],\"0/0ZGA\":[\"Kanal Adı Maskesi\"],\"0D6j7U\":[\"Özel kurallar hakkında daha fazla bilgi →\"],\"0XsHcR\":[\"Kullanıcıyı At\"],\"0ZpE//\":[\"Kullanıcıya Göre Sırala\"],\"0bEPwz\":[\"Uzakta Olarak İşaretle\"],\"0dGkPt\":[\"Kanal listesini genişlet\"],\"0gS7M5\":[\"Görünen Ad\"],\"0kS+M8\":[\"ÖrnekAĞ\"],\"0rgoY7\":[\"Yalnızca seçtiğiniz sunuculara bağlanın\"],\"0wdd7X\":[\"Katıl\"],\"0wkVYx\":[\"Özel Mesajlar\"],\"111uHX\":[\"Bağlantı önizlemesi\"],\"196EG4\":[\"Özel Sohbeti Sil\"],\"1DSr1i\":[\"Hesap kaydı oluştur\"],\"1O/24y\":[\"Kanal Listesini Aç/Kapat\"],\"1TNIig\":[\"Aç\"],\"1VPJJ2\":[\"Harici Bağlantı Uyarısı\"],\"1ZC/dv\":[\"Okunmamış bahis veya mesaj yok\"],\"1pO1zi\":[\"Sunucu adı gereklidir\"],\"1uwfzQ\":[\"Kanal Konusunu Görüntüle\"],\"268g7c\":[\"Görünen adı girin\"],\"2CEOW6\":[\"soju bouncer üzerinden bağlı ağ\"],\"2F9+AZ\":[\"Henüz ham IRC trafiÄi yakalanmadı. BaÄlanmayı veya bir mesaj göndermeyi deneyin.\"],\"2FOFq1\":[\"Ağdaki sunucu operatörleri mesajlarınızı okuyabilir\"],\"2FYpfJ\":[\"Daha fazla\"],\"2HF1Y2\":[[\"inviter\"],\", \",[\"target\"],\" kişisini \",[\"channel\"],\" kanalına davet etti\"],\"2I70QL\":[\"Kullanıcı profil bilgilerini görüntüle\"],\"2QYdmE\":[\"Kullanıcılar:\"],\"2QpEjG\":[\"ayrıldı\"],\"2YE223\":[\"#\",[\"0\"],\" kanalına mesaj (yeni satır için Enter, göndermek için Shift+Enter)\"],\"2bimFY\":[\"Sunucu şifresini kullan\"],\"2iTmdZ\":[\"Yerel Depolama:\"],\"2odkwe\":[\"Katı - Daha agresif koruma\"],\"2uDhbA\":[\"Davet edilecek kullanıcı adını girin\"],\"2ygf/L\":[\"← Geri\"],\"2zEgxj\":[\"GIF ara...\"],\"3RdPhl\":[\"Kanalı Yeniden Adlandır\"],\"3THokf\":[\"Sesli Kullanıcı\"],\"3TSz9S\":[\"Küçült\"],\"3jBDvM\":[\"Kanal Görünen Adı\"],\"3ryuFU\":[\"Uygulamayı geliştirmek için isteğe bağlı çökme raporları\"],\"3uBF/8\":[\"Görüntüleyiciyi kapat\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Hesap adını girin...\"],\"4/Rr0R\":[\"Mevcut kanala bir kullanıcı davet et\"],\"4EZrJN\":[\"Kurallar\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"Flood Profili (+F)\"],\"4RZQRK\":[\"Ne yapıyorsun?\"],\"4hfTrB\":[\"Takma Ad\"],\"4n99LO\":[[\"0\"],\" kanalında zaten var\"],\"4t6vMV\":[\"Kısa mesajlar için otomatik olarak tek satıra geç\"],\"4vsHmf\":[\"Süre (dk)\"],\"4x/Axu\":[\"Bouncer'ınızda henüz ağ yok. Başlamak için bir tane ekleyin.\"],\"5+INAX\":[\"Sizi bahseden mesajları vurgula\"],\"5R5Pv/\":[\"Oper Adı\"],\"678PKt\":[\"Ağ Adı\"],\"6Aih4U\":[\"Çevrimdışı\"],\"6CO3WE\":[\"Kanala katılmak için şifre gerekli. Anahtarı kaldırmak için boş bırakın.\"],\"6HhMs3\":[\"Ayrılma Mesajı\"],\"6V3Ea3\":[\"Kopyalandı\"],\"6lGV3K\":[\"Daha az göster\"],\"6yFOEi\":[\"Oper şifresini girin...\"],\"7+IHTZ\":[\"Dosya seçilmedi\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (örn. spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Özel mesaj gönder\"],\"7U1W7c\":[\"Çok Rahat\"],\"7Y1YQj\":[\"Gerçek ad:\"],\"7YHArF\":[\"— görüntüleyicide aç\"],\"7fjnVl\":[\"Kullanıcı ara...\"],\"7jL88x\":[\"Bu mesaj silinsin mi? Bu işlem geri alınamaz.\"],\"7nGhhM\":[\"Aklınızda ne var?\"],\"7sEpu1\":[\"Üyeler — \",[\"0\"]],\"7sNhEz\":[\"Kullanıcı Adı\"],\"8H0Q+x\":[\"Profiller hakkında daha fazla bilgi →\"],\"8Phu0A\":[\"Kullanıcılar takma adını değiştirdiğinde göster\"],\"8XTG9e\":[\"Oper şifresini girin\"],\"8XsV2J\":[\"Yeniden gönder\"],\"8ZsakT\":[\"Şifre\"],\"8kR84m\":[\"Harici bir bağlantı açmak üzeresiniz:\"],\"8lCgih\":[\"Kuralı Kaldır\"],\"8o3dPc\":[\"Yüklemek için dosyaları bırakın\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"katıldı\"],\"other\":[[\"joinCount\"],\" kez katıldı\"]}]],\"9BMLnJ\":[\"Sunucuya yeniden bağlan\"],\"9OEgyT\":[\"Tepki ekle\"],\"9PQ8m2\":[\"G-Line (global yasak)\"],\"9Qs99X\":[\"E-posta:\"],\"9QupBP\":[\"Deseni kaldır\"],\"9W7tl5\":[\"(değiştirilmedi)\"],\"9bG48P\":[\"Gönderiliyor\"],\"9f5f0u\":[\"Gizlilik hakkında sorularınız mı var? Bize ulaşın:\"],\"9iweoP\":[[\"0\"],\" üzerindeki ağlar\"],\"9unqs3\":[\"Uzakta:\"],\"9v3hwv\":[\"Sunucu bulunamadı.\"],\"9zb2WA\":[\"Bağlanıyor\"],\"A1taO8\":[\"Ara\"],\"A2adVi\":[\"Yazıyor Bildirimleri Gönder\"],\"A9Rhec\":[\"Kanal Adı\"],\"AWOSPo\":[\"Yakınlaştır\"],\"AXSpEQ\":[\"Bağlanırken Oper Ol\"],\"AdKRCX\":[\"<0>\",[\"0\"],\"0> sunucusuna bağlısınız.\"],\"AeXO77\":[\"Hesap\"],\"AhNP40\":[\"Konuma git\"],\"Ai2U7L\":[\"Host\"],\"AjBQnf\":[\"Takma ad değiştirildi\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Yanıtı iptal et\"],\"ApSx0O\":[\"\\\"\",[\"searchQuery\"],\"\\\" ile eşleşen \",[\"0\"],\" mesaj bulundu\"],\"AxPAXW\":[\"Sonuç bulunamadı\"],\"AyNqAB\":[\"Tüm sunucu olaylarını sohbette göster\"],\"B/QqGw\":[\"Klavyeden uzakta\"],\"B0sB2k\":[\"Düz metin\"],\"B8AaMI\":[\"Bu alan zorunludur\"],\"BA2c49\":[\"Sunucu gelişmiş LIST filtrelemesini desteklemiyor\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" ve \",[\"3\"],\" kişi daha yazıyor...\"],\"BGul2A\":[\"Kaydedilmemiş değişiklikleriniz var. Kaydetmeden kapatmak istediğinizden emin misiniz?\"],\"BIf9fi\":[\"Durum mesajınız\"],\"BOJWfb\":[\"soju bouncer bağlantısı kesilsin mi?\"],\"BPm98R\":[\"Hiçbir sunucu seçilmedi. Ãnce kenar çubuÄundan bir sunucu seçin; davet baÄlantıları sunucu baÅına yönetilir.\"],\"BZz3md\":[\"Kişisel web siteniz\"],\"Bgm/H7\":[\"Çok satırlı metin girişine izin ver\"],\"BiQIl1\":[\"Bu özel mesaj konuşmasını sabitle\"],\"BlNZZ2\":[\"Mesaja gitmek için tıklayın\"],\"Bowq3c\":[\"Yalnızca operatörler kanal konusunu değiştirebilir\"],\"Btozzp\":[\"Bu görüntünün süresi doldu\"],\"Bycfjm\":[\"Toplam: \",[\"0\"]],\"C6IBQc\":[\"Tüm JSON'u kopyala\"],\"C9L9wL\":[\"Veri Toplama\"],\"CDq4wC\":[\"Kullanıcıyı Yönet\"],\"CHVRxG\":[\"@\",[\"0\"],\"'a mesaj (yeni satır için Shift+Enter)\"],\"CN9zdR\":[\"Oper adı ve şifresi gereklidir\"],\"CW3sYa\":[[\"emoji\"],\" tepkisi ekle\"],\"CaAkqd\":[\"Ayrılmaları Göster\"],\"CbvaYj\":[\"Takma Adıyla Yasakla\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Bir kanal seçin\"],\"CsekCi\":[\"Normal\"],\"D+NlUC\":[\"Sistem\"],\"D28t6+\":[\"katıldı ve ayrıldı\"],\"DB8zMK\":[\"Uygula\"],\"DBcWHr\":[\"Özel bildirim sesi dosyası\"],\"DTy9Xw\":[\"Medya Önizlemeleri\"],\"Dj4pSr\":[\"Güvenli bir şifre seçin\"],\"Du+zn+\":[\"Aranıyor...\"],\"Du2T2f\":[\"Ayar bulunamadı\"],\"DwsSVQ\":[\"Filtreleri Uygula ve Yenile\"],\"E3W/zd\":[\"Varsayılan Takma Ad\"],\"E6nRW7\":[\"URL'yi Kopyala\"],\"E703RG\":[\"Modlar:\"],\"EAeu1Z\":[\"Davet Gönder\"],\"EFKJQT\":[\"Ayar\"],\"EGPQBv\":[\"Özel Flood Kuralları (+f)\"],\"ELik0r\":[\"Tam Gizlilik Politikasını Görüntüle\"],\"EPbeC2\":[\"Kanal konusunu görüntüle veya düzenle\"],\"EQCDNT\":[\"Oper kullanıcı adını girin...\"],\"EUvulZ\":[\"\\\"\",[\"searchQuery\"],\"\\\" ile eşleşen 1 mesaj bulundu\"],\"EatZYJ\":[\"Sonraki görüntü\"],\"EdQY6l\":[\"Yok\"],\"EnqLYU\":[\"Sunucu ara...\"],\"F0OKMc\":[\"Sunucuyu Düzenle\"],\"F6Int2\":[\"Vurgulamayı Etkinleştir\"],\"FDoLyE\":[\"Maks. Kullanıcı\"],\"FUU/hZ\":[\"Sohbette ne kadar harici medyanın yükleneceğini denetleyin.\"],\"Fdp03t\":[\"açık\"],\"FfPWR0\":[\"Pencere\"],\"FjkaiT\":[\"Uzaklaştır\"],\"FlqOE9\":[\"Bu ne anlama geliyor:\"],\"FolHNl\":[\"Hesabınızı ve kimlik doğrulamayı yönetin\"],\"Fp2Dif\":[\"Sunucudan ayrıldı\"],\"G5KmCc\":[\"GZ-Line (global Z-Line)\"],\"GDs0lz\":[\"<0>Risk:0> Hassas bilgiler (mesajlar, özel konuşmalar, kimlik doğrulama bilgileri) ağ yöneticilerine veya IRC sunucuları arasına konumlanmış saldırganlara açık olabilir.\"],\"GR+2I3\":[\"Davet maskesi ekle (örn. nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Açılır sunucu bildirimlerini kapat\"],\"GdhD7H\":[\"Onaylamak için tekrar tıklayın\"],\"GjRZex\":[\"Ağ bağlantısı kesilsin mi?\"],\"GlHnXw\":[\"Takma ad değişikliği başarısız: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Önizleme:\"],\"GtmO8/\":[\"kimden\"],\"GtuHUQ\":[\"Bu kanalı sunucuda yeniden adlandır. Tüm kullanıcılar yeni adı görecek.\"],\"GuGfFX\":[\"Aramayı aç/kapat\"],\"GxkJXS\":[\"Yükleniyor...\"],\"GzbwnK\":[\"Kanala katıldı\"],\"GzsUDB\":[\"Genişletilmiş Profil\"],\"H/PnT8\":[\"Emoji ekle\"],\"H6Izzl\":[\"Tercih ettiğiniz renk kodu\"],\"H9jIv+\":[\"Katılma/Ayrılma Göster\"],\"HAKBY9\":[\"Dosyaları yükle\"],\"HdE1If\":[\"Kanal\"],\"Hk4AW9\":[\"Tercih ettiğiniz görünen ad\"],\"HmHDk7\":[\"Üye Seç\"],\"HrQzPU\":[[\"networkName\"],\" üzerindeki kanallar\"],\"I2tXQ5\":[\"@\",[\"0\"],\"'a mesaj (yeni satır için Enter, göndermek için Shift+Enter)\"],\"I6bw/h\":[\"Kullanıcıyı Yasakla\"],\"I92Z+b\":[\"Bildirimleri etkinleştir\"],\"I9D72S\":[\"Bu mesajı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.\"],\"IA+1wo\":[\"Kullanıcılar kanaldan atıldığında göster\"],\"IDwkJx\":[\"IRC Operatörü\"],\"ILlU+s\":[\"Bilgi:\"],\"IUwGEM\":[\"Değişiklikleri Kaydet\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" ve \",[\"2\"],\" yazıyor...\"],\"IgrLD/\":[\"Duraklat\"],\"Im6JED\":[\"FISISALTI\"],\"ImOQa9\":[\"Yanıtla\"],\"IoHMnl\":[\"Maksimum değer \",[\"0\"]],\"IvMj+0\":[\"Op\"],\"J28zul\":[\"Bağlanıyor...\"],\"J5T9NW\":[\"Kullanıcı Bilgileri\"],\"J8Y5+z\":[\"Eyvah! Ağ bölündü! ⚠️\"],\"JBHkBA\":[\"Kanaldan ayrıldı\"],\"JCwL0Q\":[\"Neden girin (isteğe bağlı)\"],\"JFciKP\":[\"Değiştir\"],\"JXGkhG\":[\"Kanal adını değiştir (yalnızca operatörler)\"],\"JcD7qf\":[\"Daha fazla işlem\"],\"JdkA+c\":[\"Gizli (+s)\"],\"Jmu12l\":[\"Sunucu Kanalları\"],\"JoQY+E\":[\"Bu, ağı soju bouncer'ınızdan kaldırır. Tekrar kullanmak için yeniden eklemeniz gerekir.\"],\"JvQ++s\":[\"Markdown'ı Etkinleştir\"],\"K2jwh/\":[\"WHOIS verisi mevcut değil\"],\"KAXSwC\":[\"Voice\"],\"KDfTdX\":[\"Mesajı sil\"],\"KKBlUU\":[\"Gömülü\"],\"KM0pLb\":[\"Kanala hoş geldiniz!\"],\"KR6W2h\":[\"Kullanıcının Engelini Kaldır\"],\"KV+Bi1\":[\"Yalnızca Davetli (+i)\"],\"KdCtwE\":[\"Sayaçları sıfırlamadan önce flood etkinliğinin kaç saniye izleneceği\"],\"Kkezga\":[\"Sunucu Şifresi\"],\"KsiQ/8\":[\"Kullanıcıların kanala katılmak için davet edilmesi gerekir\"],\"L+gB/D\":[\"Kanal bilgisi\"],\"LC1a7n\":[\"IRC sunucusu, sunucular arası bağlantılarının düşük güvenlik seviyesine sahip olduğunu bildirdi. Bu, mesajlarınız ağdaki IRC sunucuları arasında iletilirken düzgün şifrelenmeyebileceği veya SSL/TLS sertifikalarının doğru şekilde doğrulanmayabileceği anlamına gelir.\"],\"LEwpeL\":[\"soju bouncer (kontrol)\"],\"LNfLR5\":[\"Atmaları Göster\"],\"LP+1Z7\":[\"Ağ Ekle\"],\"LQb0W/\":[\"Tüm Olayları Göster\"],\"LU7/yA\":[\"Arayüzde gösterilecek alternatif ad. Boşluk, emoji ve özel karakter içerebilir. IRC komutlarında gerçek kanal adı (\",[\"channelName\"],\") kullanılmaya devam eder.\"],\"LUb9O7\":[\"Geçerli bir sunucu portu gereklidir\"],\"LV4fT6\":[\"Açıklama (isteÄe baÄlı, örn. \\\"Beta test ekibi Ã3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Gizlilik Politikası\"],\"LcuSDR\":[\"Profil bilgilerinizi ve meta verilerinizi yönetin\"],\"LqLS9B\":[\"Takma Ad Değişikliklerini Göster\"],\"LsDQt2\":[\"Kanal Ayarları\"],\"LtI9AS\":[\"Sahip\"],\"LuNhhL\":[\"bu mesaja tepki verdi\"],\"M/AZNG\":[\"Avatar görüntünüzün URL'si\"],\"M/WIer\":[\"Mesaj Gönder\"],\"M8er/5\":[\"Ad:\"],\"MHk+7g\":[\"Önceki görüntü\"],\"MRorGe\":[\"Kullanıcıya PM Gönder\"],\"MVbSGP\":[\"Zaman Penceresi (saniye)\"],\"MkpcsT\":[\"Mesajlarınız ve ayarlarınız cihazınızda yerel olarak saklanır\"],\"MzPdC2\":[\"Sunucu Şifresi (PASS)\"],\"N/hDSy\":[\"Bot olarak işaretle - genellikle 'on' veya boş\"],\"N6j2JH\":[[\"0\"],\" ögesini düzenle\"],\"N7TQbE\":[[\"channelName\"],\" kanalına Kullanıcı Davet Et\"],\"NCca/o\":[\"Varsayılan takma adı girin...\"],\"Nqs6B9\":[\"Tüm harici medyayı gösterir. Herhangi bir URL bilinmeyen bir sunucuya istek gönderebilir.\"],\"Nt+9O7\":[\"Ham TCP yerine WebSocket kullan\"],\"NxIHzc\":[\"Kullanıcıyı at\"],\"O+v/cL\":[\"Sunucudaki tüm kanalları görüntüle\"],\"OCGpR4\":[\"(devral)\"],\"ODwSCk\":[\"GIF gönder\"],\"OGQ5kK\":[\"Bildirim seslerini ve vurguları yapılandır\"],\"OIPt1Z\":[\"Üye listesi kenar çubuğunu göster veya gizle\"],\"OKSNq/\":[\"Çok Katı\"],\"ONWvwQ\":[\"Yükle\"],\"OVKoQO\":[\"Kimlik doğrulama için hesap şifreniz\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - IRC'yi geleceğe taşıyor\"],\"OhCpra\":[\"Konu belirle…\"],\"OkltoQ\":[[\"username\"],\" kullanıcısını takma adıyla yasakla (aynı takma adla yeniden katılmasını engeller)\"],\"P+t/Te\":[\"Ek veri yok\"],\"P42Wcc\":[\"Güvenli\"],\"PD38l0\":[\"Kanal avatarı önizlemesi\"],\"PD9mEt\":[\"Mesaj yazın...\"],\"PPqfdA\":[\"Kanal yapılandırma ayarlarını aç\"],\"PSCjfZ\":[\"Bu kanal için görüntülenecek konu. Tüm kullanıcılar konuyu görebilir.\"],\"PZCecv\":[\"PDF önizleme\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 kez\"],\"other\":[[\"c\"],\" kez\"]}]],\"PguS2C\":[\"İstisna maskesi ekle (örn. nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[[\"0\"],\" kanaldan \",[\"displayedChannelsCount\"],\" tanesi gösteriliyor\"],\"PqhVlJ\":[\"Kullanıcıyı Yasakla (Host Maskesiyle)\"],\"Q+chwU\":[\"Kullanıcı adı:\"],\"Q2QY4/\":[\"Bu daveti sil\"],\"Q3v9Wc\":[\"Evet, sil\"],\"Q6hhn8\":[\"Tercihler\"],\"QF4a34\":[\"Lütfen bir kullanıcı adı girin\"],\"QGqSZ2\":[\"Renk ve Biçimlendirme\"],\"QJQd1J\":[\"Profili Düzenle\"],\"QSzGDE\":[\"Boşta\"],\"QUlny5\":[[\"0\"],\"'a hoş geldiniz!\"],\"Qoq+GP\":[\"Devamını oku\"],\"QuSkCF\":[\"Kanalları filtrele...\"],\"QwUrDZ\":[\"konuyu şu şekilde değiştirdi: \",[\"topic\"]],\"R0UH07\":[[\"1\"],\" görselinden \",[\"0\"],\". görsel\"],\"R7SsBE\":[\"Sessize Al\"],\"R8rf1X\":[\"Konu belirlemek için tıklayın\"],\"RArB3D\":[[\"channelName\"],\" kanalından \",[\"username\"],\" tarafından atıldı\"],\"RI3cWd\":[\"ObsidianIRC ile IRC dünyasını keşfedin\"],\"RIfHS5\":[\"Yeni bir davet baÄlantısı oluÅtur\"],\"RMMaN5\":[\"Moderasyonlu (+m)\"],\"RWw9Lg\":[\"Pencereyi kapat\"],\"RZ2BuZ\":[[\"account\"],\" hesap kaydı doğrulama gerektiriyor: \",[\"message\"]],\"RySp6q\":[\"Yorumları gizle\"],\"S5Togi\":[\"Bouncer'ınızdan ağlar yükleniyor…\"],\"SPKQTd\":[\"Takma ad gereklidir\"],\"SPVjfj\":[\"Boş bırakılırsa varsayılan olarak 'neden yok' kullanılır\"],\"SQKPvQ\":[\"Kullanıcı Davet Et\"],\"STmlpb\":[\"Ağ listesine geri dön\"],\"SkZcl+\":[\"Önceden tanımlanmış bir flood koruma profili seçin. Bu profiller, farklı kullanım durumları için dengeli koruma ayarları sunar.\"],\"Slr+3C\":[\"Min. Kullanıcı\"],\"Spnlre\":[[\"target\"],\" kişisini \",[\"channel\"],\" kanalına davet ettiniz\"],\"T/ckN5\":[\"Görüntüleyicide aç\"],\"T91vKp\":[\"Oynat\"],\"TV2Wdu\":[\"Verilerinizi nasıl işlediğimizi ve gizliliğinizi nasıl koruduğumuzu öğrenin.\"],\"TgFpwD\":[\"Uygulanıyor...\"],\"TkzSFB\":[\"Değişiklik Yok\"],\"TtserG\":[\"Gerçek adı girin\"],\"Ttz9J1\":[\"Şifreyi girin...\"],\"Tz0i8g\":[\"Ayarlar\"],\"U3pytU\":[\"Yönetici\"],\"UDb2YD\":[\"Tepki Ver\"],\"UE4KO5\":[\"*kanal*\"],\"UETAwW\":[\"Henüz hiç davet baÄlantısı oluÅturmadınız. İlkini oluÅturmak için yukarıdaki formu kullanın.\"],\"UGT5vp\":[\"Ayarları Kaydet\"],\"UV5hLB\":[\"Yasak bulunamadı\"],\"Uaj3Nd\":[\"Durum Mesajları\"],\"Ue3uny\":[\"Varsayılan (profil yok)\"],\"UkARhe\":[\"Normal - Standart koruma\"],\"Umn7Cj\":[\"Henüz yorum yok. İlk sen ol!\"],\"UtUIRh\":[[\"0\"],\" eski mesaj\"],\"UwzP+U\":[\"Güvenli Bağlantı\"],\"V0/A4O\":[\"Kanal Sahibi\"],\"V0zZWc\":[\"Bu, aşağıdaki bağlı ağı da kapatacak.\"],\"V4qgxE\":[\"Şu kadar dakika önce önce oluşturulan\"],\"V8yTm6\":[\"Aramayı temizle\"],\"VJMMyz\":[\"ObsidianIRC - IRC'yi geleceğe taşıyor\"],\"VJScHU\":[\"Neden\"],\"VLsmVV\":[\"Bildirimleri sessize al\"],\"VbyRUy\":[\"Yorumlar\"],\"Vmx0mQ\":[\"Ayarlayan:\"],\"VqnIZz\":[\"Gizlilik politikamızı ve veri uygulamalarımızı görüntüleyin\"],\"VrMygG\":[\"Minimum uzunluk \",[\"0\"]],\"VrnTui\":[\"Profilinizde gösterilen zamirleriniz\"],\"W8E3qn\":[\"Doğrulanmış Hesap\"],\"WAakm9\":[\"Kanalı Sil\"],\"WFxTHC\":[\"Yasaklama maskesi ekle (örn. nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Sunucu hostu gereklidir\"],\"WRYdXW\":[\"Ses konumu\"],\"WUOH5B\":[\"Kullanıcıyı Engelle\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"1 öğe daha göster\"],\"other\":[[\"1\"],\" öğe daha göster\"]}]],\"WYxRzo\":[\"Davet baÄlantılarınızı oluÅturun ve yönetin\"],\"Wd38W1\":[\"Genel bir aÄ daveti için kanal alanını boÅ bırakın. Açıklama yalnızca sizin kayıtlarınız içindir â bu listede yalnızca sizin görebileceÄiniz Åekilde gösterilir.\"],\"Weq9zb\":[\"Genel\"],\"Wfj7Sk\":[\"Bildirim seslerini sessize al veya aç\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"Kullanıcı Profili\"],\"X6S3lt\":[\"Ayarlar, kanallar, sunucular arayın...\"],\"XEHan5\":[\"Yine de Devam Et\"],\"XI1+wb\":[\"Geçersiz biçim\"],\"XIXeuC\":[\"@\",[\"0\"],\"'a mesaj\"],\"XMS+k4\":[\"Özel Mesaj Başlat\"],\"XWgxXq\":[\"Albüm\"],\"Xd7+IT\":[\"Özel Sohbetin Sabitlemesini Kaldır\"],\"Xm/s+u\":[\"Görünüm\"],\"Xp2n93\":[\"Sunucunuzun güvenilir dosya hostundan medya gösterir. Harici hizmetlere istek gönderilmez.\"],\"XvjC4F\":[\"Kaydediliyor...\"],\"Y/qryO\":[\"Aramanızla eşleşen kullanıcı bulunamadı\"],\"YAqRpI\":[[\"account\"],\" hesap kaydı başarılı: \",[\"message\"]],\"YEfzvP\":[\"Korumalı Konu (+t)\"],\"YQOn6a\":[\"Üye listesini daralt\"],\"YRCoE9\":[\"Kanal Operatörü\"],\"YURQaF\":[\"Profili Görüntüle\"],\"YdBSvr\":[\"Medya gösterimini ve harici içeriği denetleyin\"],\"Yj6U3V\":[\"Merkezi Sunucu Yok:\"],\"YjvpGx\":[\"Zamirler\"],\"YqH4l4\":[\"Anahtar yok\"],\"YyUPpV\":[\"Hesap:\"],\"ZJSWfw\":[\"Sunucudan ayrıldığınızda gösterilen mesaj\"],\"ZR1dJ4\":[\"Davetler\"],\"ZdWg0V\":[\"Tarayıcıda aç\"],\"ZhRBbl\":[\"Mesajlarda ara…\"],\"Zmcu3y\":[\"Gelişmiş Filtreler\"],\"a0bHay\":[\"<0>\",[\"0\"],\"0> bağlantısı kesilsin mi?\"],\"a2/8e5\":[\"Şu kadar dakika önce sonra konu belirlenen\"],\"aHKcKc\":[\"Önceki sayfa\"],\"aJTbXX\":[\"Oper Şifresi\"],\"aQryQv\":[\"Desen zaten mevcut\"],\"aW9pLN\":[\"Kanalda izin verilen maksimum kullanıcı sayısı. Sınır olmaması için boş bırakın.\"],\"ah4fmZ\":[\"YouTube, Vimeo, SoundCloud ve benzeri bilinen hizmetlerden önizlemeler de gösterir.\"],\"aifXak\":[\"Bu kanalda medya yok\"],\"ap2zBz\":[\"Rahat\"],\"az8lvo\":[\"Kapalı\"],\"azXSNo\":[\"Üye listesini genişlet\"],\"azdliB\":[\"Bir hesaba giriş yap\"],\"b26wlF\":[\"o/onun\"],\"bD/+Ei\":[\"Katı\"],\"bQ6BJn\":[\"Ayrıntılı flood koruma kurallarını yapılandırın. Her kural, hangi etkinlik türünün izleneceğini ve eşikler aşıldığında hangi işlemin yapılacağını belirtir.\"],\"beV7+y\":[\"Kullanıcı \",[\"channelName\"],\" kanalına katılmak için davet alacak.\"],\"bk84cH\":[\"Uzakta Mesajı\"],\"bkHdLj\":[\"IRC Sunucusu Ekle\"],\"bmQLn5\":[\"Kural Ekle\"],\"bv4cFj\":[\"Taşıma\"],\"bwRvnp\":[\"İşlem\"],\"c8+EVZ\":[\"Doğrulanmış hesap\"],\"cGYUlD\":[\"Medya önizlemesi yüklenmiyor.\"],\"cLF98o\":[\"Yorumları göster (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Kullanılabilir kullanıcı yok\"],\"cSgpoS\":[\"Özel Sohbeti Sabitle\"],\"cXeEKu\":[\"Bu, aşağıdaki bağlı \",[\"0\"],\" ağı da kapatacak.\"],\"cde3ce\":[\"<0>\",[\"0\"],\"0>'a mesaj\"],\"chQsxg\":[\"Biçimlendirilmiş çıktıyı kopyala\"],\"cl/A5J\":[[\"__DEFAULT_IRC_SERVER_NAME__\"],\"'a hoş geldiniz!\"],\"cnGeoo\":[\"Sil\"],\"coPLXT\":[\"IRC iletişimlerinizi sunucularımızda saklamıyoruz\"],\"crYH/6\":[\"SoundCloud oynatıcı\"],\"cv5DQb\":[\"ana makine ayarlanmadı\"],\"d3sis4\":[\"Sunucu Ekle\"],\"d9aN5k\":[[\"username\"],\" kullanıcısını kanaldan kaldır\"],\"dEgA5A\":[\"İptal\"],\"dGi1We\":[\"Bu özel mesaj konuşmasının sabitlemesini kaldır\"],\"dJVuyC\":[[\"channelName\"],\" kanalından ayrıldı (\",[\"reason\"],\")\"],\"dMtLDE\":[\"kime\"],\"dXqxlh\":[\"<0>⚠️ Güvenlik Riski!0> Bu bağlantı, araya girme veya ortadaki adam saldırılarına karşı savunmasız olabilir.\"],\"da9Q/R\":[\"Kanal modları değiştirildi\"],\"dhJN3N\":[\"Yorumları göster\"],\"dj2xTE\":[\"Bildirimi kapat\"],\"dpCzmC\":[\"Flood Koruma Ayarları\"],\"e9dQpT\":[\"Bu bağlantıyı yeni sekmede açmak istiyor musunuz?\"],\"ePK91l\":[\"Düzenle\"],\"eYBDuB\":[\"Bir görüntü yükleyin veya dinamik boyutlandırma için isteğe bağlı \",[\"size\"],\" değişkeni içeren bir URL sağlayın\"],\"edBbee\":[[\"username\"],\" kullanıcısını host maskesiyle yasakla (aynı IP/host'tan yeniden katılmasını engeller)\"],\"ekfzWq\":[\"Kullanıcı Ayarları\"],\"elPDWs\":[\"IRC istemci deneyiminizi özelleştirin\"],\"eu2osY\":[\"<0>💡 Öneri:0> Yalnızca bu sunucuya güveniyorsanız ve risklerin farkındaysanız devam edin. Bu bağlantı üzerinden hassas bilgi veya şifre paylaşmaktan kaçının.\"],\"euEhbr\":[[\"channel\"],\" kanalına katılmak için tıklayın\"],\"ez3vLd\":[\"Çok Satırlı Girişi Etkinleştir\"],\"f0J5Ki\":[\"Sunucular arası iletişim şifrelenmemiş bağlantılar kullanıyor olabilir\"],\"f9BHJk\":[\"Kullanıcıyı Uyar\"],\"fDOLLd\":[\"Kanal bulunamadı.\"],\"ffzDkB\":[\"Anonim Analitik:\"],\"fq1GF9\":[\"Kullanıcılar sunucudan ayrıldığında göster\"],\"gCldcN\":[\"Vurgu rengini değiştir\"],\"gEF57C\":[\"Bu sunucu yalnızca bir bağlantı türünü destekliyor\"],\"gJuLUI\":[\"Engelleme Listesi\"],\"gNzMrk\":[\"Mevcut avatar\"],\"gjPWyO\":[\"Takma adı girin...\"],\"gz6UQ3\":[\"Büyüt\"],\"h6/IMX\":[\"İlk ağınızı ekleyin\"],\"h6razj\":[\"Kanal Adı Maskesini Hariç Tut\"],\"hG6jnw\":[\"Konu belirlenmemiş\"],\"hG89Ed\":[\"Görüntü\"],\"hYgDIe\":[\"OluÅtur\"],\"hZ6znB\":[\"Port\"],\"ha+Bz5\":[\"örn. 100:1440\"],\"he3ygx\":[\"Kopyala\"],\"hehnjM\":[\"Miktar\"],\"hzdLuQ\":[\"Yalnızca Voice veya daha yüksek yetkiye sahip kullanıcılar konuşabilir\"],\"i0qMbr\":[\"Ana Sayfa\"],\"iDNBZe\":[\"Bildirimler\"],\"iH8pgl\":[\"Geri\"],\"iL9SZg\":[\"Kullanıcıyı Yasakla (Takma Adıyla)\"],\"iNt+3c\":[\"Görüntüye geri dön\"],\"iQvi+a\":[\"Bu sunucu için düşük bağlantı güvenliği konusunda uyarma\"],\"iSLIjg\":[\"Bağlan\"],\"iWXkHH\":[\"Halfop\"],\"iZeTtp\":[\"Sunucu Hostu\"],\"idD8Ev\":[\"Kaydedildi\"],\"iivqkW\":[\"Giriş Yapıldı\"],\"ij+Elv\":[\"Görüntü önizlemesi\"],\"ilIWp7\":[\"Bildirimleri Aç/Kapat\"],\"iuaqvB\":[\"Joker karakter için * kullanın. Örnekler: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Bot\"],\"j2DGR0\":[\"Host Maskesiyle Yasakla\"],\"jA4uoI\":[\"Konu:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Neden (isteğe bağlı)\"],\"jUV7CU\":[\"Avatar Yükle\"],\"jW5Uwh\":[\"Ne kadar harici medya yükleneceğini denetleyin. Kapalı / Güvenli / Güvenilir Kaynaklar / Tüm İçerik.\"],\"jXzms5\":[\"Ek seçenekleri\"],\"jZlrte\":[\"Renk\"],\"jfC/xh\":[\"İletişim\"],\"jywMpv\":[\"#yeni-kanal-adı\"],\"k112DD\":[\"Eski mesajları yükle\"],\"k3ID0F\":[\"Üyeleri filtrele…\"],\"k65gsE\":[\"Derinlemesine incele\"],\"k7Zgob\":[\"Bağlantıyı İptal Et\"],\"kAVx5h\":[\"Davet bulunamadı\"],\"kCLEPU\":[\"Bağlı Olduğu Sunucu\"],\"kF5LKb\":[\"Engellenen desenler:\"],\"kGeOx/\":[[\"0\"],\" kanalına katıl\"],\"kITKr8\":[\"Kanal modları yükleniyor...\"],\"kPpPsw\":[\"IRC Operatörüsünüz\"],\"kWJmRL\":[\"Siz\"],\"kfcRb0\":[\"Avatar\"],\"kjMqSj\":[\"JSON kopyala\"],\"krViRy\":[\"JSON olarak kopyalamak için tıklayın\"],\"ks71ra\":[\"İstisnalar\"],\"kw4lRv\":[\"Kanal Yarı Operatörü\"],\"kxgIRq\":[\"Başlamak için bir kanal seçin veya ekleyin.\"],\"ky6dWe\":[\"Avatar önizlemesi\"],\"l+GxCv\":[\"Kanallar yükleniyor...\"],\"l+IUVW\":[[\"account\"],\" hesap doğrulaması başarılı: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"yeniden bağlandı\"],\"other\":[[\"reconnectCount\"],\" kez yeniden bağlandı\"]}]],\"l1l8sj\":[[\"0\"],\"g önce\"],\"l5NhnV\":[\"#kanal (isteÄe baÄlı)\"],\"l5jmzx\":[[\"0\"],\" ve \",[\"1\"],\" yazıyor...\"],\"lCF0wC\":[\"Yenile\"],\"lHy8N5\":[\"Daha fazla kanal yükleniyor...\"],\"lasgrr\":[\"kullanıldı\"],\"lbpf14\":[[\"value\"],\" kanalına katıl\"],\"lfFsZ4\":[\"Kanallar\"],\"lkNdiH\":[\"Hesap Adı\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Görüntü Yükle\"],\"loQxaJ\":[\"Geri Döndüm\"],\"lvfaxv\":[\"ANA SAYFA\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Ekle\"],\"m8flAk\":[\"Önizleme (henüz yüklenmedi)\"],\"mEPxTp\":[\"<0>⚠️ Dikkatli olun!0> Yalnızca güvenilir kaynaklardan gelen bağlantıları açın. Kötü amaçlı bağlantılar güvenliğinizi veya gizliliğinizi tehlikeye atabilir.\"],\"mHGdhG\":[\"Sunucu bilgisi\"],\"mHS8lb\":[\"#\",[\"0\"],\" kanalına mesaj\"],\"mMYBD9\":[\"Geniş - Daha kapsamlı koruma alanı\"],\"mTGsPd\":[\"Kanal Konusu\"],\"mU8j6O\":[\"Harici Mesaj Yok (+n)\"],\"mZp8FL\":[\"Otomatik Tek Satıra Dön\"],\"mdQu8G\":[\"TakmaAdınız\"],\"miSSBQ\":[\"Yorumlar (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Kullanıcı kimliği doğrulandı\"],\"mwtcGl\":[\"Yorumları kapat\"],\"myL0MR\":[\"Bu ağ silinsin mi?\"],\"mzI/c+\":[\"İndir\"],\"n3fGRk\":[[\"0\"],\" tarafından ayarlandı\"],\"n5+j9l\":[\"örn. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Rahat - Daha az agresif koruma\"],\"nNflMD\":[\"Kanaldan ayrıl\"],\"nPXkBi\":[\"WHOIS verisi yükleniyor...\"],\"nQnxxF\":[\"#\",[\"0\"],\" kanalına mesaj (yeni satır için Shift+Enter)\"],\"nWMRxa\":[\"Sabitlemeyi Kaldır\"],\"nkC032\":[\"Flood profili yok\"],\"o69z4d\":[[\"username\"],\" kullanıcısına uyarı mesajı gönder\"],\"o9ylQi\":[\"Başlamak için GIF arayın\"],\"oFGkER\":[\"Sunucu Bildirimleri\"],\"oOi11l\":[\"En alta kaydır\"],\"oPYIL5\":[\"aÄ\"],\"oQEzQR\":[\"Yeni DM\"],\"oXOSPE\":[\"Çevrimiçi\"],\"oal760\":[\"Sunucu bağlantılarında ortadaki adam saldırıları mümkün\"],\"oeqmmJ\":[\"Güvenilir Kaynaklar\"],\"optX0N\":[[\"0\"],\"s önce\"],\"ovBPCi\":[\"Varsayılan\"],\"p0Z69r\":[\"Desen boş olamaz\"],\"p1KgtK\":[\"Ses yüklenemedi\"],\"p59pEv\":[\"Ek ayrıntılar\"],\"p7sRI6\":[\"Yazarken diğerlerini bilgilendir\"],\"pBm1od\":[\"Gizli kanal\"],\"pNmiXx\":[\"Tüm sunucular için varsayılan takma adınız\"],\"pUUo9G\":[\"Ana makine adı:\"],\"pVGPmz\":[\"Hesap Şifresi\"],\"peNE68\":[\"Kalıcı\"],\"plhHQt\":[\"Veri yok\"],\"pm6+q5\":[\"Güvenlik Uyarısı\"],\"pn5qSs\":[\"Ek Bilgiler\"],\"q0cR4S\":[\"artık **\",[\"newNick\"],\"** olarak bilinmektedir\"],\"qFcunY\":[\"Kanal LIST veya NAMES komutlarında görünmeyecek\"],\"qLpTm/\":[[\"emoji\"],\" tepkisini kaldır\"],\"qVkGWK\":[\"Sabitle\"],\"qY8wNa\":[\"Ana Sayfa\"],\"qb0xJ7\":[\"Joker karakter kullanın: * herhangi bir diziyle eşleşir, ? herhangi bir tek karakterle eşleşir. Örnekler: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Kanal Anahtarı (+k)\"],\"qtoOYG\":[\"Sınır yok\"],\"r1W2AS\":[\"Dosya sunucu görseli\"],\"rIPR2O\":[\"Şu kadar dakika önce önce konu belirlenen\"],\"rMMSYo\":[\"Maksimum uzunluk \",[\"0\"]],\"rWtzQe\":[\"Ağ bölündü ve yeniden birleşti. ✅\"],\"rYG2u6\":[\"Lütfen bekleyin...\"],\"rdUucN\":[\"Önizleme\"],\"rjGI/Q\":[\"Gizlilik\"],\"rk8iDX\":[\"GIF'ler yükleniyor...\"],\"rn6SBY\":[\"Sesi Aç\"],\"s/UKqq\":[\"Kanaldan atıldı\"],\"s7oqXR\":[\"Bir ağ seçin\"],\"s8cATI\":[[\"channelName\"],\" kanalına katıldı\"],\"sCO9ue\":[\"<0>\",[\"serverName\"],\"0> bağlantısında aşağıdaki güvenlik endişeleri bulunmaktadır:\"],\"sGH11W\":[\"Sunucu\"],\"sHI1H+\":[\"artık **\",[\"newNick\"],\"** olarak bilinmektedir\"],\"sJyV04\":[[\"inviter\"],\", sizi \",[\"channel\"],\" kanalına davet etti\"],\"sUBSbK\":[\"Henüz yukarı akış ağı yok.\"],\"sby+1/\":[\"Kopyalamak için tıklayın\"],\"sfN25C\":[\"Gerçek veya tam adınız\"],\"sliuzR\":[\"Bağlantıyı Aç\"],\"sqrO9R\":[\"Özel Bahsediler\"],\"sr6RdJ\":[\"Shift+Enter ile çok satır\"],\"swrCpB\":[\"Kanal, \",[\"user\"],\" tarafından \",[\"oldName\"],\" yerine \",[\"newName\"],\" olarak yeniden adlandırıldı\",[\"0\"]],\"sxkWRg\":[\"Gelişmiş\"],\"t/YqKh\":[\"Kaldır\"],\"t47eHD\":[\"Bu sunucudaki benzersiz tanımlayıcınız\"],\"tAkAh0\":[\"Dinamik boyutlandırma için isteğe bağlı \",[\"size\"],\" değişkeni içeren URL. Örnek: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Kanal listesi kenar çubuğunu göster veya gizle\"],\"tfDRzk\":[\"Kaydet\"],\"tiBsJk\":[[\"channelName\"],\" kanalından ayrıldı\"],\"tt4/UD\":[\"ayrıldı (\",[\"reason\"],\")\"],\"u0TcnO\":[\"{nick} takma adı zaten kullanımda, {newNick} ile yeniden deneniyor\"],\"u0a8B4\":[\"Yönetici erişimi için IRC Operatörü olarak kimlik doğrula\"],\"u0rWFU\":[\"Şu kadar dakika önce sonra oluşturulan\"],\"u72w3t\":[\"Engellenecek kullanıcılar ve desenler\"],\"u7jc2L\":[\"ayrıldı\"],\"uAQUqI\":[\"Durum\"],\"uB85T3\":[\"Kaydetme başarısız: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC Sunucuları:\"],\"ukyW4o\":[\"Davet baÄlantılarınız\"],\"usSSr/\":[\"Yakınlaştırma seviyesi\"],\"v7uvcf\":[\"Yazılım:\"],\"vE8kb+\":[\"Yeni satır için Shift+Enter kullanın (Enter gönderir)\"],\"vERlcd\":[\"Profil\"],\"vK0RL8\":[\"Konu yok\"],\"vSJd18\":[\"Video\"],\"vXIe7J\":[\"Dil\"],\"vaHYxN\":[\"Gerçek Ad\"],\"vhjbKr\":[\"Uzakta\"],\"w/nogd\":[[\"0\"],\" ağ\",[\"1\"],\" — katılmak için birini seçin\"],\"w4NYox\":[[\"title\"],\" istemcisi\"],\"w8xQRx\":[\"Geçersiz değer\"],\"wFjjxZ\":[[\"channelName\"],\" kanalından \",[\"username\"],\" tarafından atıldı (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Yasak istisnası bulunamadı\"],\"wPrGnM\":[\"Kanal Yöneticisi\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Kullanıcılar kanala katıldığında veya ayrıldığında göster\"],\"whqZ9r\":[\"Vurgulanacak ek kelimeler veya ifadeler\"],\"wm7RV4\":[\"Bildirim Sesi\"],\"wz/Yoq\":[\"Mesajlarınız sunucular arasında iletilirken ele geçirilebilir\"],\"x3+y8b\":[\"Bu baÄlantı aracılıÄıyla kayıt olan kiÅi sayısı\"],\"xCJdfg\":[\"Temizle\"],\"xOTzt5\":[\"az önce\"],\"xUHRTR\":[\"Bağlanırken otomatik olarak operatör kimliği doğrula\"],\"xWHwwQ\":[\"Yasaklar\"],\"xYilR2\":[\"Medya\"],\"xbi8D6\":[\"Bu sunucu davet baÄlantılarını desteklemiyor (<0>obby.world/invitation0> yeteneÄi duyurulmuyor). Yine de normal Åekilde sohbet edebilirsiniz; bu panel obbyircd tabanlı aÄlar içindir.\"],\"xceQrO\":[\"Yalnızca güvenli websocket'ler desteklenmektedir\"],\"xdtXa+\":[\"kanal-adı\"],\"xfXC7q\":[\"Metin Kanalları\"],\"xlCYOE\":[\"Daha fazla mesaj alınıyor...\"],\"xlhswE\":[\"Minimum değer \",[\"0\"]],\"xq97Ci\":[\"Kelime veya ifade ekle...\"],\"xuRqRq\":[\"İstemci Sınırı (+l)\"],\"xwF+7J\":[[\"0\"],\" yazıyor...\"],\"y1eoq1\":[\"BaÄlantıyı kopyala\"],\"yJztBY\":[\"Ağı sil\"],\"yNeucF\":[\"Bu sunucu genişletilmiş profil meta verilerini (IRCv3 METADATA uzantısı) desteklemiyor. Avatar, görünen ad ve durum gibi ek alanlar mevcut değil.\"],\"yPlrca\":[\"Kanal Avatarı\"],\"yQE2r9\":[\"Yükleniyor\"],\"ySU+JY\":[\"eposta@adresiniz.com\"],\"yTX1Rt\":[\"Oper Kullanıcı Adı\"],\"yYOzWD\":[\"günlükler\"],\"yfx9Re\":[\"IRC operatör şifresi\"],\"ygCKqB\":[\"Durdur\"],\"ymDxJx\":[\"IRC operatör kullanıcı adı\"],\"yrpRsQ\":[\"Ada Göre Sırala\"],\"yz7wBu\":[\"Kapat\"],\"zJw+jA\":[\"modu ayarlar: \",[\"0\"]],\"zbymaY\":[[\"0\"],\"dk önce\"],\"zebeLu\":[\"Oper kullanıcı adını girin\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/tr/messages.po b/src/locales/tr/messages.po
index 6f22e304..fac7eb57 100644
--- a/src/locales/tr/messages.po
+++ b/src/locales/tr/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - IRC'yi geleceğe taşıyor"
msgid "— open in viewer"
msgstr "— görüntüleyicide aç"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(devral)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(değiştirilmedi)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} ve {1} yazıyor..."
msgid "{0} is typing..."
msgstr "{0} yazıyor..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} ağ{1} — katılmak için birini seçin"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -69,17 +85,17 @@ msgstr "{0}, {1}, {2} ve {3} kişi daha yazıyor..."
#. placeholder {0}: Math.floor(secs / 86400)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}d ago"
-msgstr "{0}g önce"
+msgstr "{0}g önce"
#. placeholder {0}: Math.floor(secs / 3600)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}h ago"
-msgstr "{0}s önce"
+msgstr "{0}s önce"
#. placeholder {0}: Math.floor(secs / 60)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}m ago"
-msgstr "{0}dk önce"
+msgstr "{0}dk önce"
#: src/lib/eventGrouping.ts
msgid "{c, plural, one {1 time} other {{c} times}}"
@@ -115,7 +131,7 @@ msgstr "*spam*"
#: src/components/ui/InvitationsPanel.tsx
msgid "#channel (optional)"
-msgstr "#kanal (isteğe bağlı)"
+msgstr "#kanal (isteÄe baÄlı)"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "#new-channel-name"
@@ -205,6 +221,12 @@ msgstr "Davet maskesi ekle (örn. nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "IRC Sunucusu Ekle"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Ağ Ekle"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Kural Ekle"
msgid "Add Server"
msgstr "Sunucu Ekle"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "İlk ağınızı ekleyin"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Ek ayrıntılar"
@@ -377,6 +403,10 @@ msgstr "Geri"
msgid "Back to image"
msgstr "Görüntüye geri dön"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Ağ listesine geri dön"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "{username} kullanıcısını host maskesiyle yasakla (aynı IP/host'tan yeniden katılmasını engeller)"
@@ -424,6 +454,10 @@ msgstr "Sunucudaki tüm kanalları görüntüle"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Bağlantıyı İptal Et"
msgid "Cancel reply"
msgstr "Yanıtı iptal et"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Vurgu rengini değiştir"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Kanal adını değiştir (yalnızca operatörler)"
@@ -564,7 +603,7 @@ msgstr "Aramayı temizle"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "Onaylamak için tekrar tıklayın"
+msgstr "Onaylamak için tekrar tıklayın"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "İstemci Sınırı (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -666,9 +707,10 @@ msgstr "Bildirim seslerini ve vurguları yapılandır"
#: src/components/ui/InvitationsPanel.tsx
msgid "Confirm?"
-msgstr "Onaylıyor musunuz?"
+msgstr "Onaylıyor musunuz?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Bağlan"
@@ -715,7 +757,7 @@ msgstr "Kopyala"
#: src/components/ui/RawLogViewer.tsx
msgid "Copy all"
-msgstr "Tümünü kopyala"
+msgstr "Tümünü kopyala"
#: src/components/message/JsonLogMessage.tsx
msgid "Copy entire JSON"
@@ -731,7 +773,7 @@ msgstr "JSON kopyala"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy link"
-msgstr "Bağlantıyı kopyala"
+msgstr "BaÄlantıyı kopyala"
#: src/components/ui/ExternalLinkWarningModal.tsx
msgid "Copy URL"
@@ -739,15 +781,15 @@ msgstr "URL'yi Kopyala"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create"
-msgstr "Oluştur"
+msgstr "OluÅtur"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "Yeni bir davet bağlantısı oluştur"
+msgstr "Yeni bir davet baÄlantısı oluÅtur"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "Davet bağlantılarınızı oluşturun ve yönetin"
+msgstr "Davet baÄlantılarınızı oluÅturun ve yönetin"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,6 +856,10 @@ msgstr "Kanalı Sil"
msgid "Delete message"
msgstr "Mesajı sil"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Ağı sil"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Özel Sohbeti Sil"
@@ -826,15 +872,36 @@ msgstr "Bu daveti sil"
msgid "Delete this message? This cannot be undone."
msgstr "Bu mesaj silinsin mi? Bu işlem geri alınamaz."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Bu ağ silinsin mi?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "Açıklama (isteğe bağlı, örn. \"Beta test ekibi Ç3\")"
+msgstr "Açıklama (isteÄe baÄlı, örn. \"Beta test ekibi Ã3\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Bağlantıyı Kes"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "<0>{0}0> bağlantısı kesilsin mi?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "soju bouncer bağlantısı kesilsin mi?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Ağ bağlantısı kesilsin mi?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Keşfet"
@@ -891,20 +958,31 @@ msgstr "İndir"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "Yüklemek için dosyaları bırakın"
+msgstr "Yüklemek için dosyaları bırakın"
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "örn. <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "örn. 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Düzenle"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "{0} ögesini düzenle"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Profili Düzenle"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "ANA SAYFA"
msgid "Homepage"
msgstr "Ana Sayfa"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Host"
@@ -1285,7 +1364,7 @@ msgstr "Kanala katıldı"
#: src/components/ui/InvitationsPanel.tsx
msgid "just now"
-msgstr "az önce"
+msgstr "az önce"
#: src/components/ui/ModerationModal.tsx
#: src/components/ui/UserContextMenu.tsx
@@ -1323,7 +1402,7 @@ msgstr "Kanaldan ayrıl"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Genel bir ağ daveti için kanal alanını boş bırakın. Açıklama yalnızca sizin kayıtlarınız içindir — bu listede yalnızca sizin görebileceğiniz şekilde gösterilir."
+msgstr "Genel bir aÄ daveti için kanal alanını boÅ bırakın. Açıklama yalnızca sizin kayıtlarınız içindir â bu listede yalnızca sizin görebileceÄiniz Åekilde gösterilir."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "Kanaldan ayrıldı"
msgid "Let others know when you are typing"
msgstr "Yazarken diğerlerini bilgilendir"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Bağlantı önizlemesi"
@@ -1375,6 +1458,10 @@ msgstr "GIF'ler yükleniyor..."
msgid "Loading more channels..."
msgstr "Daha fazla kanal yükleniyor..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Bouncer'ınızdan ağlar yükleniyor…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "WHOIS verisi yükleniyor..."
@@ -1563,12 +1650,23 @@ msgstr "Ad:"
#: src/components/ui/InvitationsPanel.tsx
msgid "network"
-msgstr "ağ"
+msgstr "aÄ"
+
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "soju bouncer üzerinden bağlı ağ"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Ağ Adı"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "{0} üzerindeki ağlar"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Yeni DM"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (örn. spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Dosya seçilmedi"
msgid "No flood profile"
msgstr "Flood profili yok"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "ana makine ayarlanmadı"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Davet bulunamadı"
@@ -1672,7 +1775,7 @@ msgstr "Medya önizlemesi yüklenmiyor."
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "Henüz ham IRC trafiği yakalanmadı. Bağlanmayı veya bir mesaj göndermeyi deneyin."
+msgstr "Henüz ham IRC trafiÄi yakalanmadı. BaÄlanmayı veya bir mesaj göndermeyi deneyin."
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "Sonuç bulunamadı"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "Hiçbir sunucu seçilmedi. Önce kenar çubuğundan bir sunucu seçin; davet bağlantıları sunucu başına yönetilir."
+msgstr "Hiçbir sunucu seçilmedi. Ãnce kenar çubuÄundan bir sunucu seçin; davet baÄlantıları sunucu baÅına yönetilir."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "Konu belirlenmemiş"
msgid "No unread mentions or messages"
msgstr "Okunmamış bahis veya mesaj yok"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Henüz yukarı akış ağı yok."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Kullanılabilir kullanıcı yok"
@@ -1784,6 +1891,10 @@ msgstr "Eyvah! Ağ bölündü! ⚠️"
msgid "Op"
msgstr "Op"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Aç"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Kanal yapılandırma ayarlarını aç"
@@ -1887,6 +1998,10 @@ msgstr "Özel Sohbeti Sabitle"
msgid "Pin this private message conversation"
msgstr "Bu özel mesaj konuşmasını sabitle"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Düz metin"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "Kullanıcıya PM Gönder"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Port"
@@ -2006,6 +2122,7 @@ msgstr "bu mesaja tepki verdi"
msgid "Read more"
msgstr "Devamını oku"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Neden"
msgid "Reason (optional)"
msgstr "Neden (isteğe bağlı)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Sunucuya yeniden bağlan"
@@ -2095,6 +2213,7 @@ msgstr "Kurallar"
msgid "Safe"
msgstr "Güvenli"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Konuma git"
msgid "Select a channel"
msgstr "Bir kanal seçin"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Bir ağ seçin"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Üye Seç"
@@ -2276,6 +2399,10 @@ msgstr "Ağdaki sunucu operatörleri mesajlarınızı okuyabilir"
msgid "Server Password"
msgstr "Sunucu Şifresi"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Sunucu Şifresi (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Sunucular arası iletişim şifrelenmemiş bağlantılar kullanıyor olabilir"
@@ -2380,6 +2507,12 @@ msgstr "Giriş Yapıldı"
msgid "Software:"
msgstr "Yazılım:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "soju bouncer (kontrol)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Ada Göre Sırala"
@@ -2457,7 +2590,11 @@ msgstr "Bu görüntünün süresi doldu"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "Bu bağlantı aracılığıyla kayıt olan kişi sayısı"
+msgstr "Bu baÄlantı aracılıÄıyla kayıt olan kiÅi sayısı"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Bu, ağı soju bouncer'ınızdan kaldırır. Tekrar kullanmak için yeniden eklemeniz gerekir."
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "Bu sunucu genişletilmiş profil meta verilerini (IRCv3 METADATA uzantı
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Bu sunucu davet bağlantılarını desteklemiyor (<0>obby.world/invitation0> yeteneği duyurulmuyor). Yine de normal şekilde sohbet edebilirsiniz; bu panel obbyircd tabanlı ağlar içindir."
+msgstr "Bu sunucu davet baÄlantılarını desteklemiyor (<0>obby.world/invitation0> yeteneÄi duyurulmuyor). Yine de normal Åekilde sohbet edebilirsiniz; bu panel obbyircd tabanlı aÄlar içindir."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Bu sunucu yalnızca bir bağlantı türünü destekliyor"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Bu, aşağıdaki bağlı {0} ağı da kapatacak."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Bu, aşağıdaki bağlı ağı da kapatacak."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Süre (dk)"
@@ -2479,6 +2625,10 @@ msgstr "Süre (dk)"
msgid "Time Window (seconds)"
msgstr "Zaman Penceresi (saniye)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Konu:"
msgid "Total: {0}"
msgstr "Toplam: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Taşıma"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Güvenilir Kaynaklar"
@@ -2615,7 +2769,7 @@ msgstr "Joker karakter kullanın: * herhangi bir diziyle eşleşir, ? herhangi b
#: src/components/ui/InvitationsPanel.tsx
msgid "used"
-msgstr "kullanıldı"
+msgstr "kullanıldı"
#: src/components/message/JsonLogMessage.tsx
#: src/components/ui/UserProfileModal.tsx
@@ -2641,6 +2795,7 @@ msgstr "Kullanıcı Profili"
msgid "User Settings"
msgstr "Kullanıcı Ayarları"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Geniş - Daha kapsamlı koruma alanı"
msgid "Will default to 'no reason' if left empty"
msgstr "Boş bırakılırsa varsayılan olarak 'neden yok' kullanılır"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Evet, sil"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "Kaydedilmemiş değişiklikleriniz var. Kaydetmeden kapatmak istediğini
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "Henüz hiç davet bağlantısı oluşturmadınız. İlkini oluşturmak için yukarıdaki formu kullanın."
+msgstr "Henüz hiç davet baÄlantısı oluÅturmadınız. İlkini oluÅturmak için yukarıdaki formu kullanın."
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "{target} kişisini {channel} kanalına davet ettiniz"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "<0>{0}0> sunucusuna bağlısınız."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Kimlik doğrulama için hesap şifreniz"
@@ -2822,13 +2986,17 @@ msgstr "Kimlik doğrulama için hesap şifreniz"
msgid "Your account username for authentication"
msgstr "Kimlik doğrulama için hesap kullanıcı adınız"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Bouncer'ınızda henüz ağ yok. Başlamak için bir tane ekleyin."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Tüm sunucular için varsayılan takma adınız"
#: src/components/ui/InvitationsPanel.tsx
msgid "Your invite links"
-msgstr "Davet bağlantılarınız"
+msgstr "Davet baÄlantılarınız"
#: src/components/ui/UserSettings.tsx
msgid "Your messages and settings are stored locally on your device"
diff --git a/src/locales/uk/messages.mjs b/src/locales/uk/messages.mjs
index 6bc57b7d..cd5f6915 100644
--- a/src/locales/uk/messages.mjs
+++ b/src/locales/uk/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Недійсний формат шаблону. Використовуйте формат nick!user@host (дозволені символи * як шаблони)\"],\"+6NQQA\":[\"Загальний канал підтримки\"],\"+6NyRG\":[\"Клієнт\"],\"+K0AvT\":[\"Від'єднатися\"],\"+cyFdH\":[\"Стандартне повідомлення при позначенні себе відсутнім\"],\"+mVPqU\":[\"Відображати Markdown-форматування у повідомленнях\"],\"+vqCJH\":[\"Ім'я користувача вашого акаунту для автентифікації\"],\"+yPBXI\":[\"Вибрати файл\"],\"+zy2Nq\":[\"Тип\"],\"/09cao\":[\"Низький рівень безпеки з'єднання (Рівень \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Користувачі за межами каналу не можуть надсилати до нього повідомлення\"],\"/4C8U0\":[\"Копіювати все\"],\"/6BzZF\":[\"Перемкнути список учасників\"],\"/AkXyp\":[\"Підтвердити?\"],\"/TNOPk\":[\"Користувач відсутній\"],\"/XQgft\":[\"Дослідити\"],\"/cF7Rs\":[\"Гучність\"],\"/dqduX\":[\"Наступна сторінка\"],\"/fc3q4\":[\"Весь вміст\"],\"/kISDh\":[\"Увімкнути звуки сповіщень\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Аудіо\"],\"/rfkZe\":[\"Відтворювати звуки для згадок і повідомлень\"],\"0/0ZGA\":[\"Маска назви каналу\"],\"0D6j7U\":[\"Дізнатися більше про власні правила →\"],\"0XsHcR\":[\"Вигнати користувача\"],\"0ZpE//\":[\"Сортувати за кількістю користувачів\"],\"0bEPwz\":[\"Встановити статус «Відсутній»\"],\"0dGkPt\":[\"Розгорнути список каналів\"],\"0gS7M5\":[\"Відображуване ім'я\"],\"0kS+M8\":[\"ПрикладМЕРЕЖА\"],\"0rgoY7\":[\"Підключатися лише до обраних вами серверів\"],\"0wdd7X\":[\"Приєднатися\"],\"0wkVYx\":[\"Приватні повідомлення\"],\"111uHX\":[\"Попередній перегляд посилання\"],\"196EG4\":[\"Видалити приватний чат\"],\"1DSr1i\":[\"Зареєструвати акаунт\"],\"1O/24y\":[\"Перемкнути список каналів\"],\"1VPJJ2\":[\"Попередження про зовнішнє посилання\"],\"1ZC/dv\":[\"Немає непрочитаних згадувань або повідомлень\"],\"1pO1zi\":[\"Назва сервера обов'язкова\"],\"1uwfzQ\":[\"Переглянути тему каналу\"],\"268g7c\":[\"Введіть відображуване ім'я\"],\"2F9+AZ\":[\"Поки що не зафіксовано необробленого IRC-трафіку. Спробуйте підключитися або надіслати повідомлення.\"],\"2FOFq1\":[\"Оператори сервера в мережі можуть потенційно читати ваші повідомлення\"],\"2FYpfJ\":[\"Більше\"],\"2HF1Y2\":[[\"inviter\"],\" запросив \",[\"target\"],\" приєднатися до \",[\"channel\"]],\"2I70QL\":[\"Переглянути інформацію профілю користувача\"],\"2QYdmE\":[\"Користувачі:\"],\"2QpEjG\":[\"вийшов\"],\"2YE223\":[\"Повідомлення #\",[\"0\"],\" (Enter для нового рядка, Shift+Enter для відправки)\"],\"2bimFY\":[\"Використовувати пароль сервера\"],\"2iTmdZ\":[\"Локальне сховище:\"],\"2odkwe\":[\"Строгий - більш агресивний захист\"],\"2uDhbA\":[\"Введіть ім'я користувача для запрошення\"],\"2ygf/L\":[\"← Назад\"],\"2zEgxj\":[\"Пошук GIF...\"],\"3RdPhl\":[\"Перейменувати канал\"],\"3THokf\":[\"Користувач з голосом\"],\"3TSz9S\":[\"Згорнути\"],\"3jBDvM\":[\"Відображувана назва каналу\"],\"3ryuFU\":[\"Необов'язкові звіти про збої для покращення додатку\"],\"3uBF/8\":[\"Закрити переглядач\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Введіть ім'я акаунту...\"],\"4/Rr0R\":[\"Запросити користувача до поточного каналу\"],\"4EZrJN\":[\"Правила\"],\"4JJtW9\":[\"#переповнення\"],\"4NqeT4\":[\"Профіль флуду (+F)\"],\"4RZQRK\":[\"Що ти зараз робиш?\"],\"4hfTrB\":[\"Псевдонім\"],\"4n99LO\":[\"Вже в \",[\"0\"]],\"4t6vMV\":[\"Автоматично перемикатися на один рядок для коротких повідомлень\"],\"4vsHmf\":[\"Час (хв)\"],\"5+INAX\":[\"Виділяти повідомлення, що вас згадують\"],\"5R5Pv/\":[\"Ім'я оператора\"],\"678PKt\":[\"Назва мережі\"],\"6Aih4U\":[\"Офлайн\"],\"6CO3WE\":[\"Пароль для входу до каналу. Залиште порожнім для видалення ключа.\"],\"6HhMs3\":[\"Повідомлення про вихід\"],\"6V3Ea3\":[\"Скопійовано\"],\"6lGV3K\":[\"Показати менше\"],\"6yFOEi\":[\"Введіть пароль оператора...\"],\"7+IHTZ\":[\"Файл не вибрано\"],\"73hrRi\":[\"nick!user@host (напр., spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Надіслати приватне повідомлення\"],\"7U1W7c\":[\"Дуже розслаблений\"],\"7Y1YQj\":[\"Справжнє ім'я:\"],\"7YHArF\":[\"— відкрити у переглядачі\"],\"7fjnVl\":[\"Пошук користувачів...\"],\"7jL88x\":[\"Видалити це повідомлення? Цю дію неможливо скасувати.\"],\"7nGhhM\":[\"Що у вас на думці?\"],\"7sEpu1\":[\"Учасники — \",[\"0\"]],\"7sNhEz\":[\"Ім'я користувача\"],\"8H0Q+x\":[\"Дізнатися більше про профілі →\"],\"8Phu0A\":[\"Відображати, коли користувачі змінюють псевдонім\"],\"8XTG9e\":[\"Введіть пароль оператора\"],\"8XsV2J\":[\"Повторити надсилання\"],\"8ZsakT\":[\"Пароль\"],\"8kR84m\":[\"Ви збираєтеся відкрити зовнішнє посилання:\"],\"8lCgih\":[\"Видалити правило\"],\"8o3dPc\":[\"Перетягніть файли для завантаження\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"приєднався\"],\"few\":[\"приєднався \",[\"joinCount\"],\" рази\"],\"many\":[\"приєднався \",[\"joinCount\"],\" разів\"],\"other\":[\"приєднався \",[\"joinCount\"],\" рази\"]}]],\"9BMLnJ\":[\"Перепідключитися до сервера\"],\"9OEgyT\":[\"Додати реакцію\"],\"9PQ8m2\":[\"G-Line (глобальний бан)\"],\"9Qs99X\":[\"Електронна пошта:\"],\"9QupBP\":[\"Видалити шаблон\"],\"9bG48P\":[\"Надсилання\"],\"9f5f0u\":[\"Питання про конфіденційність? Зв'яжіться з нами:\"],\"9unqs3\":[\"Відсутність:\"],\"9v3hwv\":[\"Сервери не знайдено.\"],\"9zb2WA\":[\"Підключення\"],\"A1taO8\":[\"Пошук\"],\"A2adVi\":[\"Надсилати сповіщення про введення\"],\"A9Rhec\":[\"Назва каналу\"],\"AWOSPo\":[\"Збільшити\"],\"AXSpEQ\":[\"Оператор при підключенні\"],\"AeXO77\":[\"Акаунт\"],\"AhNP40\":[\"Перемотати\"],\"Ai2U7L\":[\"Хост\"],\"AjBQnf\":[\"Змінено псевдонім\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Скасувати відповідь\"],\"ApSx0O\":[\"Знайдено \",[\"0\"],\" повідомлень, що відповідають \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Результатів не знайдено\"],\"AyNqAB\":[\"Відображати всі події сервера в чаті\"],\"B/QqGw\":[\"Відійшов від клавіатури\"],\"B8AaMI\":[\"Це поле обов'язкове\"],\"BA2c49\":[\"Сервер не підтримує розширену фільтрацію LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" та ще \",[\"3\"],\" пишуть...\"],\"BGul2A\":[\"У вас є незбережені зміни. Ви впевнені, що хочете закрити без збереження?\"],\"BIf9fi\":[\"Ваше статусне повідомлення\"],\"BPm98R\":[\"Жоден сервер не вибрано. Спочатку оберіть сервер з бічної панелі; запрошувальні посилання керуються окремо для кожного сервера.\"],\"BZz3md\":[\"Ваш особистий веб-сайт\"],\"Bgm/H7\":[\"Дозволити введення кількох рядків тексту\"],\"BiQIl1\":[\"Закріпити цю приватну розмову\"],\"BlNZZ2\":[\"Натисніть, щоб перейти до повідомлення\"],\"Bowq3c\":[\"Тільки оператори можуть змінювати тему каналу\"],\"Btozzp\":[\"Термін дії цього зображення минув\"],\"Bycfjm\":[\"Всього: \",[\"0\"]],\"C6IBQc\":[\"Скопіювати весь JSON\"],\"C9L9wL\":[\"Збір даних\"],\"CDq4wC\":[\"Помірний режим для користувача\"],\"CHVRxG\":[\"Повідомлення @\",[\"0\"],\" (Shift+Enter для нового рядка)\"],\"CN9zdR\":[\"Ім'я та пароль оператора обов'язкові\"],\"CW3sYa\":[\"Додати реакцію \",[\"emoji\"]],\"CaAkqd\":[\"Показати виходи\"],\"CbvaYj\":[\"Блокування за псевдонімом\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Вибрати канал\"],\"CsekCi\":[\"Нормальний\"],\"D+NlUC\":[\"Система\"],\"D28t6+\":[\"приєднався та вийшов\"],\"DB8zMK\":[\"Застосувати\"],\"DBcWHr\":[\"Власний файл звуку сповіщення\"],\"DTy9Xw\":[\"Попередній перегляд медіа\"],\"Dj4pSr\":[\"Виберіть надійний пароль\"],\"Du+zn+\":[\"Пошук...\"],\"Du2T2f\":[\"Налаштування не знайдено\"],\"DwsSVQ\":[\"Застосувати фільтри та оновити\"],\"E3W/zd\":[\"Стандартний псевдонім\"],\"E6nRW7\":[\"Копіювати URL\"],\"E703RG\":[\"Режими:\"],\"EAeu1Z\":[\"Надіслати запрошення\"],\"EFKJQT\":[\"Налаштування\"],\"EGPQBv\":[\"Власні правила флуду (+f)\"],\"ELik0r\":[\"Переглянути повну політику конфіденційності\"],\"EPbeC2\":[\"Переглянути або редагувати тему каналу\"],\"EQCDNT\":[\"Введіть ім'я користувача оператора...\"],\"EUvulZ\":[\"Знайдено 1 повідомлення, що відповідає \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Наступне зображення\"],\"EdQY6l\":[\"Немає\"],\"EnqLYU\":[\"Пошук серверів...\"],\"F0OKMc\":[\"Редагувати сервер\"],\"F6Int2\":[\"Увімкнути виділення\"],\"FDoLyE\":[\"Макс. користувачів\"],\"FUU/hZ\":[\"Контролюйте, скільки зовнішніх медіа завантажується у чаті.\"],\"Fdp03t\":[\"увімк\"],\"FfPWR0\":[\"Модальне вікно\"],\"FjkaiT\":[\"Зменшити\"],\"FlqOE9\":[\"Що це означає:\"],\"FolHNl\":[\"Керуйте своїм обліковим записом та автентифікацією\"],\"Fp2Dif\":[\"Вийти з сервера\"],\"G5KmCc\":[\"GZ-Line (глобальна Z-Line)\"],\"GDs0lz\":[\"<0>Ризик:0> Конфіденційна інформація (повідомлення, приватні розмови, дані автентифікації) може бути доступна мережевим адміністраторам або зловмисникам між IRC-серверами.\"],\"GR+2I3\":[\"Додати маску запрошення (напр., nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Закрити виспливаючі сповіщення сервера\"],\"GdhD7H\":[\"Натисніть ще раз для підтвердження\"],\"GlHnXw\":[\"Зміна псевдоніму не вдалась: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Попередній перегляд:\"],\"GtmO8/\":[\"від\"],\"GtuHUQ\":[\"Перейменувати цей канал на сервері. Усі користувачі побачать нову назву.\"],\"GuGfFX\":[\"Перемкнути пошук\"],\"GxkJXS\":[\"Завантаження...\"],\"GzbwnK\":[\"Приєднався до каналу\"],\"GzsUDB\":[\"Розширений профіль\"],\"H/PnT8\":[\"Вставити емодзі\"],\"H6Izzl\":[\"Ваш бажаний код кольору\"],\"H9jIv+\":[\"Показати входи/виходи\"],\"HAKBY9\":[\"Завантажити файли\"],\"HdE1If\":[\"Канал\"],\"Hk4AW9\":[\"Ваше бажане відображуване ім'я\"],\"HmHDk7\":[\"Вибрати учасника\"],\"HrQzPU\":[\"Канали на \",[\"networkName\"]],\"I2tXQ5\":[\"Повідомлення @\",[\"0\"],\" (Enter для нового рядка, Shift+Enter для відправки)\"],\"I6bw/h\":[\"Заблокувати користувача\"],\"I92Z+b\":[\"Увімкнути сповіщення\"],\"I9D72S\":[\"Ви впевнені, що хочете видалити це повідомлення? Цю дію неможливо скасувати.\"],\"IA+1wo\":[\"Відображати, коли користувачів виганяють з каналів\"],\"IDwkJx\":[\"IRC оператор\"],\"ILlU+s\":[\"Інфо:\"],\"IUwGEM\":[\"Зберегти зміни\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" та \",[\"2\"],\" пишуть...\"],\"IgrLD/\":[\"Пауза\"],\"Im6JED\":[\"ШЕПІТ\"],\"ImOQa9\":[\"Відповісти\"],\"IoHMnl\":[\"Максимальне значення: \",[\"0\"]],\"IvMj+0\":[\"Оп\"],\"J28zul\":[\"Підключення...\"],\"J5T9NW\":[\"Інформація про користувача\"],\"J8Y5+z\":[\"Ой! Розрив мережі! ⚠️\"],\"JBHkBA\":[\"Покинув канал\"],\"JCwL0Q\":[\"Введіть причину (необов'язково)\"],\"JFciKP\":[\"Перемкнути\"],\"JXGkhG\":[\"Змінити назву каналу (лише оператори)\"],\"JcD7qf\":[\"Більше дій\"],\"JdkA+c\":[\"Секретний (+s)\"],\"Jmu12l\":[\"Канали сервера\"],\"JvQ++s\":[\"Увімкнути Markdown\"],\"K2jwh/\":[\"Дані WHOIS недоступні\"],\"KAXSwC\":[\"Голос\"],\"KDfTdX\":[\"Видалити повідомлення\"],\"KKBlUU\":[\"Вбудувати\"],\"KM0pLb\":[\"Ласкаво просимо до каналу!\"],\"KR6W2h\":[\"Скасувати ігнорування\"],\"KV+Bi1\":[\"Тільки за запрошенням (+i)\"],\"KdCtwE\":[\"Скільки секунд відстежувати активність флуду перед скиданням лічильників\"],\"Kkezga\":[\"Пароль сервера\"],\"KsiQ/8\":[\"Користувачі повинні бути запрошені для входу до каналу\"],\"L+gB/D\":[\"Інформація про канал\"],\"LC1a7n\":[\"IRC-сервер повідомив, що зв'язки між серверами мають низький рівень безпеки. Це означає, що коли ваші повідомлення передаються між IRC-серверами в мережі, вони можуть бути не належним чином зашифровані або SSL/TLS-сертифікати можуть не перевірятися правильно.\"],\"LNfLR5\":[\"Показати виключення\"],\"LQb0W/\":[\"Показати всі події\"],\"LU7/yA\":[\"Альтернативна назва для відображення в інтерфейсі. Може містити пробіли, емодзі та спеціальні символи. Справжня назва каналу (\",[\"channelName\"],\") використовуватиметься для IRC-команд.\"],\"LUb9O7\":[\"Потрібен дійсний порт сервера\"],\"LV4fT6\":[\"Опис (необов'язково, напр. \\\"Бета-тестувальники Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Політика конфіденційності\"],\"LcuSDR\":[\"Керуйте інформацією профілю та метаданими\"],\"LqLS9B\":[\"Показати зміни псевдоніма\"],\"LsDQt2\":[\"Налаштування каналу\"],\"LtI9AS\":[\"Власник\"],\"LuNhhL\":[\"відреагував на це повідомлення\"],\"M/AZNG\":[\"URL зображення вашого аватара\"],\"M/WIer\":[\"Надіслати повідомлення\"],\"M8er/5\":[\"Ім'я:\"],\"MHk+7g\":[\"Попереднє зображення\"],\"MRorGe\":[\"ПП користувачу\"],\"MVbSGP\":[\"Часове вікно (секунди)\"],\"MkpcsT\":[\"Ваші повідомлення та налаштування зберігаються локально на вашому пристрої\"],\"N/hDSy\":[\"Позначити як бот - зазвичай 'on' або порожньо\"],\"N7TQbE\":[\"Запросити користувача до \",[\"channelName\"]],\"NCca/o\":[\"Введіть стандартний нік...\"],\"Nqs6B9\":[\"Показує всі зовнішні медіа. Будь-яка URL може спричинити запит до невідомого сервера.\"],\"Nt+9O7\":[\"Використовувати WebSocket замість звичайного TCP\"],\"NxIHzc\":[\"Відключити користувача\"],\"O+v/cL\":[\"Переглянути всі канали на сервері\"],\"ODwSCk\":[\"Надіслати GIF\"],\"OGQ5kK\":[\"Налаштувати звуки сповіщень та виділення\"],\"OIPt1Z\":[\"Показати або приховати бічну панель зі списком учасників\"],\"OKSNq/\":[\"Дуже строгий\"],\"ONWvwQ\":[\"Завантажити\"],\"OVKoQO\":[\"Пароль вашого акаунту для автентифікації\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Привносимо IRC у майбутнє\"],\"OhCpra\":[\"Встановити тему…\"],\"OkltoQ\":[\"Заблокувати \",[\"username\"],\" за псевдонімом (запобігає повторному входу з тим же ніком)\"],\"P+t/Te\":[\"Немає додаткових даних\"],\"P42Wcc\":[\"Безпечно\"],\"PD38l0\":[\"Попередній перегляд аватара каналу\"],\"PD9mEt\":[\"Введіть повідомлення...\"],\"PPqfdA\":[\"Відкрити налаштування конфігурації каналу\"],\"PSCjfZ\":[\"Тема, яка відображатиметься для цього каналу. Усі користувачі можуть бачити тему.\"],\"PZCecv\":[\"Попередній перегляд PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 раз\"],\"few\":[[\"c\"],\" рази\"],\"many\":[[\"c\"],\" разів\"],\"other\":[[\"c\"],\" рази\"]}]],\"PguS2C\":[\"Додати маску винятку (напр., nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Показано \",[\"displayedChannelsCount\"],\" із \",[\"0\"],\" каналів\"],\"PqhVlJ\":[\"Заблокувати користувача (за маскою хоста)\"],\"Q+chwU\":[\"Ім'я користувача:\"],\"Q2QY4/\":[\"Видалити це запрошення\"],\"Q6hhn8\":[\"Налаштування\"],\"QF4a34\":[\"Будь ласка, введіть ім'я користувача\"],\"QGqSZ2\":[\"Колір і форматування\"],\"QJQd1J\":[\"Редагувати профіль\"],\"QSzGDE\":[\"Бездіяльний\"],\"QUlny5\":[\"Ласкаво просимо до \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Читати далі\"],\"QuSkCF\":[\"Фільтрувати канали...\"],\"QwUrDZ\":[\"змінив тему на: \",[\"topic\"]],\"R0UH07\":[\"Зображення \",[\"0\"],\" з \",[\"1\"]],\"R7SsBE\":[\"Вимкнути звук\"],\"R8rf1X\":[\"Натисніть для встановлення теми\"],\"RArB3D\":[\"був кікнутий з \",[\"channelName\"],\" користувачем \",[\"username\"]],\"RI3cWd\":[\"Відкрийте світ IRC з ObsidianIRC\"],\"RIfHS5\":[\"Створити нове запрошувальне посилання\"],\"RMMaN5\":[\"Модерований (+m)\"],\"RWw9Lg\":[\"Закрити вікно\"],\"RZ2BuZ\":[\"Реєстрація облікового запису \",[\"account\"],\" потребує підтвердження: \",[\"message\"]],\"RySp6q\":[\"Приховати коментарі\"],\"SPKQTd\":[\"Псевдонім обов'язковий\"],\"SPVjfj\":[\"За замовчуванням буде 'без причини', якщо залишити порожнім\"],\"SQKPvQ\":[\"Запросити користувача\"],\"SkZcl+\":[\"Виберіть готовий профіль захисту від флуду. Ці профілі надають збалансовані налаштування захисту для різних випадків використання.\"],\"Slr+3C\":[\"Мін. користувачів\"],\"Spnlre\":[\"Ви запросили \",[\"target\"],\" приєднатися до \",[\"channel\"]],\"T/ckN5\":[\"Відкрити у переглядачі\"],\"T91vKp\":[\"Відтворити\"],\"TV2Wdu\":[\"Дізнайтесь, як ми обробляємо ваші дані та захищаємо вашу конфіденційність.\"],\"TgFpwD\":[\"Застосовується...\"],\"TkzSFB\":[\"Без змін\"],\"TtserG\":[\"Введіть справжнє ім'я\"],\"Ttz9J1\":[\"Введіть пароль...\"],\"Tz0i8g\":[\"Налаштування\"],\"U3pytU\":[\"Адмін\"],\"UDb2YD\":[\"React\"],\"UE4KO5\":[\"*канал*\"],\"UETAwW\":[\"Ви ще не створили жодного запрошувального посилання. Скористайтеся формою вище, щоб створити перше.\"],\"UGT5vp\":[\"Зберегти налаштування\"],\"UV5hLB\":[\"Заборон не знайдено\"],\"Uaj3Nd\":[\"Статусні повідомлення\"],\"Ue3uny\":[\"За замовчуванням (без профілю)\"],\"UkARhe\":[\"Нормальний - стандартний захист\"],\"Umn7Cj\":[\"Коментарів ще немає. Будьте першим!\"],\"UtUIRh\":[[\"0\"],\" старіших повідомлень\"],\"UwzP+U\":[\"Безпечне з'єднання\"],\"V0/A4O\":[\"Власник каналу\"],\"V4qgxE\":[\"Створено до (хв тому)\"],\"V8yTm6\":[\"Очистити пошук\"],\"VJMMyz\":[\"ObsidianIRC - Привносимо IRC у майбутнє\"],\"VJScHU\":[\"Причина\"],\"VLsmVV\":[\"Вимкнути сповіщення\"],\"VbyRUy\":[\"Коментарі\"],\"Vmx0mQ\":[\"Встановлено:\"],\"VqnIZz\":[\"Переглянути нашу політику конфіденційності та практики роботи з даними\"],\"VrMygG\":[\"Мінімальна довжина: \",[\"0\"]],\"VrnTui\":[\"Ваші займенники, відображені у вашому профілі\"],\"W8E3qn\":[\"Автентифікований акаунт\"],\"WAakm9\":[\"Видалити канал\"],\"WFxTHC\":[\"Додати маску бану (напр., nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Хост сервера обов'язковий\"],\"WRYdXW\":[\"Позиція аудіо\"],\"WUOH5B\":[\"Ігнорувати користувача\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Показати ще 1 елемент\"],\"few\":[\"Показати ще \",[\"1\"],\" елементи\"],\"many\":[\"Показати ще \",[\"1\"],\" елементів\"],\"other\":[\"Показати ще \",[\"1\"],\" елементи\"]}]],\"WYxRzo\":[\"Створюйте та керуйте своїми запрошувальними посиланнями\"],\"Wd38W1\":[\"Залиште поле каналу порожнім для загального запрошення в мережу. Опис призначений лише для ваших записів — видимий лише вам у цьому списку.\"],\"Weq9zb\":[\"Загальне\"],\"Wfj7Sk\":[\"Вимкнути або увімкнути звуки сповіщень\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*спам*\"],\"WzMCru\":[\"Профіль користувача\"],\"X6S3lt\":[\"Пошук налаштувань, каналів, серверів...\"],\"XEHan5\":[\"Продовжити все одно\"],\"XI1+wb\":[\"Недійсний формат\"],\"XIXeuC\":[\"Повідомлення @\",[\"0\"]],\"XMS+k4\":[\"Почати приватне повідомлення\"],\"XWgxXq\":[\"Альбом\"],\"Xd7+IT\":[\"Відкріпити приватну розмову\"],\"Xm/s+u\":[\"Дисплей\"],\"Xp2n93\":[\"Показує медіа з надійного файлового хоста вашого сервера. Запити до зовнішніх сервісів не здійснюються.\"],\"XvjC4F\":[\"Збереження...\"],\"Y/qryO\":[\"Користувачів за вашим запитом не знайдено\"],\"YAqRpI\":[\"Реєстрація облікового запису \",[\"account\"],\" успішна: \",[\"message\"]],\"YEfzvP\":[\"Захищена тема (+t)\"],\"YQOn6a\":[\"Згорнути список учасників\"],\"YRCoE9\":[\"Оператор каналу\"],\"YURQaF\":[\"Переглянути профіль\"],\"YdBSvr\":[\"Керувати відображенням медіа та зовнішнього вмісту\"],\"Yj6U3V\":[\"Без центрального сервера:\"],\"YjvpGx\":[\"Займенники\"],\"YqH4l4\":[\"Без ключа\"],\"YyUPpV\":[\"Акаунт:\"],\"ZJSWfw\":[\"Повідомлення при від'єднанні від сервера\"],\"ZR1dJ4\":[\"Запрошення\"],\"ZdWg0V\":[\"Відкрити в браузері\"],\"ZhRBbl\":[\"Пошук повідомлень…\"],\"Zmcu3y\":[\"Розширені фільтри\"],\"a2/8e5\":[\"Тему встановлено після (хв тому)\"],\"aHKcKc\":[\"Попередня сторінка\"],\"aJTbXX\":[\"Пароль оператора\"],\"aQryQv\":[\"Шаблон вже існує\"],\"aW9pLN\":[\"Максимальна кількість користувачів у каналі. Залиште порожнім для відсутності обмежень.\"],\"ah4fmZ\":[\"Також показує попередній перегляд з YouTube, Vimeo, SoundCloud та подібних відомих сервісів.\"],\"aifXak\":[\"Немає медіа у цьому каналі\"],\"ap2zBz\":[\"Розслаблений\"],\"az8lvo\":[\"Вимк\"],\"azXSNo\":[\"Розгорнути список учасників\"],\"azdliB\":[\"Увійти в акаунт\"],\"b26wlF\":[\"вона/її\"],\"bD/+Ei\":[\"Строгий\"],\"bQ6BJn\":[\"Налаштуйте детальні правила захисту від флуду. Кожне правило вказує, яку активність відстежувати і які дії вживати при перевищенні порогів.\"],\"beV7+y\":[\"Користувач отримає запрошення приєднатися до \",[\"channelName\"],\".\"],\"bk84cH\":[\"Повідомлення про відсутність\"],\"bkHdLj\":[\"Додати IRC-сервер\"],\"bmQLn5\":[\"Додати правило\"],\"bwRvnp\":[\"Дія\"],\"c8+EVZ\":[\"Підтверджений акаунт\"],\"cGYUlD\":[\"Медіа-перегляди не завантажені.\"],\"cLF98o\":[\"Показати коментарі (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Користувачів немає\"],\"cSgpoS\":[\"Закріпити приватну розмову\"],\"cde3ce\":[\"Повідомлення <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Скопіювати форматований вивід\"],\"cl/A5J\":[\"Ласкаво просимо до \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Видалити\"],\"coPLXT\":[\"Ми не зберігаємо ваші IRC-комунікації на наших серверах\"],\"crYH/6\":[\"Програвач SoundCloud\"],\"d3sis4\":[\"Додати сервер\"],\"d9aN5k\":[\"Видалити \",[\"username\"],\" з каналу\"],\"dEgA5A\":[\"Скасувати\"],\"dGi1We\":[\"Відкріпити цю приватну розмову\"],\"dJVuyC\":[\"покинув \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"до\"],\"dXqxlh\":[\"<0>⚠️ Загроза безпеці!0> Це з'єднання може бути вразливим до перехоплення або атак «людина посередині».\"],\"da9Q/R\":[\"Змінено режими каналу\"],\"dhJN3N\":[\"Показати коментарі\"],\"dj2xTE\":[\"Відхилити сповіщення\"],\"dpCzmC\":[\"Налаштування захисту від флуду\"],\"e9dQpT\":[\"Бажаєте відкрити це посилання в новій вкладці?\"],\"ePK91l\":[\"Редагувати\"],\"eYBDuB\":[\"Завантажте зображення або вкажіть URL з необов'язковою підстановкою \",[\"size\"],\" для динамічного розміру\"],\"edBbee\":[\"Заблокувати \",[\"username\"],\" за маскою хоста (запобігає повторному входу з тієї ж IP/хоста)\"],\"ekfzWq\":[\"Налаштування користувача\"],\"elPDWs\":[\"Налаштуйте свій IRC-клієнт\"],\"eu2osY\":[\"<0>💡 Рекомендація:0> Продовжуйте лише якщо довіряєте цьому серверу і розумієте ризики. Уникайте передачі конфіденційної інформації або паролів через це з'єднання.\"],\"euEhbr\":[\"Натисніть, щоб приєднатися до \",[\"channel\"]],\"ez3vLd\":[\"Увімкнути багаторядкове введення\"],\"f0J5Ki\":[\"Зв'язок між серверами може використовувати незашифровані з'єднання\"],\"f9BHJk\":[\"Попередити користувача\"],\"fDOLLd\":[\"Канали не знайдено.\"],\"ffzDkB\":[\"Анонімна аналітика:\"],\"fq1GF9\":[\"Відображати, коли користувачі від'єднуються від сервера\"],\"gEF57C\":[\"Цей сервер підтримує лише один тип з'єднання\"],\"gJuLUI\":[\"Список ігнорування\"],\"gNzMrk\":[\"Поточний аватар\"],\"gjPWyO\":[\"Введіть нік...\"],\"gz6UQ3\":[\"Розгорнути\"],\"h6razj\":[\"Маска виключення назви каналу\"],\"hG6jnw\":[\"Тема не встановлена\"],\"hG89Ed\":[\"Зображення\"],\"hYgDIe\":[\"Створити\"],\"hZ6znB\":[\"Порт\"],\"ha+Bz5\":[\"напр., 100:1440\"],\"he3ygx\":[\"Копіювати\"],\"hehnjM\":[\"Кількість\"],\"hzdLuQ\":[\"Говорити можуть лише користувачі з голосом або вище\"],\"i0qMbr\":[\"Головна\"],\"iDNBZe\":[\"Сповіщення\"],\"iH8pgl\":[\"Назад\"],\"iL9SZg\":[\"Заблокувати користувача (за псевдонімом)\"],\"iNt+3c\":[\"Назад до зображення\"],\"iQvi+a\":[\"Не попереджати мене про низький рівень безпеки для цього сервера\"],\"iSLIjg\":[\"Підключитися\"],\"iWXkHH\":[\"Напів-оператор\"],\"iZeTtp\":[\"Хост сервера\"],\"idD8Ev\":[\"Збережено\"],\"iivqkW\":[\"Час входу\"],\"ij+Elv\":[\"Попередній перегляд зображення\"],\"ilIWp7\":[\"Перемкнути сповіщення\"],\"iuaqvB\":[\"Використовуйте * для шаблонів. Приклади: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Бот\"],\"j2DGR0\":[\"Блокування за маскою хоста\"],\"jA4uoI\":[\"Тема:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Причина (необов'язково)\"],\"jUV7CU\":[\"Завантажити аватар\"],\"jW5Uwh\":[\"Контролюйте завантаження зовнішніх медіа. Вимк / Безпечно / Надійні джерела / Весь вміст.\"],\"jXzms5\":[\"Параметри вкладення\"],\"jZlrte\":[\"Колір\"],\"jfC/xh\":[\"Контакт\"],\"jywMpv\":[\"#нова-назва-каналу\"],\"k112DD\":[\"Завантажити старіші повідомлення\"],\"k3ID0F\":[\"Фільтрувати учасників…\"],\"k65gsE\":[\"Детальний огляд\"],\"k7Zgob\":[\"Скасувати підключення\"],\"kAVx5h\":[\"Запрошень не знайдено\"],\"kCLEPU\":[\"Підключено до\"],\"kF5LKb\":[\"Ігноровані шаблони:\"],\"kGeOx/\":[\"Приєднатися до \",[\"0\"]],\"kITKr8\":[\"Завантаження режимів каналу...\"],\"kPpPsw\":[\"Ви є IRC-оператором\"],\"kWJmRL\":[\"Ви\"],\"kfcRb0\":[\"Аватар\"],\"kjMqSj\":[\"Скопіювати JSON\"],\"krViRy\":[\"Натисніть для копіювання як JSON\"],\"ks71ra\":[\"Винятки\"],\"kw4lRv\":[\"Напів-оператор каналу\"],\"kxgIRq\":[\"Виберіть або додайте канал для початку.\"],\"ky6dWe\":[\"Попередній перегляд аватара\"],\"l+GxCv\":[\"Завантаження каналів...\"],\"l+IUVW\":[\"Верифікація облікового запису \",[\"account\"],\" успішна: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"перепідключився\"],\"few\":[\"перепідключився \",[\"reconnectCount\"],\" рази\"],\"many\":[\"перепідключився \",[\"reconnectCount\"],\" разів\"],\"other\":[\"перепідключився \",[\"reconnectCount\"],\" рази\"]}]],\"l1l8sj\":[[\"0\"],\" д тому\"],\"l5NhnV\":[\"#канал (необов'язково)\"],\"l5jmzx\":[[\"0\"],\" та \",[\"1\"],\" пишуть...\"],\"lCF0wC\":[\"Оновити\"],\"lHy8N5\":[\"Завантаження більше каналів...\"],\"lasgrr\":[\"використано\"],\"lbpf14\":[\"Приєднатися до \",[\"value\"]],\"lfFsZ4\":[\"Канали\"],\"lkNdiH\":[\"Ім'я акаунту\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Завантажити зображення\"],\"loQxaJ\":[\"Я повернувся\"],\"lvfaxv\":[\"ГОЛОВНА\"],\"m16xKo\":[\"Додати\"],\"m8flAk\":[\"Попередній перегляд (ще не завантажено)\"],\"mEPxTp\":[\"<0>⚠️ Будьте обережні!0> Відкривайте лише посилання з надійних джерел. Шкідливі посилання можуть порушити вашу безпеку або конфіденційність.\"],\"mHGdhG\":[\"Інформація про сервер\"],\"mHS8lb\":[\"Повідомлення #\",[\"0\"]],\"mMYBD9\":[\"Широкий - ширша область захисту\"],\"mTGsPd\":[\"Тема каналу\"],\"mU8j6O\":[\"Без зовнішніх повідомлень (+n)\"],\"mZp8FL\":[\"Автоматичне повернення до одного рядка\"],\"mdQu8G\":[\"ВашПсевдонім\"],\"miSSBQ\":[\"Коментарі (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Користувач автентифікований\"],\"mwtcGl\":[\"Закрити коментарі\"],\"mzI/c+\":[\"Завантажити\"],\"n3fGRk\":[\"встановлено \",[\"0\"]],\"nE9jsU\":[\"Розслаблений - менш агресивний захист\"],\"nNflMD\":[\"Покинути канал\"],\"nPXkBi\":[\"Завантаження даних WHOIS...\"],\"nQnxxF\":[\"Повідомлення #\",[\"0\"],\" (Shift+Enter для нового рядка)\"],\"nWMRxa\":[\"Відкріпити\"],\"nkC032\":[\"Без профілю флуду\"],\"o69z4d\":[\"Надіслати попереджувальне повідомлення \",[\"username\"]],\"o9ylQi\":[\"Знайдіть GIF для початку\"],\"oFGkER\":[\"Повідомлення сервера\"],\"oOi11l\":[\"Прокрутити вниз\"],\"oPYIL5\":[\"мережа\"],\"oQEzQR\":[\"Нове DM\"],\"oXOSPE\":[\"Онлайн\"],\"oal760\":[\"Можливі атаки «людина посередині» на з'єднання сервера\"],\"oeqmmJ\":[\"Надійні джерела\"],\"optX0N\":[[\"0\"],\" год тому\"],\"ovBPCi\":[\"За замовчуванням\"],\"p0Z69r\":[\"Шаблон не може бути порожнім\"],\"p1KgtK\":[\"Не вдалося завантажити аудіо\"],\"p59pEv\":[\"Детальніше\"],\"p7sRI6\":[\"Повідомляти інших, коли ви друкуєте\"],\"pBm1od\":[\"Секретний канал\"],\"pNmiXx\":[\"Ваш псевдонім за замовчуванням для всіх серверів\"],\"pUUo9G\":[\"Хост:\"],\"pVGPmz\":[\"Пароль акаунту\"],\"peNE68\":[\"Постійний\"],\"plhHQt\":[\"Немає даних\"],\"pm6+q5\":[\"Попередження про безпеку\"],\"pn5qSs\":[\"Додаткова інформація\"],\"q0cR4S\":[\"тепер відомий як **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Канал не відображатиметься у командах LIST або NAMES\"],\"qLpTm/\":[\"Видалити реакцію \",[\"emoji\"]],\"qVkGWK\":[\"Закріпити\"],\"qY8wNa\":[\"Головна сторінка\"],\"qb0xJ7\":[\"Використовуйте шаблони: * відповідає будь-якій послідовності, ? відповідає будь-якому одному символу. Приклади: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Ключ каналу (+k)\"],\"qtoOYG\":[\"Без обмежень\"],\"r1W2AS\":[\"Зображення з файлового хостингу\"],\"rIPR2O\":[\"Тему встановлено до (хв тому)\"],\"rMMSYo\":[\"Максимальна довжина: \",[\"0\"]],\"rWtzQe\":[\"Мережа розділилась і возз'єдналась. ✅\"],\"rYG2u6\":[\"Будь ласка, зачекайте...\"],\"rdUucN\":[\"Попередній перегляд\"],\"rjGI/Q\":[\"Конфіденційність\"],\"rk8iDX\":[\"Завантаження GIF...\"],\"rn6SBY\":[\"Увімкнути звук\"],\"s/UKqq\":[\"Виключено з каналу\"],\"s8cATI\":[\"приєднався до \",[\"channelName\"]],\"sCO9ue\":[\"З'єднання з <0>\",[\"serverName\"],\"0> має такі проблеми безпеки:\"],\"sGH11W\":[\"Сервер\"],\"sHI1H+\":[\"тепер відомий як **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" запросив вас приєднатися до \",[\"channel\"]],\"sby+1/\":[\"Натисніть для копіювання\"],\"sfN25C\":[\"Ваше справжнє або повне ім'я\"],\"sliuzR\":[\"Відкрити посилання\"],\"sqrO9R\":[\"Власні згадки\"],\"sr6RdJ\":[\"Багаторядковий на Shift+Enter\"],\"swrCpB\":[\"Канал перейменовано з \",[\"oldName\"],\" на \",[\"newName\"],\" користувачем \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Додаткові\"],\"t/YqKh\":[\"Видалити\"],\"t47eHD\":[\"Ваш унікальний ідентифікатор на цьому сервері\"],\"tAkAh0\":[\"URL з необов'язковою підстановкою \",[\"size\"],\" для динамічного розміру. Приклад: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Показати або приховати бічну панель зі списком каналів\"],\"tfDRzk\":[\"Зберегти\"],\"tiBsJk\":[\"покинув \",[\"channelName\"]],\"tt4/UD\":[\"вийшов (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Псевдонім {nick} вже використовується, повторна спроба з {newNick}\"],\"u0a8B4\":[\"Автентифікуватися як IRC-оператор для адміністративного доступу\"],\"u0rWFU\":[\"Створено після (хв тому)\"],\"u72w3t\":[\"Користувачі та шаблони для ігнорування\"],\"u7jc2L\":[\"вийшов\"],\"uAQUqI\":[\"Статус\"],\"uB85T3\":[\"Помилка збереження: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC сервери:\"],\"ukyW4o\":[\"Ваші запрошувальні посилання\"],\"usSSr/\":[\"Рівень масштабу\"],\"v7uvcf\":[\"Програма:\"],\"vE8kb+\":[\"Використовуйте Shift+Enter для нового рядка (Enter надсилає)\"],\"vERlcd\":[\"Профіль\"],\"vK0RL8\":[\"Без теми\"],\"vSJd18\":[\"Відео\"],\"vXIe7J\":[\"Мова\"],\"vaHYxN\":[\"Справжнє ім'я\"],\"vhjbKr\":[\"Відсутній\"],\"w4NYox\":[\"клієнт \",[\"title\"]],\"w8xQRx\":[\"Недійсне значення\"],\"wFjjxZ\":[\"був кікнутий з \",[\"channelName\"],\" користувачем \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Винятків з заборон не знайдено\"],\"wPrGnM\":[\"Адміністратор каналу\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Відображати, коли користувачі входять або виходять з каналів\"],\"whqZ9r\":[\"Додаткові слова або фрази для виділення\"],\"wm7RV4\":[\"Звук сповіщення\"],\"wz/Yoq\":[\"Ваші повідомлення можуть бути перехоплені при передачі між серверами\"],\"x3+y8b\":[\"Стільки людей зареєструвалося через це посилання\"],\"xCJdfg\":[\"Очистити\"],\"xOTzt5\":[\"щойно\"],\"xUHRTR\":[\"Автоматично автентифікуватися як оператор при підключенні\"],\"xWHwwQ\":[\"Блокування\"],\"xYilR2\":[\"Медіа\"],\"xbi8D6\":[\"Цей сервер не підтримує запрошувальні посилання (можливість<0>obby.world/invitation0>не оголошена). Ви все ще можете нормально спілкуватися; ця панель призначена для мереж на основі obbyircd.\"],\"xceQrO\":[\"Підтримуються лише захищені websocket-з'єднання\"],\"xdtXa+\":[\"назва-каналу\"],\"xfXC7q\":[\"Текстові канали\"],\"xlCYOE\":[\"Отримання більше повідомлень...\"],\"xlhswE\":[\"Мінімальне значення: \",[\"0\"]],\"xq97Ci\":[\"Додати слово або фразу...\"],\"xuRqRq\":[\"Ліміт клієнтів (+l)\"],\"xwF+7J\":[[\"0\"],\" пише...\"],\"y1eoq1\":[\"Копіювати посилання\"],\"yNeucF\":[\"Цей сервер не підтримує розширені метадані профілю (розширення IRCv3 METADATA). Додаткові поля, такі як аватар, відображуване ім'я та статус, недоступні.\"],\"yPlrca\":[\"Аватар каналу\"],\"yQE2r9\":[\"Завантаження\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Ім'я оператора\"],\"yYOzWD\":[\"логи\"],\"yfx9Re\":[\"Пароль IRC оператора\"],\"ygCKqB\":[\"Зупинити\"],\"ymDxJx\":[\"Ім'я IRC оператора\"],\"yrpRsQ\":[\"Сортувати за назвою\"],\"yz7wBu\":[\"Закрити\"],\"zJw+jA\":[\"встановлює режим: \",[\"0\"]],\"zbymaY\":[[\"0\"],\" хв тому\"],\"zebeLu\":[\"Введіть ім'я оператора\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"Недійсний формат шаблону. Використовуйте формат nick!user@host (дозволені символи * як шаблони)\"],\"+6NQQA\":[\"Загальний канал підтримки\"],\"+6NyRG\":[\"Клієнт\"],\"+K0AvT\":[\"Від'єднатися\"],\"+cyFdH\":[\"Стандартне повідомлення при позначенні себе відсутнім\"],\"+mVPqU\":[\"Відображати Markdown-форматування у повідомленнях\"],\"+vqCJH\":[\"Ім'я користувача вашого акаунту для автентифікації\"],\"+yPBXI\":[\"Вибрати файл\"],\"+zy2Nq\":[\"Тип\"],\"/09cao\":[\"Низький рівень безпеки з'єднання (Рівень \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"Користувачі за межами каналу не можуть надсилати до нього повідомлення\"],\"/4C8U0\":[\"ÐопÑÑваÑи вÑе\"],\"/6BzZF\":[\"Перемкнути список учасників\"],\"/AkXyp\":[\"ÐÑдÑвеÑдиÑи?\"],\"/TNOPk\":[\"Користувач відсутній\"],\"/XQgft\":[\"Дослідити\"],\"/cF7Rs\":[\"Гучність\"],\"/dqduX\":[\"Наступна сторінка\"],\"/fc3q4\":[\"Весь вміст\"],\"/kISDh\":[\"Увімкнути звуки сповіщень\"],\"/n04sB\":[\"Kill\"],\"/rTz0M\":[\"Аудіо\"],\"/rfkZe\":[\"Відтворювати звуки для згадок і повідомлень\"],\"0/0ZGA\":[\"Маска назви каналу\"],\"0D6j7U\":[\"Дізнатися більше про власні правила →\"],\"0XsHcR\":[\"Вигнати користувача\"],\"0ZpE//\":[\"Сортувати за кількістю користувачів\"],\"0bEPwz\":[\"Встановити статус «Відсутній»\"],\"0dGkPt\":[\"Розгорнути список каналів\"],\"0gS7M5\":[\"Відображуване ім'я\"],\"0kS+M8\":[\"ПрикладМЕРЕЖА\"],\"0rgoY7\":[\"Підключатися лише до обраних вами серверів\"],\"0wdd7X\":[\"Приєднатися\"],\"0wkVYx\":[\"Приватні повідомлення\"],\"111uHX\":[\"Попередній перегляд посилання\"],\"196EG4\":[\"Видалити приватний чат\"],\"1DSr1i\":[\"Зареєструвати акаунт\"],\"1O/24y\":[\"Перемкнути список каналів\"],\"1TNIig\":[\"Відкрити\"],\"1VPJJ2\":[\"Попередження про зовнішнє посилання\"],\"1ZC/dv\":[\"Немає непрочитаних згадувань або повідомлень\"],\"1pO1zi\":[\"Назва сервера обов'язкова\"],\"1uwfzQ\":[\"Переглянути тему каналу\"],\"268g7c\":[\"Введіть відображуване ім'я\"],\"2CEOW6\":[\"Мережа, прив'язана через bouncer soju\"],\"2F9+AZ\":[\"Ðоки Ñо не заÑÑкÑовано необÑобленого IRC-ÑÑаÑÑкÑ. СпÑобÑйÑе пÑдклÑÑиÑиÑÑ Ð°Ð±Ð¾ надÑÑлаÑи повÑдомленнÑ.\"],\"2FOFq1\":[\"Оператори сервера в мережі можуть потенційно читати ваші повідомлення\"],\"2FYpfJ\":[\"Більше\"],\"2HF1Y2\":[[\"inviter\"],\" запросив \",[\"target\"],\" приєднатися до \",[\"channel\"]],\"2I70QL\":[\"Переглянути інформацію профілю користувача\"],\"2QYdmE\":[\"Користувачі:\"],\"2QpEjG\":[\"вийшов\"],\"2YE223\":[\"Повідомлення #\",[\"0\"],\" (Enter для нового рядка, Shift+Enter для відправки)\"],\"2bimFY\":[\"Використовувати пароль сервера\"],\"2iTmdZ\":[\"Локальне сховище:\"],\"2odkwe\":[\"Строгий - більш агресивний захист\"],\"2uDhbA\":[\"Введіть ім'я користувача для запрошення\"],\"2ygf/L\":[\"← Назад\"],\"2zEgxj\":[\"Пошук GIF...\"],\"3RdPhl\":[\"Перейменувати канал\"],\"3THokf\":[\"Користувач з голосом\"],\"3TSz9S\":[\"Згорнути\"],\"3jBDvM\":[\"Відображувана назва каналу\"],\"3ryuFU\":[\"Необов'язкові звіти про збої для покращення додатку\"],\"3uBF/8\":[\"Закрити переглядач\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"Введіть ім'я акаунту...\"],\"4/Rr0R\":[\"Запросити користувача до поточного каналу\"],\"4EZrJN\":[\"Правила\"],\"4JJtW9\":[\"#переповнення\"],\"4NqeT4\":[\"Профіль флуду (+F)\"],\"4RZQRK\":[\"Що ти зараз робиш?\"],\"4hfTrB\":[\"Псевдонім\"],\"4n99LO\":[\"Вже в \",[\"0\"]],\"4t6vMV\":[\"Автоматично перемикатися на один рядок для коротких повідомлень\"],\"4vsHmf\":[\"Час (хв)\"],\"4x/Axu\":[\"Ваш баунсер ще не має жодних мереж. Додайте одну, щоб почати.\"],\"5+INAX\":[\"Виділяти повідомлення, що вас згадують\"],\"5R5Pv/\":[\"Ім'я оператора\"],\"678PKt\":[\"Назва мережі\"],\"6Aih4U\":[\"Офлайн\"],\"6CO3WE\":[\"Пароль для входу до каналу. Залиште порожнім для видалення ключа.\"],\"6HhMs3\":[\"Повідомлення про вихід\"],\"6V3Ea3\":[\"Скопійовано\"],\"6lGV3K\":[\"Показати менше\"],\"6yFOEi\":[\"Введіть пароль оператора...\"],\"7+IHTZ\":[\"Файл не вибрано\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host (напр., spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"Надіслати приватне повідомлення\"],\"7U1W7c\":[\"Дуже розслаблений\"],\"7Y1YQj\":[\"Справжнє ім'я:\"],\"7YHArF\":[\"— відкрити у переглядачі\"],\"7fjnVl\":[\"Пошук користувачів...\"],\"7jL88x\":[\"Видалити це повідомлення? Цю дію неможливо скасувати.\"],\"7nGhhM\":[\"Що у вас на думці?\"],\"7sEpu1\":[\"Учасники — \",[\"0\"]],\"7sNhEz\":[\"Ім'я користувача\"],\"8H0Q+x\":[\"Дізнатися більше про профілі →\"],\"8Phu0A\":[\"Відображати, коли користувачі змінюють псевдонім\"],\"8XTG9e\":[\"Введіть пароль оператора\"],\"8XsV2J\":[\"Повторити надсилання\"],\"8ZsakT\":[\"Пароль\"],\"8kR84m\":[\"Ви збираєтеся відкрити зовнішнє посилання:\"],\"8lCgih\":[\"Видалити правило\"],\"8o3dPc\":[\"ÐеÑеÑÑгнÑÑÑ Ñайли Ð´Ð»Ñ Ð·Ð°Ð²Ð°Ð½ÑаженнÑ\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"one\":[\"приєднався\"],\"few\":[\"приєднався \",[\"joinCount\"],\" рази\"],\"many\":[\"приєднався \",[\"joinCount\"],\" разів\"],\"other\":[\"приєднався \",[\"joinCount\"],\" рази\"]}]],\"9BMLnJ\":[\"Перепідключитися до сервера\"],\"9OEgyT\":[\"Додати реакцію\"],\"9PQ8m2\":[\"G-Line (глобальний бан)\"],\"9Qs99X\":[\"Електронна пошта:\"],\"9QupBP\":[\"Видалити шаблон\"],\"9W7tl5\":[\"(без змін)\"],\"9bG48P\":[\"Надсилання\"],\"9f5f0u\":[\"Питання про конфіденційність? Зв'яжіться з нами:\"],\"9iweoP\":[\"Мережі на \",[\"0\"]],\"9unqs3\":[\"Відсутність:\"],\"9v3hwv\":[\"Сервери не знайдено.\"],\"9zb2WA\":[\"Підключення\"],\"A1taO8\":[\"Пошук\"],\"A2adVi\":[\"Надсилати сповіщення про введення\"],\"A9Rhec\":[\"Назва каналу\"],\"AWOSPo\":[\"Збільшити\"],\"AXSpEQ\":[\"Оператор при підключенні\"],\"AdKRCX\":[\"Ви підключені до <0>\",[\"0\"],\"0>.\"],\"AeXO77\":[\"Акаунт\"],\"AhNP40\":[\"Перемотати\"],\"Ai2U7L\":[\"Хост\"],\"AjBQnf\":[\"Змінено псевдонім\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"Скасувати відповідь\"],\"ApSx0O\":[\"Знайдено \",[\"0\"],\" повідомлень, що відповідають \\\"\",[\"searchQuery\"],\"\\\"\"],\"AxPAXW\":[\"Результатів не знайдено\"],\"AyNqAB\":[\"Відображати всі події сервера в чаті\"],\"B/QqGw\":[\"Відійшов від клавіатури\"],\"B0sB2k\":[\"Незашифровано\"],\"B8AaMI\":[\"Це поле обов'язкове\"],\"BA2c49\":[\"Сервер не підтримує розширену фільтрацію LIST\"],\"BDKt3I\":[[\"0\"],\", \",[\"1\"],\", \",[\"2\"],\" та ще \",[\"3\"],\" пишуть...\"],\"BGul2A\":[\"У вас є незбережені зміни. Ви впевнені, що хочете закрити без збереження?\"],\"BIf9fi\":[\"Ваше статусне повідомлення\"],\"BOJWfb\":[\"Від'єднатися від bouncer soju?\"],\"BPm98R\":[\"Ðоден ÑеÑÐ²ÐµÑ Ð½Ðµ вибÑано. СпоÑаÑÐºÑ Ð¾Ð±ÐµÑÑÑÑ ÑеÑÐ²ÐµÑ Ð· бÑÑÐ½Ð¾Ñ Ð¿Ð°Ð½ÐµÐ»Ñ; запÑоÑÑвалÑÐ½Ñ Ð¿Ð¾ÑÐ¸Ð»Ð°Ð½Ð½Ñ ÐºÐµÑÑÑÑÑÑÑ Ð¾ÐºÑемо Ð´Ð»Ñ ÐºÐ¾Ð¶Ð½Ð¾Ð³Ð¾ ÑеÑвеÑа.\"],\"BZz3md\":[\"Ваш особистий веб-сайт\"],\"Bgm/H7\":[\"Дозволити введення кількох рядків тексту\"],\"BiQIl1\":[\"Закріпити цю приватну розмову\"],\"BlNZZ2\":[\"Натисніть, щоб перейти до повідомлення\"],\"Bowq3c\":[\"Тільки оператори можуть змінювати тему каналу\"],\"Btozzp\":[\"Термін дії цього зображення минув\"],\"Bycfjm\":[\"Всього: \",[\"0\"]],\"C6IBQc\":[\"Скопіювати весь JSON\"],\"C9L9wL\":[\"Збір даних\"],\"CDq4wC\":[\"Помірний режим для користувача\"],\"CHVRxG\":[\"Повідомлення @\",[\"0\"],\" (Shift+Enter для нового рядка)\"],\"CN9zdR\":[\"Ім'я та пароль оператора обов'язкові\"],\"CW3sYa\":[\"Додати реакцію \",[\"emoji\"]],\"CaAkqd\":[\"Показати виходи\"],\"CbvaYj\":[\"Блокування за псевдонімом\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"Вибрати канал\"],\"CsekCi\":[\"Нормальний\"],\"D+NlUC\":[\"Система\"],\"D28t6+\":[\"приєднався та вийшов\"],\"DB8zMK\":[\"Застосувати\"],\"DBcWHr\":[\"Власний файл звуку сповіщення\"],\"DTy9Xw\":[\"Попередній перегляд медіа\"],\"Dj4pSr\":[\"Виберіть надійний пароль\"],\"Du+zn+\":[\"Пошук...\"],\"Du2T2f\":[\"Налаштування не знайдено\"],\"DwsSVQ\":[\"Застосувати фільтри та оновити\"],\"E3W/zd\":[\"Стандартний псевдонім\"],\"E6nRW7\":[\"Копіювати URL\"],\"E703RG\":[\"Режими:\"],\"EAeu1Z\":[\"Надіслати запрошення\"],\"EFKJQT\":[\"Налаштування\"],\"EGPQBv\":[\"Власні правила флуду (+f)\"],\"ELik0r\":[\"Переглянути повну політику конфіденційності\"],\"EPbeC2\":[\"Переглянути або редагувати тему каналу\"],\"EQCDNT\":[\"Введіть ім'я користувача оператора...\"],\"EUvulZ\":[\"Знайдено 1 повідомлення, що відповідає \\\"\",[\"searchQuery\"],\"\\\"\"],\"EatZYJ\":[\"Наступне зображення\"],\"EdQY6l\":[\"Немає\"],\"EnqLYU\":[\"Пошук серверів...\"],\"F0OKMc\":[\"Редагувати сервер\"],\"F6Int2\":[\"Увімкнути виділення\"],\"FDoLyE\":[\"Макс. користувачів\"],\"FUU/hZ\":[\"Контролюйте, скільки зовнішніх медіа завантажується у чаті.\"],\"Fdp03t\":[\"увімк\"],\"FfPWR0\":[\"Модальне вікно\"],\"FjkaiT\":[\"Зменшити\"],\"FlqOE9\":[\"Що це означає:\"],\"FolHNl\":[\"Керуйте своїм обліковим записом та автентифікацією\"],\"Fp2Dif\":[\"Вийти з сервера\"],\"G5KmCc\":[\"GZ-Line (глобальна Z-Line)\"],\"GDs0lz\":[\"<0>Ризик:0> Конфіденційна інформація (повідомлення, приватні розмови, дані автентифікації) може бути доступна мережевим адміністраторам або зловмисникам між IRC-серверами.\"],\"GR+2I3\":[\"Додати маску запрошення (напр., nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"Закрити виспливаючі сповіщення сервера\"],\"GdhD7H\":[\"ÐаÑиÑнÑÑÑ Ñе Ñаз Ð´Ð»Ñ Ð¿ÑдÑвеÑдженнÑ\"],\"GjRZex\":[\"Від'єднати мережу?\"],\"GlHnXw\":[\"Зміна псевдоніму не вдалась: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"Попередній перегляд:\"],\"GtmO8/\":[\"від\"],\"GtuHUQ\":[\"Перейменувати цей канал на сервері. Усі користувачі побачать нову назву.\"],\"GuGfFX\":[\"Перемкнути пошук\"],\"GxkJXS\":[\"Завантаження...\"],\"GzbwnK\":[\"Приєднався до каналу\"],\"GzsUDB\":[\"Розширений профіль\"],\"H/PnT8\":[\"Вставити емодзі\"],\"H6Izzl\":[\"Ваш бажаний код кольору\"],\"H9jIv+\":[\"Показати входи/виходи\"],\"HAKBY9\":[\"Завантажити файли\"],\"HdE1If\":[\"Канал\"],\"Hk4AW9\":[\"Ваше бажане відображуване ім'я\"],\"HmHDk7\":[\"Вибрати учасника\"],\"HrQzPU\":[\"Канали на \",[\"networkName\"]],\"I2tXQ5\":[\"Повідомлення @\",[\"0\"],\" (Enter для нового рядка, Shift+Enter для відправки)\"],\"I6bw/h\":[\"Заблокувати користувача\"],\"I92Z+b\":[\"Увімкнути сповіщення\"],\"I9D72S\":[\"Ви впевнені, що хочете видалити це повідомлення? Цю дію неможливо скасувати.\"],\"IA+1wo\":[\"Відображати, коли користувачів виганяють з каналів\"],\"IDwkJx\":[\"IRC оператор\"],\"ILlU+s\":[\"Інфо:\"],\"IUwGEM\":[\"Зберегти зміни\"],\"IVeGK6\":[[\"0\"],\", \",[\"1\"],\" та \",[\"2\"],\" пишуть...\"],\"IgrLD/\":[\"Пауза\"],\"Im6JED\":[\"ШЕПІТ\"],\"ImOQa9\":[\"Відповісти\"],\"IoHMnl\":[\"Максимальне значення: \",[\"0\"]],\"IvMj+0\":[\"Оп\"],\"J28zul\":[\"Підключення...\"],\"J5T9NW\":[\"Інформація про користувача\"],\"J8Y5+z\":[\"Ой! Розрив мережі! ⚠️\"],\"JBHkBA\":[\"Покинув канал\"],\"JCwL0Q\":[\"Введіть причину (необов'язково)\"],\"JFciKP\":[\"Перемкнути\"],\"JXGkhG\":[\"Змінити назву каналу (лише оператори)\"],\"JcD7qf\":[\"Більше дій\"],\"JdkA+c\":[\"Секретний (+s)\"],\"Jmu12l\":[\"Канали сервера\"],\"JoQY+E\":[\"Це видалить мережу з вашого bouncer soju. Щоб використати її знову, вам потрібно буде додати її заново.\"],\"JvQ++s\":[\"Увімкнути Markdown\"],\"K2jwh/\":[\"Дані WHOIS недоступні\"],\"KAXSwC\":[\"Голос\"],\"KDfTdX\":[\"Видалити повідомлення\"],\"KKBlUU\":[\"Вбудувати\"],\"KM0pLb\":[\"Ласкаво просимо до каналу!\"],\"KR6W2h\":[\"Скасувати ігнорування\"],\"KV+Bi1\":[\"Тільки за запрошенням (+i)\"],\"KdCtwE\":[\"Скільки секунд відстежувати активність флуду перед скиданням лічильників\"],\"Kkezga\":[\"Пароль сервера\"],\"KsiQ/8\":[\"Користувачі повинні бути запрошені для входу до каналу\"],\"L+gB/D\":[\"Інформація про канал\"],\"LC1a7n\":[\"IRC-сервер повідомив, що зв'язки між серверами мають низький рівень безпеки. Це означає, що коли ваші повідомлення передаються між IRC-серверами в мережі, вони можуть бути не належним чином зашифровані або SSL/TLS-сертифікати можуть не перевірятися правильно.\"],\"LEwpeL\":[\"bouncer soju (керування)\"],\"LNfLR5\":[\"Показати виключення\"],\"LP+1Z7\":[\"Додати мережу\"],\"LQb0W/\":[\"Показати всі події\"],\"LU7/yA\":[\"Альтернативна назва для відображення в інтерфейсі. Може містити пробіли, емодзі та спеціальні символи. Справжня назва каналу (\",[\"channelName\"],\") використовуватиметься для IRC-команд.\"],\"LUb9O7\":[\"Потрібен дійсний порт сервера\"],\"LV4fT6\":[\"ÐÐ¿Ð¸Ñ (необов'Ñзково, напÑ. \\\"ÐеÑа-ÑеÑÑÑвалÑники Q3\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"Політика конфіденційності\"],\"LcuSDR\":[\"Керуйте інформацією профілю та метаданими\"],\"LqLS9B\":[\"Показати зміни псевдоніма\"],\"LsDQt2\":[\"Налаштування каналу\"],\"LtI9AS\":[\"Власник\"],\"LuNhhL\":[\"відреагував на це повідомлення\"],\"M/AZNG\":[\"URL зображення вашого аватара\"],\"M/WIer\":[\"Надіслати повідомлення\"],\"M8er/5\":[\"Ім'я:\"],\"MHk+7g\":[\"Попереднє зображення\"],\"MRorGe\":[\"ПП користувачу\"],\"MVbSGP\":[\"Часове вікно (секунди)\"],\"MkpcsT\":[\"Ваші повідомлення та налаштування зберігаються локально на вашому пристрої\"],\"MzPdC2\":[\"Пароль сервера (PASS)\"],\"N/hDSy\":[\"Позначити як бот - зазвичай 'on' або порожньо\"],\"N6j2JH\":[\"Редагувати \",[\"0\"]],\"N7TQbE\":[\"Запросити користувача до \",[\"channelName\"]],\"NCca/o\":[\"Введіть стандартний нік...\"],\"Nqs6B9\":[\"Показує всі зовнішні медіа. Будь-яка URL може спричинити запит до невідомого сервера.\"],\"Nt+9O7\":[\"Використовувати WebSocket замість звичайного TCP\"],\"NxIHzc\":[\"Відключити користувача\"],\"O+v/cL\":[\"Переглянути всі канали на сервері\"],\"OCGpR4\":[\"(успадковано)\"],\"ODwSCk\":[\"Надіслати GIF\"],\"OGQ5kK\":[\"Налаштувати звуки сповіщень та виділення\"],\"OIPt1Z\":[\"Показати або приховати бічну панель зі списком учасників\"],\"OKSNq/\":[\"Дуже строгий\"],\"ONWvwQ\":[\"Завантажити\"],\"OVKoQO\":[\"Пароль вашого акаунту для автентифікації\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - Привносимо IRC у майбутнє\"],\"OhCpra\":[\"Встановити тему…\"],\"OkltoQ\":[\"Заблокувати \",[\"username\"],\" за псевдонімом (запобігає повторному входу з тим же ніком)\"],\"P+t/Te\":[\"Немає додаткових даних\"],\"P42Wcc\":[\"Безпечно\"],\"PD38l0\":[\"Попередній перегляд аватара каналу\"],\"PD9mEt\":[\"Введіть повідомлення...\"],\"PPqfdA\":[\"Відкрити налаштування конфігурації каналу\"],\"PSCjfZ\":[\"Тема, яка відображатиметься для цього каналу. Усі користувачі можуть бачити тему.\"],\"PZCecv\":[\"Попередній перегляд PDF\"],\"PeLgsC\":[[\"c\",\"plural\",{\"one\":[\"1 раз\"],\"few\":[[\"c\"],\" рази\"],\"many\":[[\"c\"],\" разів\"],\"other\":[[\"c\"],\" рази\"]}]],\"PguS2C\":[\"Додати маску винятку (напр., nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"Показано \",[\"displayedChannelsCount\"],\" із \",[\"0\"],\" каналів\"],\"PqhVlJ\":[\"Заблокувати користувача (за маскою хоста)\"],\"Q+chwU\":[\"Ім'я користувача:\"],\"Q2QY4/\":[\"ÐидалиÑи Ñе запÑоÑеннÑ\"],\"Q3v9Wc\":[\"Так, видалити\"],\"Q6hhn8\":[\"Налаштування\"],\"QF4a34\":[\"Будь ласка, введіть ім'я користувача\"],\"QGqSZ2\":[\"Колір і форматування\"],\"QJQd1J\":[\"Редагувати профіль\"],\"QSzGDE\":[\"Бездіяльний\"],\"QUlny5\":[\"Ласкаво просимо до \",[\"0\"],\"!\"],\"Qoq+GP\":[\"Читати далі\"],\"QuSkCF\":[\"Фільтрувати канали...\"],\"QwUrDZ\":[\"змінив тему на: \",[\"topic\"]],\"R0UH07\":[\"Зображення \",[\"0\"],\" з \",[\"1\"]],\"R7SsBE\":[\"Вимкнути звук\"],\"R8rf1X\":[\"Натисніть для встановлення теми\"],\"RArB3D\":[\"був кікнутий з \",[\"channelName\"],\" користувачем \",[\"username\"]],\"RI3cWd\":[\"Відкрийте світ IRC з ObsidianIRC\"],\"RIfHS5\":[\"СÑвоÑиÑи нове запÑоÑÑвалÑне поÑиланнÑ\"],\"RMMaN5\":[\"Модерований (+m)\"],\"RWw9Lg\":[\"Закрити вікно\"],\"RZ2BuZ\":[\"Реєстрація облікового запису \",[\"account\"],\" потребує підтвердження: \",[\"message\"]],\"RySp6q\":[\"Приховати коментарі\"],\"S5Togi\":[\"Завантаження мереж з вашого баунсера…\"],\"SPKQTd\":[\"Псевдонім обов'язковий\"],\"SPVjfj\":[\"За замовчуванням буде 'без причини', якщо залишити порожнім\"],\"SQKPvQ\":[\"Запросити користувача\"],\"STmlpb\":[\"Назад до списку мереж\"],\"SkZcl+\":[\"Виберіть готовий профіль захисту від флуду. Ці профілі надають збалансовані налаштування захисту для різних випадків використання.\"],\"Slr+3C\":[\"Мін. користувачів\"],\"Spnlre\":[\"Ви запросили \",[\"target\"],\" приєднатися до \",[\"channel\"]],\"T/ckN5\":[\"Відкрити у переглядачі\"],\"T91vKp\":[\"Відтворити\"],\"TV2Wdu\":[\"Дізнайтесь, як ми обробляємо ваші дані та захищаємо вашу конфіденційність.\"],\"TgFpwD\":[\"Застосовується...\"],\"TkzSFB\":[\"Без змін\"],\"TtserG\":[\"Введіть справжнє ім'я\"],\"Ttz9J1\":[\"Введіть пароль...\"],\"Tz0i8g\":[\"Налаштування\"],\"U3pytU\":[\"Адмін\"],\"UDb2YD\":[\"React\"],\"UE4KO5\":[\"*канал*\"],\"UETAwW\":[\"Ðи Ñе не ÑÑвоÑили жодного запÑоÑÑвалÑного поÑиланнÑ. СкоÑиÑÑайÑеÑÑ ÑоÑÐ¼Ð¾Ñ Ð²Ð¸Ñе, Ñоб ÑÑвоÑиÑи пеÑÑе.\"],\"UGT5vp\":[\"Зберегти налаштування\"],\"UV5hLB\":[\"Заборон не знайдено\"],\"Uaj3Nd\":[\"Статусні повідомлення\"],\"Ue3uny\":[\"За замовчуванням (без профілю)\"],\"UkARhe\":[\"Нормальний - стандартний захист\"],\"Umn7Cj\":[\"Коментарів ще немає. Будьте першим!\"],\"UtUIRh\":[[\"0\"],\" старіших повідомлень\"],\"UwzP+U\":[\"Безпечне з'єднання\"],\"V0/A4O\":[\"Власник каналу\"],\"V0zZWc\":[\"Це також закриє прив'язану мережу нижче.\"],\"V4qgxE\":[\"Створено до (хв тому)\"],\"V8yTm6\":[\"Очистити пошук\"],\"VJMMyz\":[\"ObsidianIRC - Привносимо IRC у майбутнє\"],\"VJScHU\":[\"Причина\"],\"VLsmVV\":[\"Вимкнути сповіщення\"],\"VbyRUy\":[\"Коментарі\"],\"Vmx0mQ\":[\"Встановлено:\"],\"VqnIZz\":[\"Переглянути нашу політику конфіденційності та практики роботи з даними\"],\"VrMygG\":[\"Мінімальна довжина: \",[\"0\"]],\"VrnTui\":[\"Ваші займенники, відображені у вашому профілі\"],\"W8E3qn\":[\"Автентифікований акаунт\"],\"WAakm9\":[\"Видалити канал\"],\"WFxTHC\":[\"Додати маску бану (напр., nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"Хост сервера обов'язковий\"],\"WRYdXW\":[\"Позиція аудіо\"],\"WUOH5B\":[\"Ігнорувати користувача\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"one\":[\"Показати ще 1 елемент\"],\"few\":[\"Показати ще \",[\"1\"],\" елементи\"],\"many\":[\"Показати ще \",[\"1\"],\" елементів\"],\"other\":[\"Показати ще \",[\"1\"],\" елементи\"]}]],\"WYxRzo\":[\"СÑвоÑÑйÑе Ñа кеÑÑйÑе ÑвоÑми запÑоÑÑвалÑними поÑиланнÑми\"],\"Wd38W1\":[\"ÐалиÑÑе поле ÐºÐ°Ð½Ð°Ð»Ñ Ð¿Ð¾ÑожнÑм Ð´Ð»Ñ Ð·Ð°Ð³Ð°Ð»Ñного запÑоÑÐµÐ½Ð½Ñ Ð² меÑежÑ. ÐÐ¿Ð¸Ñ Ð¿ÑизнаÑений лиÑе Ð´Ð»Ñ Ð²Ð°ÑиÑ
запиÑÑв â видимий лиÑе вам Ñ ÑÑÐ¾Ð¼Ñ ÑпиÑкÑ.\"],\"Weq9zb\":[\"Загальне\"],\"Wfj7Sk\":[\"Вимкнути або увімкнути звуки сповіщень\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*спам*\"],\"WzMCru\":[\"Профіль користувача\"],\"X6S3lt\":[\"Пошук налаштувань, каналів, серверів...\"],\"XEHan5\":[\"Продовжити все одно\"],\"XI1+wb\":[\"Недійсний формат\"],\"XIXeuC\":[\"Повідомлення @\",[\"0\"]],\"XMS+k4\":[\"Почати приватне повідомлення\"],\"XWgxXq\":[\"Альбом\"],\"Xd7+IT\":[\"Відкріпити приватну розмову\"],\"Xm/s+u\":[\"Дисплей\"],\"Xp2n93\":[\"Показує медіа з надійного файлового хоста вашого сервера. Запити до зовнішніх сервісів не здійснюються.\"],\"XvjC4F\":[\"Збереження...\"],\"Y/qryO\":[\"Користувачів за вашим запитом не знайдено\"],\"YAqRpI\":[\"Реєстрація облікового запису \",[\"account\"],\" успішна: \",[\"message\"]],\"YEfzvP\":[\"Захищена тема (+t)\"],\"YQOn6a\":[\"Згорнути список учасників\"],\"YRCoE9\":[\"Оператор каналу\"],\"YURQaF\":[\"Переглянути профіль\"],\"YdBSvr\":[\"Керувати відображенням медіа та зовнішнього вмісту\"],\"Yj6U3V\":[\"Без центрального сервера:\"],\"YjvpGx\":[\"Займенники\"],\"YqH4l4\":[\"Без ключа\"],\"YyUPpV\":[\"Акаунт:\"],\"ZJSWfw\":[\"Повідомлення при від'єднанні від сервера\"],\"ZR1dJ4\":[\"Запрошення\"],\"ZdWg0V\":[\"Відкрити в браузері\"],\"ZhRBbl\":[\"Пошук повідомлень…\"],\"Zmcu3y\":[\"Розширені фільтри\"],\"a0bHay\":[\"Від'єднати <0>\",[\"0\"],\"0>?\"],\"a2/8e5\":[\"Тему встановлено після (хв тому)\"],\"aHKcKc\":[\"Попередня сторінка\"],\"aJTbXX\":[\"Пароль оператора\"],\"aQryQv\":[\"Шаблон вже існує\"],\"aW9pLN\":[\"Максимальна кількість користувачів у каналі. Залиште порожнім для відсутності обмежень.\"],\"ah4fmZ\":[\"Також показує попередній перегляд з YouTube, Vimeo, SoundCloud та подібних відомих сервісів.\"],\"aifXak\":[\"Немає медіа у цьому каналі\"],\"ap2zBz\":[\"Розслаблений\"],\"az8lvo\":[\"Вимк\"],\"azXSNo\":[\"Розгорнути список учасників\"],\"azdliB\":[\"Увійти в акаунт\"],\"b26wlF\":[\"вона/її\"],\"bD/+Ei\":[\"Строгий\"],\"bQ6BJn\":[\"Налаштуйте детальні правила захисту від флуду. Кожне правило вказує, яку активність відстежувати і які дії вживати при перевищенні порогів.\"],\"beV7+y\":[\"Користувач отримає запрошення приєднатися до \",[\"channelName\"],\".\"],\"bk84cH\":[\"Повідомлення про відсутність\"],\"bkHdLj\":[\"Додати IRC-сервер\"],\"bmQLn5\":[\"Додати правило\"],\"bv4cFj\":[\"Транспорт\"],\"bwRvnp\":[\"Дія\"],\"c8+EVZ\":[\"Підтверджений акаунт\"],\"cGYUlD\":[\"Медіа-перегляди не завантажені.\"],\"cLF98o\":[\"Показати коментарі (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"Користувачів немає\"],\"cSgpoS\":[\"Закріпити приватну розмову\"],\"cXeEKu\":[\"Це також закриє \",[\"0\"],\" прив'язаних мереж нижче.\"],\"cde3ce\":[\"Повідомлення <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"Скопіювати форматований вивід\"],\"cl/A5J\":[\"Ласкаво просимо до \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"Видалити\"],\"coPLXT\":[\"Ми не зберігаємо ваші IRC-комунікації на наших серверах\"],\"crYH/6\":[\"Програвач SoundCloud\"],\"cv5DQb\":[\"хост не вказано\"],\"d3sis4\":[\"Додати сервер\"],\"d9aN5k\":[\"Видалити \",[\"username\"],\" з каналу\"],\"dEgA5A\":[\"Скасувати\"],\"dGi1We\":[\"Відкріпити цю приватну розмову\"],\"dJVuyC\":[\"покинув \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"до\"],\"dXqxlh\":[\"<0>⚠️ Загроза безпеці!0> Це з'єднання може бути вразливим до перехоплення або атак «людина посередині».\"],\"da9Q/R\":[\"Змінено режими каналу\"],\"dhJN3N\":[\"Показати коментарі\"],\"dj2xTE\":[\"Відхилити сповіщення\"],\"dpCzmC\":[\"Налаштування захисту від флуду\"],\"e9dQpT\":[\"Бажаєте відкрити це посилання в новій вкладці?\"],\"ePK91l\":[\"Редагувати\"],\"eYBDuB\":[\"Завантажте зображення або вкажіть URL з необов'язковою підстановкою \",[\"size\"],\" для динамічного розміру\"],\"edBbee\":[\"Заблокувати \",[\"username\"],\" за маскою хоста (запобігає повторному входу з тієї ж IP/хоста)\"],\"ekfzWq\":[\"Налаштування користувача\"],\"elPDWs\":[\"Налаштуйте свій IRC-клієнт\"],\"eu2osY\":[\"<0>💡 Рекомендація:0> Продовжуйте лише якщо довіряєте цьому серверу і розумієте ризики. Уникайте передачі конфіденційної інформації або паролів через це з'єднання.\"],\"euEhbr\":[\"Натисніть, щоб приєднатися до \",[\"channel\"]],\"ez3vLd\":[\"Увімкнути багаторядкове введення\"],\"f0J5Ki\":[\"Зв'язок між серверами може використовувати незашифровані з'єднання\"],\"f9BHJk\":[\"Попередити користувача\"],\"fDOLLd\":[\"Канали не знайдено.\"],\"ffzDkB\":[\"Анонімна аналітика:\"],\"fq1GF9\":[\"Відображати, коли користувачі від'єднуються від сервера\"],\"gCldcN\":[\"Змінити колір акценту\"],\"gEF57C\":[\"Цей сервер підтримує лише один тип з'єднання\"],\"gJuLUI\":[\"Список ігнорування\"],\"gNzMrk\":[\"Поточний аватар\"],\"gjPWyO\":[\"Введіть нік...\"],\"gz6UQ3\":[\"Розгорнути\"],\"h6/IMX\":[\"Додайте свою першу мережу\"],\"h6razj\":[\"Маска виключення назви каналу\"],\"hG6jnw\":[\"Тема не встановлена\"],\"hG89Ed\":[\"Зображення\"],\"hYgDIe\":[\"СÑвоÑиÑи\"],\"hZ6znB\":[\"Порт\"],\"ha+Bz5\":[\"напр., 100:1440\"],\"he3ygx\":[\"ÐопÑÑваÑи\"],\"hehnjM\":[\"Кількість\"],\"hzdLuQ\":[\"Говорити можуть лише користувачі з голосом або вище\"],\"i0qMbr\":[\"Головна\"],\"iDNBZe\":[\"Сповіщення\"],\"iH8pgl\":[\"Назад\"],\"iL9SZg\":[\"Заблокувати користувача (за псевдонімом)\"],\"iNt+3c\":[\"Назад до зображення\"],\"iQvi+a\":[\"Не попереджати мене про низький рівень безпеки для цього сервера\"],\"iSLIjg\":[\"Підключитися\"],\"iWXkHH\":[\"Напів-оператор\"],\"iZeTtp\":[\"Хост сервера\"],\"idD8Ev\":[\"Збережено\"],\"iivqkW\":[\"Час входу\"],\"ij+Elv\":[\"Попередній перегляд зображення\"],\"ilIWp7\":[\"Перемкнути сповіщення\"],\"iuaqvB\":[\"Використовуйте * для шаблонів. Приклади: baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"Бот\"],\"j2DGR0\":[\"Блокування за маскою хоста\"],\"jA4uoI\":[\"Тема:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"Причина (необов'язково)\"],\"jUV7CU\":[\"Завантажити аватар\"],\"jW5Uwh\":[\"Контролюйте завантаження зовнішніх медіа. Вимк / Безпечно / Надійні джерела / Весь вміст.\"],\"jXzms5\":[\"Параметри вкладення\"],\"jZlrte\":[\"Колір\"],\"jfC/xh\":[\"Контакт\"],\"jywMpv\":[\"#нова-назва-каналу\"],\"k112DD\":[\"Завантажити старіші повідомлення\"],\"k3ID0F\":[\"Фільтрувати учасників…\"],\"k65gsE\":[\"Детальний огляд\"],\"k7Zgob\":[\"Скасувати підключення\"],\"kAVx5h\":[\"Запрошень не знайдено\"],\"kCLEPU\":[\"Підключено до\"],\"kF5LKb\":[\"Ігноровані шаблони:\"],\"kGeOx/\":[\"Приєднатися до \",[\"0\"]],\"kITKr8\":[\"Завантаження режимів каналу...\"],\"kPpPsw\":[\"Ви є IRC-оператором\"],\"kWJmRL\":[\"Ви\"],\"kfcRb0\":[\"Аватар\"],\"kjMqSj\":[\"Скопіювати JSON\"],\"krViRy\":[\"Натисніть для копіювання як JSON\"],\"ks71ra\":[\"Винятки\"],\"kw4lRv\":[\"Напів-оператор каналу\"],\"kxgIRq\":[\"Виберіть або додайте канал для початку.\"],\"ky6dWe\":[\"Попередній перегляд аватара\"],\"l+GxCv\":[\"Завантаження каналів...\"],\"l+IUVW\":[\"Верифікація облікового запису \",[\"account\"],\" успішна: \",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"one\":[\"перепідключився\"],\"few\":[\"перепідключився \",[\"reconnectCount\"],\" рази\"],\"many\":[\"перепідключився \",[\"reconnectCount\"],\" разів\"],\"other\":[\"перепідключився \",[\"reconnectCount\"],\" рази\"]}]],\"l1l8sj\":[[\"0\"],\" д ÑомÑ\"],\"l5NhnV\":[\"#канал (необов'Ñзково)\"],\"l5jmzx\":[[\"0\"],\" та \",[\"1\"],\" пишуть...\"],\"lCF0wC\":[\"ÐновиÑи\"],\"lHy8N5\":[\"Завантаження більше каналів...\"],\"lasgrr\":[\"викоÑиÑÑано\"],\"lbpf14\":[\"Приєднатися до \",[\"value\"]],\"lfFsZ4\":[\"Канали\"],\"lkNdiH\":[\"Ім'я акаунту\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"Завантажити зображення\"],\"loQxaJ\":[\"Я повернувся\"],\"lvfaxv\":[\"ГОЛОВНА\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"Додати\"],\"m8flAk\":[\"Попередній перегляд (ще не завантажено)\"],\"mEPxTp\":[\"<0>⚠️ Будьте обережні!0> Відкривайте лише посилання з надійних джерел. Шкідливі посилання можуть порушити вашу безпеку або конфіденційність.\"],\"mHGdhG\":[\"Інформація про сервер\"],\"mHS8lb\":[\"Повідомлення #\",[\"0\"]],\"mMYBD9\":[\"Широкий - ширша область захисту\"],\"mTGsPd\":[\"Тема каналу\"],\"mU8j6O\":[\"Без зовнішніх повідомлень (+n)\"],\"mZp8FL\":[\"Автоматичне повернення до одного рядка\"],\"mdQu8G\":[\"ВашПсевдонім\"],\"miSSBQ\":[\"Коментарі (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"Користувач автентифікований\"],\"mwtcGl\":[\"Закрити коментарі\"],\"myL0MR\":[\"Видалити цю мережу?\"],\"mzI/c+\":[\"Завантажити\"],\"n3fGRk\":[\"встановлено \",[\"0\"]],\"n5+j9l\":[\"напр. <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"Розслаблений - менш агресивний захист\"],\"nNflMD\":[\"Покинути канал\"],\"nPXkBi\":[\"Завантаження даних WHOIS...\"],\"nQnxxF\":[\"Повідомлення #\",[\"0\"],\" (Shift+Enter для нового рядка)\"],\"nWMRxa\":[\"Відкріпити\"],\"nkC032\":[\"Без профілю флуду\"],\"o69z4d\":[\"Надіслати попереджувальне повідомлення \",[\"username\"]],\"o9ylQi\":[\"Знайдіть GIF для початку\"],\"oFGkER\":[\"Повідомлення сервера\"],\"oOi11l\":[\"Прокрутити вниз\"],\"oPYIL5\":[\"меÑежа\"],\"oQEzQR\":[\"Нове DM\"],\"oXOSPE\":[\"Онлайн\"],\"oal760\":[\"Можливі атаки «людина посередині» на з'єднання сервера\"],\"oeqmmJ\":[\"Надійні джерела\"],\"optX0N\":[[\"0\"],\" год ÑомÑ\"],\"ovBPCi\":[\"За замовчуванням\"],\"p0Z69r\":[\"Шаблон не може бути порожнім\"],\"p1KgtK\":[\"Не вдалося завантажити аудіо\"],\"p59pEv\":[\"Детальніше\"],\"p7sRI6\":[\"Повідомляти інших, коли ви друкуєте\"],\"pBm1od\":[\"Секретний канал\"],\"pNmiXx\":[\"Ваш псевдонім за замовчуванням для всіх серверів\"],\"pUUo9G\":[\"Хост:\"],\"pVGPmz\":[\"Пароль акаунту\"],\"peNE68\":[\"Постійний\"],\"plhHQt\":[\"Немає даних\"],\"pm6+q5\":[\"Попередження про безпеку\"],\"pn5qSs\":[\"Додаткова інформація\"],\"q0cR4S\":[\"тепер відомий як **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"Канал не відображатиметься у командах LIST або NAMES\"],\"qLpTm/\":[\"Видалити реакцію \",[\"emoji\"]],\"qVkGWK\":[\"Закріпити\"],\"qY8wNa\":[\"Головна сторінка\"],\"qb0xJ7\":[\"Використовуйте шаблони: * відповідає будь-якій послідовності, ? відповідає будь-якому одному символу. Приклади: nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"Ключ каналу (+k)\"],\"qtoOYG\":[\"Без обмежень\"],\"r1W2AS\":[\"Зображення з файлового хостингу\"],\"rIPR2O\":[\"Тему встановлено до (хв тому)\"],\"rMMSYo\":[\"Максимальна довжина: \",[\"0\"]],\"rWtzQe\":[\"Мережа розділилась і возз'єдналась. ✅\"],\"rYG2u6\":[\"Будь ласка, зачекайте...\"],\"rdUucN\":[\"Попередній перегляд\"],\"rjGI/Q\":[\"Конфіденційність\"],\"rk8iDX\":[\"Завантаження GIF...\"],\"rn6SBY\":[\"Увімкнути звук\"],\"s/UKqq\":[\"Виключено з каналу\"],\"s7oqXR\":[\"Виберіть мережу\"],\"s8cATI\":[\"приєднався до \",[\"channelName\"]],\"sCO9ue\":[\"З'єднання з <0>\",[\"serverName\"],\"0> має такі проблеми безпеки:\"],\"sGH11W\":[\"Сервер\"],\"sHI1H+\":[\"тепер відомий як **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" запросив вас приєднатися до \",[\"channel\"]],\"sUBSbK\":[\"Поки що немає висхідних мереж.\"],\"sby+1/\":[\"Натисніть для копіювання\"],\"sfN25C\":[\"Ваше справжнє або повне ім'я\"],\"sliuzR\":[\"Відкрити посилання\"],\"sqrO9R\":[\"Власні згадки\"],\"sr6RdJ\":[\"Багаторядковий на Shift+Enter\"],\"swrCpB\":[\"Канал перейменовано з \",[\"oldName\"],\" на \",[\"newName\"],\" користувачем \",[\"user\"],[\"0\"]],\"sxkWRg\":[\"Додаткові\"],\"t/YqKh\":[\"Видалити\"],\"t47eHD\":[\"Ваш унікальний ідентифікатор на цьому сервері\"],\"tAkAh0\":[\"URL з необов'язковою підстановкою \",[\"size\"],\" для динамічного розміру. Приклад: https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"Показати або приховати бічну панель зі списком каналів\"],\"tfDRzk\":[\"Зберегти\"],\"tiBsJk\":[\"покинув \",[\"channelName\"]],\"tt4/UD\":[\"вийшов (\",[\"reason\"],\")\"],\"u0TcnO\":[\"Псевдонім {nick} вже використовується, повторна спроба з {newNick}\"],\"u0a8B4\":[\"Автентифікуватися як IRC-оператор для адміністративного доступу\"],\"u0rWFU\":[\"Створено після (хв тому)\"],\"u72w3t\":[\"Користувачі та шаблони для ігнорування\"],\"u7jc2L\":[\"вийшов\"],\"uAQUqI\":[\"Статус\"],\"uB85T3\":[\"Помилка збереження: \",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC сервери:\"],\"ukyW4o\":[\"ÐаÑÑ Ð·Ð°Ð¿ÑоÑÑвалÑÐ½Ñ Ð¿Ð¾ÑиланнÑ\"],\"usSSr/\":[\"Рівень масштабу\"],\"v7uvcf\":[\"Програма:\"],\"vE8kb+\":[\"Використовуйте Shift+Enter для нового рядка (Enter надсилає)\"],\"vERlcd\":[\"Профіль\"],\"vK0RL8\":[\"Без теми\"],\"vSJd18\":[\"Відео\"],\"vXIe7J\":[\"Мова\"],\"vaHYxN\":[\"Справжнє ім'я\"],\"vhjbKr\":[\"Відсутній\"],\"w/nogd\":[[\"0\"],\" мережа\",[\"1\"],\" — оберіть одну для приєднання\"],\"w4NYox\":[\"клієнт \",[\"title\"]],\"w8xQRx\":[\"Недійсне значення\"],\"wFjjxZ\":[\"був кікнутий з \",[\"channelName\"],\" користувачем \",[\"username\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"Винятків з заборон не знайдено\"],\"wPrGnM\":[\"Адміністратор каналу\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"Відображати, коли користувачі входять або виходять з каналів\"],\"whqZ9r\":[\"Додаткові слова або фрази для виділення\"],\"wm7RV4\":[\"Звук сповіщення\"],\"wz/Yoq\":[\"Ваші повідомлення можуть бути перехоплені при передачі між серверами\"],\"x3+y8b\":[\"СÑÑлÑки лÑдей заÑеÑÑÑÑÑвалоÑÑ ÑеÑез Ñе поÑиланнÑ\"],\"xCJdfg\":[\"Очистити\"],\"xOTzt5\":[\"Ñойно\"],\"xUHRTR\":[\"Автоматично автентифікуватися як оператор при підключенні\"],\"xWHwwQ\":[\"Блокування\"],\"xYilR2\":[\"Медіа\"],\"xbi8D6\":[\"Цей ÑеÑÐ²ÐµÑ Ð½Ðµ пÑдÑÑимÑÑ Ð·Ð°Ð¿ÑоÑÑвалÑÐ½Ñ Ð¿Ð¾ÑÐ¸Ð»Ð°Ð½Ð½Ñ (можливÑÑÑÑ<0>obby.world/invitation0>не оголоÑена). Ðи вÑе Ñе можеÑе ноÑмалÑно ÑпÑлкÑваÑиÑÑ; ÑÑ Ð¿Ð°Ð½ÐµÐ»Ñ Ð¿ÑизнаÑена Ð´Ð»Ñ Ð¼ÐµÑеж на оÑÐ½Ð¾Ð²Ñ obbyircd.\"],\"xceQrO\":[\"Підтримуються лише захищені websocket-з'єднання\"],\"xdtXa+\":[\"назва-каналу\"],\"xfXC7q\":[\"Текстові канали\"],\"xlCYOE\":[\"Отримання більше повідомлень...\"],\"xlhswE\":[\"Мінімальне значення: \",[\"0\"]],\"xq97Ci\":[\"Додати слово або фразу...\"],\"xuRqRq\":[\"Ліміт клієнтів (+l)\"],\"xwF+7J\":[[\"0\"],\" пише...\"],\"y1eoq1\":[\"ÐопÑÑваÑи поÑиланнÑ\"],\"yJztBY\":[\"Видалити мережу\"],\"yNeucF\":[\"Цей сервер не підтримує розширені метадані профілю (розширення IRCv3 METADATA). Додаткові поля, такі як аватар, відображуване ім'я та статус, недоступні.\"],\"yPlrca\":[\"Аватар каналу\"],\"yQE2r9\":[\"Завантаження\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Ім'я оператора\"],\"yYOzWD\":[\"логи\"],\"yfx9Re\":[\"Пароль IRC оператора\"],\"ygCKqB\":[\"Зупинити\"],\"ymDxJx\":[\"Ім'я IRC оператора\"],\"yrpRsQ\":[\"Сортувати за назвою\"],\"yz7wBu\":[\"Закрити\"],\"zJw+jA\":[\"встановлює режим: \",[\"0\"]],\"zbymaY\":[[\"0\"],\" Ñ
в ÑомÑ\"],\"zebeLu\":[\"Введіть ім'я оператора\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/uk/messages.po b/src/locales/uk/messages.po
index 54c41b2a..f913da2e 100644
--- a/src/locales/uk/messages.po
+++ b/src/locales/uk/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - Привносимо IRC у майбутнє"
msgid "— open in viewer"
msgstr "— відкрити у переглядачі"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(успадковано)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(без змін)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} та {1} пишуть..."
msgid "{0} is typing..."
msgstr "{0} пише..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} мережа{1} — оберіть одну для приєднання"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -69,17 +85,17 @@ msgstr "{0}, {1}, {2} та ще {3} пишуть..."
#. placeholder {0}: Math.floor(secs / 86400)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}d ago"
-msgstr "{0} д тому"
+msgstr "{0} д ÑомÑ"
#. placeholder {0}: Math.floor(secs / 3600)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}h ago"
-msgstr "{0} год тому"
+msgstr "{0} год ÑомÑ"
#. placeholder {0}: Math.floor(secs / 60)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}m ago"
-msgstr "{0} хв тому"
+msgstr "{0} Ñ
в ÑомÑ"
#: src/lib/eventGrouping.ts
msgid "{c, plural, one {1 time} other {{c} times}}"
@@ -115,7 +131,7 @@ msgstr "*спам*"
#: src/components/ui/InvitationsPanel.tsx
msgid "#channel (optional)"
-msgstr "#канал (необов'язково)"
+msgstr "#канал (необов'Ñзково)"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "#new-channel-name"
@@ -205,6 +221,12 @@ msgstr "Додати маску запрошення (напр., nick!*@*, *!*@h
msgid "Add IRC Server"
msgstr "Додати IRC-сервер"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "Додати мережу"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "Додати правило"
msgid "Add Server"
msgstr "Додати сервер"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "Додайте свою першу мережу"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "Детальніше"
@@ -377,6 +403,10 @@ msgstr "Назад"
msgid "Back to image"
msgstr "Назад до зображення"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "Назад до списку мереж"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "Заблокувати {username} за маскою хоста (запобігає повторному входу з тієї ж IP/хоста)"
@@ -424,6 +454,10 @@ msgstr "Переглянути всі канали на сервері"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "Скасувати підключення"
msgid "Cancel reply"
msgstr "Скасувати відповідь"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "Змінити колір акценту"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "Змінити назву каналу (лише оператори)"
@@ -564,7 +603,7 @@ msgstr "Очистити пошук"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "Натисніть ще раз для підтвердження"
+msgstr "ÐаÑиÑнÑÑÑ Ñе Ñаз Ð´Ð»Ñ Ð¿ÑдÑвеÑдженнÑ"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "Ліміт клієнтів (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -666,9 +707,10 @@ msgstr "Налаштувати звуки сповіщень та виділен
#: src/components/ui/InvitationsPanel.tsx
msgid "Confirm?"
-msgstr "Підтвердити?"
+msgstr "ÐÑдÑвеÑдиÑи?"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "Підключитися"
@@ -711,11 +753,11 @@ msgstr "Скопійовано"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy"
-msgstr "Копіювати"
+msgstr "ÐопÑÑваÑи"
#: src/components/ui/RawLogViewer.tsx
msgid "Copy all"
-msgstr "Копіювати все"
+msgstr "ÐопÑÑваÑи вÑе"
#: src/components/message/JsonLogMessage.tsx
msgid "Copy entire JSON"
@@ -731,7 +773,7 @@ msgstr "Скопіювати JSON"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy link"
-msgstr "Копіювати посилання"
+msgstr "ÐопÑÑваÑи поÑиланнÑ"
#: src/components/ui/ExternalLinkWarningModal.tsx
msgid "Copy URL"
@@ -739,15 +781,15 @@ msgstr "Копіювати URL"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create"
-msgstr "Створити"
+msgstr "СÑвоÑиÑи"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "Створити нове запрошувальне посилання"
+msgstr "СÑвоÑиÑи нове запÑоÑÑвалÑне поÑиланнÑ"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "Створюйте та керуйте своїми запрошувальними посиланнями"
+msgstr "СÑвоÑÑйÑе Ñа кеÑÑйÑе ÑвоÑми запÑоÑÑвалÑними поÑиланнÑми"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,27 +856,52 @@ msgstr "Видалити канал"
msgid "Delete message"
msgstr "Видалити повідомлення"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "Видалити мережу"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "Видалити приватний чат"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "Видалити це запрошення"
+msgstr "ÐидалиÑи Ñе запÑоÑеннÑ"
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "Видалити це повідомлення? Цю дію неможливо скасувати."
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "Видалити цю мережу?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "Опис (необов'язково, напр. \"Бета-тестувальники Q3\")"
+msgstr "ÐÐ¿Ð¸Ñ (необов'Ñзково, напÑ. \"ÐеÑа-ÑеÑÑÑвалÑники Q3\")"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "Від'єднатися"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "Від'єднати <0>{0}0>?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "Від'єднатися від bouncer soju?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "Від'єднати мережу?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "Дослідити"
@@ -891,20 +958,31 @@ msgstr "Завантажити"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "Перетягніть файли для завантаження"
+msgstr "ÐеÑеÑÑгнÑÑÑ Ñайли Ð´Ð»Ñ Ð·Ð°Ð²Ð°Ð½ÑаженнÑ"
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "напр. <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "напр., 100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "Редагувати"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "Редагувати {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "Редагувати профіль"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "ГОЛОВНА"
msgid "Homepage"
msgstr "Головна сторінка"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "Хост"
@@ -1285,7 +1364,7 @@ msgstr "Приєднався до каналу"
#: src/components/ui/InvitationsPanel.tsx
msgid "just now"
-msgstr "щойно"
+msgstr "Ñойно"
#: src/components/ui/ModerationModal.tsx
#: src/components/ui/UserContextMenu.tsx
@@ -1323,7 +1402,7 @@ msgstr "Покинути канал"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "Залиште поле каналу порожнім для загального запрошення в мережу. Опис призначений лише для ваших записів — видимий лише вам у цьому списку."
+msgstr "ÐалиÑÑе поле ÐºÐ°Ð½Ð°Ð»Ñ Ð¿Ð¾ÑожнÑм Ð´Ð»Ñ Ð·Ð°Ð³Ð°Ð»Ñного запÑоÑÐµÐ½Ð½Ñ Ð² меÑежÑ. ÐÐ¿Ð¸Ñ Ð¿ÑизнаÑений лиÑе Ð´Ð»Ñ Ð²Ð°ÑиÑ
запиÑÑв â видимий лиÑе вам Ñ ÑÑÐ¾Ð¼Ñ ÑпиÑкÑ."
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "Покинув канал"
msgid "Let others know when you are typing"
msgstr "Повідомляти інших, коли ви друкуєте"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "Попередній перегляд посилання"
@@ -1375,6 +1458,10 @@ msgstr "Завантаження GIF..."
msgid "Loading more channels..."
msgstr "Завантаження більше каналів..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "Завантаження мереж з вашого баунсера…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "Завантаження даних WHOIS..."
@@ -1563,12 +1650,23 @@ msgstr "Ім'я:"
#: src/components/ui/InvitationsPanel.tsx
msgid "network"
-msgstr "мережа"
+msgstr "меÑежа"
+
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "Мережа, прив'язана через bouncer soju"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "Назва мережі"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "Мережі на {0}"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "Нове DM"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host (напр., spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "Файл не вибрано"
msgid "No flood profile"
msgstr "Без профілю флуду"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "хост не вказано"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "Запрошень не знайдено"
@@ -1672,7 +1775,7 @@ msgstr "Медіа-перегляди не завантажені."
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "Поки що не зафіксовано необробленого IRC-трафіку. Спробуйте підключитися або надіслати повідомлення."
+msgstr "Ðоки Ñо не заÑÑкÑовано необÑобленого IRC-ÑÑаÑÑкÑ. СпÑобÑйÑе пÑдклÑÑиÑиÑÑ Ð°Ð±Ð¾ надÑÑлаÑи повÑдомленнÑ."
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "Результатів не знайдено"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "Жоден сервер не вибрано. Спочатку оберіть сервер з бічної панелі; запрошувальні посилання керуються окремо для кожного сервера."
+msgstr "Ðоден ÑеÑÐ²ÐµÑ Ð½Ðµ вибÑано. СпоÑаÑÐºÑ Ð¾Ð±ÐµÑÑÑÑ ÑеÑÐ²ÐµÑ Ð· бÑÑÐ½Ð¾Ñ Ð¿Ð°Ð½ÐµÐ»Ñ; запÑоÑÑвалÑÐ½Ñ Ð¿Ð¾ÑÐ¸Ð»Ð°Ð½Ð½Ñ ÐºÐµÑÑÑÑÑÑÑ Ð¾ÐºÑемо Ð´Ð»Ñ ÐºÐ¾Ð¶Ð½Ð¾Ð³Ð¾ ÑеÑвеÑа."
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "Тема не встановлена"
msgid "No unread mentions or messages"
msgstr "Немає непрочитаних згадувань або повідомлень"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "Поки що немає висхідних мереж."
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "Користувачів немає"
@@ -1784,6 +1891,10 @@ msgstr "Ой! Розрив мережі! ⚠️"
msgid "Op"
msgstr "Оп"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "Відкрити"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "Відкрити налаштування конфігурації каналу"
@@ -1887,6 +1998,10 @@ msgstr "Закріпити приватну розмову"
msgid "Pin this private message conversation"
msgstr "Закріпити цю приватну розмову"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "Незашифровано"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "ПП користувачу"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "Порт"
@@ -2006,6 +2122,7 @@ msgstr "відреагував на це повідомлення"
msgid "Read more"
msgstr "Читати далі"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "Причина"
msgid "Reason (optional)"
msgstr "Причина (необов'язково)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "Перепідключитися до сервера"
@@ -2031,7 +2149,7 @@ msgstr "Перепідключитися до сервера"
#: src/components/ui/InvitationsPanel.tsx
#: src/components/ui/InvitationsPanel.tsx
msgid "Refresh"
-msgstr "Оновити"
+msgstr "ÐновиÑи"
#: src/components/ui/AddServerModal.tsx
msgid "Register for an account"
@@ -2095,6 +2213,7 @@ msgstr "Правила"
msgid "Safe"
msgstr "Безпечно"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "Перемотати"
msgid "Select a channel"
msgstr "Вибрати канал"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "Виберіть мережу"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "Вибрати учасника"
@@ -2276,6 +2399,10 @@ msgstr "Оператори сервера в мережі можуть поте
msgid "Server Password"
msgstr "Пароль сервера"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "Пароль сервера (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "Зв'язок між серверами може використовувати незашифровані з'єднання"
@@ -2380,6 +2507,12 @@ msgstr "Час входу"
msgid "Software:"
msgstr "Програма:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "bouncer soju (керування)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "Сортувати за назвою"
@@ -2457,7 +2590,11 @@ msgstr "Термін дії цього зображення минув"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "Стільки людей зареєструвалося через це посилання"
+msgstr "СÑÑлÑки лÑдей заÑеÑÑÑÑÑвалоÑÑ ÑеÑез Ñе поÑиланнÑ"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "Це видалить мережу з вашого bouncer soju. Щоб використати її знову, вам потрібно буде додати її заново."
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "Цей сервер не підтримує розширені мета
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "Цей сервер не підтримує запрошувальні посилання (можливість<0>obby.world/invitation0>не оголошена). Ви все ще можете нормально спілкуватися; ця панель призначена для мереж на основі obbyircd."
+msgstr "Цей ÑеÑÐ²ÐµÑ Ð½Ðµ пÑдÑÑимÑÑ Ð·Ð°Ð¿ÑоÑÑвалÑÐ½Ñ Ð¿Ð¾ÑÐ¸Ð»Ð°Ð½Ð½Ñ (можливÑÑÑÑ<0>obby.world/invitation0>не оголоÑена). Ðи вÑе Ñе можеÑе ноÑмалÑно ÑпÑлкÑваÑиÑÑ; ÑÑ Ð¿Ð°Ð½ÐµÐ»Ñ Ð¿ÑизнаÑена Ð´Ð»Ñ Ð¼ÐµÑеж на оÑÐ½Ð¾Ð²Ñ obbyircd."
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "Цей сервер підтримує лише один тип з'єднання"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "Це також закриє {0} прив'язаних мереж нижче."
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "Це також закриє прив'язану мережу нижче."
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "Час (хв)"
@@ -2479,6 +2625,10 @@ msgstr "Час (хв)"
msgid "Time Window (seconds)"
msgstr "Часове вікно (секунди)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "Тема:"
msgid "Total: {0}"
msgstr "Всього: {0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "Транспорт"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "Надійні джерела"
@@ -2615,7 +2769,7 @@ msgstr "Використовуйте шаблони: * відповідає бу
#: src/components/ui/InvitationsPanel.tsx
msgid "used"
-msgstr "використано"
+msgstr "викоÑиÑÑано"
#: src/components/message/JsonLogMessage.tsx
#: src/components/ui/UserProfileModal.tsx
@@ -2641,6 +2795,7 @@ msgstr "Профіль користувача"
msgid "User Settings"
msgstr "Налаштування користувача"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "Широкий - ширша область захисту"
msgid "Will default to 'no reason' if left empty"
msgstr "За замовчуванням буде 'без причини', якщо залишити порожнім"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "Так, видалити"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "У вас є незбережені зміни. Ви впевнені,
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "Ви ще не створили жодного запрошувального посилання. Скористайтеся формою вище, щоб створити перше."
+msgstr "Ðи Ñе не ÑÑвоÑили жодного запÑоÑÑвалÑного поÑиланнÑ. СкоÑиÑÑайÑеÑÑ ÑоÑÐ¼Ð¾Ñ Ð²Ð¸Ñе, Ñоб ÑÑвоÑиÑи пеÑÑе."
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "Ви запросили {target} приєднатися до {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "Ви підключені до <0>{0}0>."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "Пароль вашого акаунту для автентифікації"
@@ -2822,13 +2986,17 @@ msgstr "Пароль вашого акаунту для автентифікац
msgid "Your account username for authentication"
msgstr "Ім'я користувача вашого акаунту для автентифікації"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "Ваш баунсер ще не має жодних мереж. Додайте одну, щоб почати."
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "Ваш псевдонім за замовчуванням для всіх серверів"
#: src/components/ui/InvitationsPanel.tsx
msgid "Your invite links"
-msgstr "Ваші запрошувальні посилання"
+msgstr "ÐаÑÑ Ð·Ð°Ð¿ÑоÑÑвалÑÐ½Ñ Ð¿Ð¾ÑиланнÑ"
#: src/components/ui/UserSettings.tsx
msgid "Your messages and settings are stored locally on your device"
diff --git a/src/locales/zh-TW/messages.mjs b/src/locales/zh-TW/messages.mjs
index 12dd39cf..9d5aab3e 100644
--- a/src/locales/zh-TW/messages.mjs
+++ b/src/locales/zh-TW/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"無效的模式格式,請使用 nick!user@host 格式(允許萬用字元 *)\"],\"+6NQQA\":[\"通用支持频道\"],\"+6NyRG\":[\"客戶端\"],\"+K0AvT\":[\"断开连接\"],\"+cyFdH\":[\"標記為離開時的預設訊息\"],\"+mVPqU\":[\"在訊息中渲染 Markdown 格式\"],\"+vqCJH\":[\"您的帳戶認證使用者名稱\"],\"+yPBXI\":[\"選擇檔案\"],\"+zy2Nq\":[\"類型\"],\"/09cao\":[\"链路安全级别较低(级别 \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"頻道外的使用者無法傳送訊息\"],\"/4C8U0\":[\"全部複製\"],\"/6BzZF\":[\"切换成员列表\"],\"/AkXyp\":[\"確認?\"],\"/TNOPk\":[\"使用者已離開\"],\"/XQgft\":[\"探索\"],\"/cF7Rs\":[\"音量\"],\"/dqduX\":[\"下一页\"],\"/fc3q4\":[\"所有内容\"],\"/kISDh\":[\"啟用通知聲音\"],\"/n04sB\":[\"踢出\"],\"/rTz0M\":[\"音訊\"],\"/rfkZe\":[\"提及和訊息時播放聲音\"],\"0/0ZGA\":[\"頻道名稱遮罩\"],\"0D6j7U\":[\"了解更多自訂規則 →\"],\"0XsHcR\":[\"踢出用户\"],\"0ZpE//\":[\"依使用者數排序\"],\"0bEPwz\":[\"设置为离开\"],\"0dGkPt\":[\"展开频道列表\"],\"0gS7M5\":[\"显示名称\"],\"0kS+M8\":[\"範例網路\"],\"0rgoY7\":[\"僅連線至您選擇的伺服器\"],\"0wdd7X\":[\"加入\"],\"0wkVYx\":[\"私人訊息\"],\"111uHX\":[\"链接预览\"],\"196EG4\":[\"删除私聊\"],\"1DSr1i\":[\"注册账户\"],\"1O/24y\":[\"切换频道列表\"],\"1VPJJ2\":[\"外部链接警告\"],\"1ZC/dv\":[\"沒有未讀提及或訊息\"],\"1pO1zi\":[\"服务器名称为必填项\"],\"1uwfzQ\":[\"查看频道主题\"],\"268g7c\":[\"输入显示名称\"],\"2F9+AZ\":[\"尚未擷取到任何原始 IRC 流量。請嘗試連線或傳送訊息。\"],\"2FOFq1\":[\"网络上的服务器管理员可能读取您的消息\"],\"2FYpfJ\":[\"更多\"],\"2HF1Y2\":[[\"inviter\"],\" 邀請了 \",[\"target\"],\" 加入 \",[\"channel\"]],\"2I70QL\":[\"查看用户资料信息\"],\"2QYdmE\":[\"用戶:\"],\"2QpEjG\":[\"已離開\"],\"2YE223\":[\"发消息到 #\",[\"0\"],\"(Enter 换行,Shift+Enter 发送)\"],\"2bimFY\":[\"使用服务器密码\"],\"2iTmdZ\":[\"本機儲存:\"],\"2odkwe\":[\"嚴格 – 更強的保護\"],\"2uDhbA\":[\"输入要邀请的用户名\"],\"2ygf/L\":[\"← 返回\"],\"2zEgxj\":[\"搜索 GIF...\"],\"3RdPhl\":[\"重命名频道\"],\"3THokf\":[\"有發言權的使用者\"],\"3TSz9S\":[\"最小化\"],\"3jBDvM\":[\"頻道顯示名稱\"],\"3ryuFU\":[\"可選的當機報告以改善應用程式\"],\"3uBF/8\":[\"关闭查看器\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"輸入帳戶名稱...\"],\"4/Rr0R\":[\"邀请用户加入当前频道\"],\"4EZrJN\":[\"規則\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"洪水設定檔 (+F)\"],\"4RZQRK\":[\"你在做什麼?\"],\"4hfTrB\":[\"昵称\"],\"4n99LO\":[\"已在 \",[\"0\"]],\"4t6vMV\":[\"短訊息自動切換至單行\"],\"4vsHmf\":[\"時間(分鐘)\"],\"5+INAX\":[\"醒目提示提及您的訊息\"],\"5R5Pv/\":[\"Oper 用户名\"],\"678PKt\":[\"网络名称\"],\"6Aih4U\":[\"离线\"],\"6CO3WE\":[\"加入頻道需要密碼,留空可移除金鑰。\"],\"6HhMs3\":[\"退出消息\"],\"6V3Ea3\":[\"已复制\"],\"6lGV3K\":[\"收起\"],\"6yFOEi\":[\"輸入 oper 密碼...\"],\"7+IHTZ\":[\"未選擇檔案\"],\"73hrRi\":[\"nick!user@host(例如:spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"发送私信\"],\"7U1W7c\":[\"非常宽松\"],\"7Y1YQj\":[\"真實姓名:\"],\"7YHArF\":[\"— 在檢視器中開啟\"],\"7fjnVl\":[\"搜索用户...\"],\"7jL88x\":[\"刪除此訊息?此操作無法復原。\"],\"7nGhhM\":[\"您在想什么?\"],\"7sEpu1\":[\"成员 — \",[\"0\"]],\"7sNhEz\":[\"用户名\"],\"8H0Q+x\":[\"了解更多設定檔 →\"],\"8Phu0A\":[\"顯示使用者更改暱稱的事件\"],\"8XTG9e\":[\"输入 oper 密码\"],\"8XsV2J\":[\"重新傳送\"],\"8ZsakT\":[\"密码\"],\"8kR84m\":[\"您即将打开一个外部链接:\"],\"8lCgih\":[\"移除規則\"],\"8o3dPc\":[\"拖放檔案以上傳\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"other\":[\"已加入 \",[\"joinCount\"],\" 次\"]}]],\"9BMLnJ\":[\"重新连接服务器\"],\"9OEgyT\":[\"添加表情\"],\"9PQ8m2\":[\"G-Line(全域封禁)\"],\"9Qs99X\":[\"電子郵件:\"],\"9QupBP\":[\"删除规则\"],\"9bG48P\":[\"傳送中\"],\"9f5f0u\":[\"有隱私問題?請聯絡我們:\"],\"9unqs3\":[\"離開:\"],\"9v3hwv\":[\"未找到服务器。\"],\"9zb2WA\":[\"连接中\"],\"A1taO8\":[\"搜索\"],\"A2adVi\":[\"傳送正在輸入通知\"],\"A9Rhec\":[\"頻道名稱\"],\"AWOSPo\":[\"放大\"],\"AXSpEQ\":[\"連線時獲取 Oper\"],\"AeXO77\":[\"账户\"],\"AhNP40\":[\"跳转\"],\"Ai2U7L\":[\"主機\"],\"AjBQnf\":[\"已更改昵称\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"取消回复\"],\"ApSx0O\":[\"找到 \",[\"0\"],\" 則符合「\",[\"searchQuery\"],\"」的訊息\"],\"AxPAXW\":[\"未找到結果\"],\"AyNqAB\":[\"在聊天中顯示所有伺服器事件\"],\"B/QqGw\":[\"暂时离开\"],\"B8AaMI\":[\"此欄位為必填\"],\"BA2c49\":[\"伺服器不支援進階 LIST 篩選\"],\"BDKt3I\":[[\"0\"],\"、\",[\"1\"],\"、\",[\"2\"],\" 以及另外 \",[\"3\"],\" 人正在输入...\"],\"BGul2A\":[\"您有未保存的更改。确定要关闭而不保存吗?\"],\"BIf9fi\":[\"您的狀態訊息\"],\"BPm98R\":[\"尚未選擇伺服器。請先從側邊欄選擇一個伺服器;邀請連結是依伺服器管理的。\"],\"BZz3md\":[\"您的個人網站\"],\"Bgm/H7\":[\"允許輸入多行文字\"],\"BiQIl1\":[\"置顶此私信对话\"],\"BlNZZ2\":[\"点击跳转到消息\"],\"Bowq3c\":[\"只有管理員可以更改頻道主題\"],\"Btozzp\":[\"此图片已过期\"],\"Bycfjm\":[\"總計:\",[\"0\"]],\"C6IBQc\":[\"複製完整 JSON\"],\"C9L9wL\":[\"資料收集\"],\"CDq4wC\":[\"管理用户\"],\"CHVRxG\":[\"发消息给 @\",[\"0\"],\"(Shift+Enter 换行)\"],\"CN9zdR\":[\"Oper 用户名和密码为必填项\"],\"CW3sYa\":[\"添加表情 \",[\"emoji\"]],\"CaAkqd\":[\"顯示退出事件\"],\"CbvaYj\":[\"依暱稱封禁\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"选择频道\"],\"CsekCi\":[\"普通\"],\"D+NlUC\":[\"系统\"],\"D28t6+\":[\"已加入並退出\"],\"DB8zMK\":[\"套用\"],\"DBcWHr\":[\"自訂通知音效檔\"],\"DTy9Xw\":[\"媒體預覽\"],\"Dj4pSr\":[\"选择一个安全密码\"],\"Du+zn+\":[\"搜尋中...\"],\"Du2T2f\":[\"未找到設定\"],\"DwsSVQ\":[\"套用篩選並重新整理\"],\"E3W/zd\":[\"預設暱稱\"],\"E6nRW7\":[\"复制链接\"],\"E703RG\":[\"模式:\"],\"EAeu1Z\":[\"傳送邀請\"],\"EFKJQT\":[\"設定\"],\"EGPQBv\":[\"自訂洪水規則 (+f)\"],\"ELik0r\":[\"查看完整隱私權政策\"],\"EPbeC2\":[\"查看或编辑频道主题\"],\"EQCDNT\":[\"輸入 oper 用戶名...\"],\"EUvulZ\":[\"找到 1 則符合「\",[\"searchQuery\"],\"」的訊息\"],\"EatZYJ\":[\"下一张图片\"],\"EdQY6l\":[\"無\"],\"EnqLYU\":[\"搜索服务器...\"],\"F0OKMc\":[\"编辑服务器\"],\"F6Int2\":[\"啟用醒目提示\"],\"FDoLyE\":[\"最多使用者數\"],\"FUU/hZ\":[\"控制聊天中載入的外部媒體數量。\"],\"Fdp03t\":[\"開啟\"],\"FfPWR0\":[\"弹窗\"],\"FjkaiT\":[\"缩小\"],\"FlqOE9\":[\"这意味着:\"],\"FolHNl\":[\"管理您的账户和身份验证\"],\"Fp2Dif\":[\"已退出服务器\"],\"G5KmCc\":[\"GZ-Line(全域 Z-Line)\"],\"GDs0lz\":[\"<0>风险:0> 敏感信息(消息、私人对话、身份验证详情)可能会暴露给网络管理员或位于 IRC 服务器之间的攻击者。\"],\"GR+2I3\":[\"新增邀請遮罩(例如 nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"关闭弹出的服务器通知\"],\"GdhD7H\":[\"再按一次以確認\"],\"GlHnXw\":[\"暱稱修改失敗: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"預覽:\"],\"GtmO8/\":[\"来自\"],\"GtuHUQ\":[\"在伺服器上重新命名此頻道,所有使用者都會看到新名稱。\"],\"GuGfFX\":[\"切换搜索\"],\"GxkJXS\":[\"上傳中...\"],\"GzbwnK\":[\"已加入频道\"],\"GzsUDB\":[\"擴充個人資料\"],\"H/PnT8\":[\"插入表情\"],\"H6Izzl\":[\"您的首選顏色代碼\"],\"H9jIv+\":[\"顯示加入/離開\"],\"HAKBY9\":[\"上傳檔案\"],\"HdE1If\":[\"頻道\"],\"Hk4AW9\":[\"您的首選顯示名稱\"],\"HmHDk7\":[\"选择成员\"],\"HrQzPU\":[[\"networkName\"],\" 上的頻道\"],\"I2tXQ5\":[\"发消息给 @\",[\"0\"],\"(Enter 换行,Shift+Enter 发送)\"],\"I6bw/h\":[\"封禁使用者\"],\"I92Z+b\":[\"启用通知\"],\"I9D72S\":[\"您確定要刪除此訊息嗎?此操作無法復原。\"],\"IA+1wo\":[\"顯示使用者被踢出頻道的事件\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"資訊:\"],\"IUwGEM\":[\"保存更改\"],\"IVeGK6\":[[\"0\"],\"、\",[\"1\"],\" 和 \",[\"2\"],\" 正在输入...\"],\"IgrLD/\":[\"暫停\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"回复\"],\"IoHMnl\":[\"最大值為 \",[\"0\"]],\"IvMj+0\":[\"管理员\"],\"J28zul\":[\"正在連線...\"],\"J5T9NW\":[\"使用者資訊\"],\"J8Y5+z\":[\"哎呀!網路分裂!⚠️\"],\"JBHkBA\":[\"已离开频道\"],\"JCwL0Q\":[\"输入原因(可选)\"],\"JFciKP\":[\"切换\"],\"JXGkhG\":[\"更改频道名称(仅限管理员)\"],\"JcD7qf\":[\"更多操作\"],\"JdkA+c\":[\"隱密 (+s)\"],\"Jmu12l\":[\"服务器频道\"],\"JvQ++s\":[\"啟用 Markdown\"],\"K2jwh/\":[\"無可用 WHOIS 資料\"],\"KAXSwC\":[\"语音权限\"],\"KDfTdX\":[\"删除消息\"],\"KKBlUU\":[\"嵌入\"],\"KM0pLb\":[\"欢迎来到本频道!\"],\"KR6W2h\":[\"取消忽略使用者\"],\"KV+Bi1\":[\"僅限邀請 (+i)\"],\"KdCtwE\":[\"在重置計數器之前監控洪水活動的秒數\"],\"Kkezga\":[\"服务器密码\"],\"KsiQ/8\":[\"使用者必須受邀才能加入頻道\"],\"L+gB/D\":[\"頻道資訊\"],\"LC1a7n\":[\"IRC 服务器报告其服务器间链路的安全级别较低。这意味着当您的消息在网络中的 IRC 服务器之间转发时,可能未经过妥善加密,或者 SSL/TLS 证书未被正确验证。\"],\"LNfLR5\":[\"顯示踢出事件\"],\"LQb0W/\":[\"顯示所有事件\"],\"LU7/yA\":[\"顯示用的別名,可包含空格、表情符號和特殊字元。真實頻道名(\",[\"channelName\"],\")仍用於 IRC 指令。\"],\"LUb9O7\":[\"需要有效的服务器端口\"],\"LV4fT6\":[\"描述(選填,例如「第三季 Beta 測試者」)\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"隱私權政策\"],\"LcuSDR\":[\"管理您的个人资料信息和元数据\"],\"LqLS9B\":[\"顯示暱稱變更\"],\"LsDQt2\":[\"频道设置\"],\"LtI9AS\":[\"频道所有者\"],\"LuNhhL\":[\"对此消息做出了回应\"],\"M/AZNG\":[\"頭像圖片 URL\"],\"M/WIer\":[\"傳送訊息\"],\"M8er/5\":[\"名稱:\"],\"MHk+7g\":[\"上一张图片\"],\"MRorGe\":[\"私信用户\"],\"MVbSGP\":[\"時間視窗(秒)\"],\"MkpcsT\":[\"您的訊息和設定儲存在本機裝置上\"],\"N/hDSy\":[\"標記為機器人——通常為 'on' 或空白\"],\"N7TQbE\":[\"邀請使用者加入 \",[\"channelName\"]],\"NCca/o\":[\"輸入預設暱稱...\"],\"Nqs6B9\":[\"显示所有外部媒体。任何 URL 都可能向未知服务器发送请求。\"],\"Nt+9O7\":[\"使用 WebSocket 而非原始 TCP\"],\"NxIHzc\":[\"踢出用戶\"],\"O+v/cL\":[\"浏览服务器上的所有频道\"],\"ODwSCk\":[\"发送 GIF\"],\"OGQ5kK\":[\"配置通知声音和高亮提示\"],\"OIPt1Z\":[\"显示或隐藏成员列表侧栏\"],\"OKSNq/\":[\"非常严格\"],\"ONWvwQ\":[\"上傳\"],\"OVKoQO\":[\"您的帳戶認證密碼\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - 將 IRC 帶入未來\"],\"OhCpra\":[\"设置主题…\"],\"OkltoQ\":[\"通过昵称封禁 \",[\"username\"],\"(阻止其使用相同昵称重新加入)\"],\"P+t/Te\":[\"無其他資料\"],\"P42Wcc\":[\"安全\"],\"PD38l0\":[\"频道头像预览\"],\"PD9mEt\":[\"输入消息...\"],\"PPqfdA\":[\"打开频道配置设置\"],\"PSCjfZ\":[\"此頻道顯示的主題,所有使用者均可查看。\"],\"PZCecv\":[\"PDF 預覽\"],\"PeLgsC\":[[\"c\",\"plural\",{\"other\":[[\"c\"],\" 次\"]}]],\"PguS2C\":[\"新增例外遮罩(例如 nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"顯示 \",[\"displayedChannelsCount\"],\",共 \",[\"0\"],\" 個頻道\"],\"PqhVlJ\":[\"封禁用户(按 Hostmask)\"],\"Q+chwU\":[\"用戶名:\"],\"Q2QY4/\":[\"刪除此邀請\"],\"Q6hhn8\":[\"偏好设置\"],\"QF4a34\":[\"請輸入使用者名稱\"],\"QGqSZ2\":[\"颜色与格式\"],\"QJQd1J\":[\"編輯資料\"],\"QSzGDE\":[\"閒置\"],\"QUlny5\":[\"欢迎来到 \",[\"0\"],\"!\"],\"Qoq+GP\":[\"阅读更多\"],\"QuSkCF\":[\"筛选频道...\"],\"QwUrDZ\":[\"將主題更改為:\",[\"topic\"]],\"R0UH07\":[\"第 \",[\"0\"],\" 張,共 \",[\"1\"],\" 張\"],\"R7SsBE\":[\"靜音\"],\"R8rf1X\":[\"点击设置主题\"],\"RArB3D\":[\"被 \",[\"username\"],\" 踢出 \",[\"channelName\"]],\"RI3cWd\":[\"使用 ObsidianIRC 探索 IRC 的世界\"],\"RIfHS5\":[\"建立新的邀請連結\"],\"RMMaN5\":[\"已審核 (+m)\"],\"RWw9Lg\":[\"關閉視窗\"],\"RZ2BuZ\":[\"帳戶 \",[\"account\"],\" 註冊需要驗證:\",[\"message\"]],\"RySp6q\":[\"隱藏評論\"],\"SPKQTd\":[\"昵称为必填项\"],\"SPVjfj\":[\"留空将默认显示\\\"无原因\\\"\"],\"SQKPvQ\":[\"邀请用户\"],\"SkZcl+\":[\"選擇預定義的洪水保護設定檔。這些設定檔為不同使用場景提供均衡的保護設定。\"],\"Slr+3C\":[\"最少使用者數\"],\"Spnlre\":[\"您邀請了 \",[\"target\"],\" 加入 \",[\"channel\"]],\"T/ckN5\":[\"在查看器中打开\"],\"T91vKp\":[\"播放\"],\"TV2Wdu\":[\"了解我們如何處理您的資料並保護您的隱私。\"],\"TgFpwD\":[\"正在套用...\"],\"TkzSFB\":[\"无更改\"],\"TtserG\":[\"输入真实姓名\"],\"Ttz9J1\":[\"輸入密碼...\"],\"Tz0i8g\":[\"設定\"],\"U3pytU\":[\"管理员\"],\"UDb2YD\":[\"添加表情\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"您尚未建立任何邀請連結。請使用上方表單建立第一個。\"],\"UGT5vp\":[\"儲存設定\"],\"UV5hLB\":[\"未找到封禁\"],\"Uaj3Nd\":[\"状态消息\"],\"Ue3uny\":[\"預設(無設定檔)\"],\"UkARhe\":[\"普通 – 標準保護\"],\"Umn7Cj\":[\"尚無評論,成為第一個吧!\"],\"UtUIRh\":[[\"0\"],\" 則較早的訊息\"],\"UwzP+U\":[\"安全連線\"],\"V0/A4O\":[\"頻道擁有者\"],\"V4qgxE\":[\"建立時間早於(分鐘前)\"],\"V8yTm6\":[\"清除搜索\"],\"VJMMyz\":[\"ObsidianIRC - 将 IRC 带入未来\"],\"VJScHU\":[\"原因\"],\"VLsmVV\":[\"静音通知\"],\"VbyRUy\":[\"評論\"],\"Vmx0mQ\":[\"設定者:\"],\"VqnIZz\":[\"查看我们的隐私政策和数据使用规范\"],\"VrMygG\":[\"最小長度為 \",[\"0\"]],\"VrnTui\":[\"您的代名詞,顯示在個人資料中\"],\"W8E3qn\":[\"已驗證帳戶\"],\"WAakm9\":[\"删除频道\"],\"WFxTHC\":[\"新增封禁遮罩(例如 nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"服务器地址为必填项\"],\"WRYdXW\":[\"音频进度\"],\"WUOH5B\":[\"忽略使用者\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"other\":[\"顯示另外 \",[\"1\"],\" 個項目\"]}]],\"WYxRzo\":[\"建立並管理您的邀請連結\"],\"Wd38W1\":[\"頻道留空可建立一般網路邀請。描述僅供您自己記錄使用——只有您能在此清單中看到。\"],\"Weq9zb\":[\"一般\"],\"Wfj7Sk\":[\"开启或关闭通知声音\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"用户资料\"],\"X6S3lt\":[\"搜索设置、频道、服务器...\"],\"XEHan5\":[\"仍然继续\"],\"XI1+wb\":[\"格式無效\"],\"XIXeuC\":[\"发消息给 @\",[\"0\"]],\"XMS+k4\":[\"发起私信\"],\"XWgxXq\":[\"相簿\"],\"Xd7+IT\":[\"取消置顶私聊\"],\"Xm/s+u\":[\"顯示\"],\"Xp2n93\":[\"显示来自服务器受信任文件主机的媒体。不会向外部服务发送任何请求。\"],\"XvjC4F\":[\"正在儲存...\"],\"Y/qryO\":[\"未找到与搜索条件匹配的用户\"],\"YAqRpI\":[\"帳戶 \",[\"account\"],\" 註冊成功:\",[\"message\"]],\"YEfzvP\":[\"受保護主題 (+t)\"],\"YQOn6a\":[\"收起成员列表\"],\"YRCoE9\":[\"頻道操作員\"],\"YURQaF\":[\"查看個人資料\"],\"YdBSvr\":[\"控制媒体显示和外部内容\"],\"Yj6U3V\":[\"無中央伺服器:\"],\"YjvpGx\":[\"代词\"],\"YqH4l4\":[\"无密钥\"],\"YyUPpV\":[\"帳戶:\"],\"ZJSWfw\":[\"中斷伺服器連線時顯示的訊息\"],\"ZR1dJ4\":[\"邀請\"],\"ZdWg0V\":[\"在浏览器中打开\"],\"ZhRBbl\":[\"搜索消息…\"],\"Zmcu3y\":[\"進階篩選\"],\"a2/8e5\":[\"主題設定時間晚於(分鐘前)\"],\"aHKcKc\":[\"上一页\"],\"aJTbXX\":[\"Oper 密码\"],\"aQryQv\":[\"模式已存在\"],\"aW9pLN\":[\"頻道允許的最大使用者數,留空表示無限制。\"],\"ah4fmZ\":[\"同时显示来自 YouTube、Vimeo、SoundCloud 及类似知名服务的预览。\"],\"aifXak\":[\"此頻道沒有媒體\"],\"ap2zBz\":[\"宽松\"],\"az8lvo\":[\"关\"],\"azXSNo\":[\"展开成员列表\"],\"azdliB\":[\"登录账户\"],\"b26wlF\":[\"她/她的\"],\"bD/+Ei\":[\"严格\"],\"bQ6BJn\":[\"設定詳細的洪水保護規則。每條規則指定要監控的活動類型以及超過閾值時採取的操作。\"],\"beV7+y\":[\"使用者將收到加入 \",[\"channelName\"],\" 的邀請。\"],\"bk84cH\":[\"离开消息\"],\"bkHdLj\":[\"添加 IRC 服务器\"],\"bmQLn5\":[\"新增規則\"],\"bwRvnp\":[\"操作\"],\"c8+EVZ\":[\"已验证账户\"],\"cGYUlD\":[\"未加载任何媒体预览。\"],\"cLF98o\":[\"顯示評論 (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"没有可用用户\"],\"cSgpoS\":[\"置顶私聊\"],\"cde3ce\":[\"发消息给 <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"複製格式化輸出\"],\"cl/A5J\":[\"欢迎来到 \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"删除\"],\"coPLXT\":[\"我們不在伺服器上儲存您的 IRC 通訊\"],\"crYH/6\":[\"SoundCloud 播放器\"],\"d3sis4\":[\"添加服务器\"],\"d9aN5k\":[\"将 \",[\"username\"],\" 移出频道\"],\"dEgA5A\":[\"取消\"],\"dGi1We\":[\"取消置顶此私信对话\"],\"dJVuyC\":[\"離開了 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"至\"],\"dXqxlh\":[\"<0>⚠️ 安全风险!0> 此连接可能容易遭受窃听或中间人攻击。\"],\"da9Q/R\":[\"已更改频道模式\"],\"dhJN3N\":[\"顯示評論\"],\"dj2xTE\":[\"关闭通知\"],\"dpCzmC\":[\"洪水保護設定\"],\"e9dQpT\":[\"是否在新标签页中打开此链接?\"],\"ePK91l\":[\"编辑\"],\"eYBDuB\":[\"上傳圖片或提供含可選 \",[\"size\"],\" 替換的 URL\"],\"edBbee\":[\"通过 hostmask 封禁 \",[\"username\"],\"(阻止其从相同 IP/主机重新加入)\"],\"ekfzWq\":[\"使用者設定\"],\"elPDWs\":[\"自定义您的 IRC 客户端体验\"],\"eu2osY\":[\"<0>💡 建议:0> 仅在您信任此服务器并了解相关风险的情况下继续操作。避免通过此连接共享敏感信息或密码。\"],\"euEhbr\":[\"點擊加入 \",[\"channel\"]],\"ez3vLd\":[\"啟用多行輸入\"],\"f0J5Ki\":[\"服务器之间的通信可能使用未加密的连接\"],\"f9BHJk\":[\"警告用户\"],\"fDOLLd\":[\"未找到頻道。\"],\"ffzDkB\":[\"匿名分析:\"],\"fq1GF9\":[\"顯示使用者中斷伺服器連線的事件\"],\"gEF57C\":[\"此伺服器僅支援一種連線類型\"],\"gJuLUI\":[\"忽略清單\"],\"gNzMrk\":[\"目前頭像\"],\"gjPWyO\":[\"輸入暱稱...\"],\"gz6UQ3\":[\"最大化\"],\"h6razj\":[\"排除頻道名稱遮罩\"],\"hG6jnw\":[\"未设置主题\"],\"hG89Ed\":[\"圖片\"],\"hYgDIe\":[\"建立\"],\"hZ6znB\":[\"端口\"],\"ha+Bz5\":[\"例如:100:1440\"],\"he3ygx\":[\"複製\"],\"hehnjM\":[\"數量\"],\"hzdLuQ\":[\"只有獲得發言權或更高權限的使用者才能發言\"],\"i0qMbr\":[\"主页\"],\"iDNBZe\":[\"通知\"],\"iH8pgl\":[\"返回\"],\"iL9SZg\":[\"封禁用户(按昵称)\"],\"iNt+3c\":[\"返回图片\"],\"iQvi+a\":[\"不再提醒我此服务器的低链路安全问题\"],\"iSLIjg\":[\"連線\"],\"iWXkHH\":[\"半管理员\"],\"iZeTtp\":[\"服务器地址\"],\"idD8Ev\":[\"已儲存\"],\"iivqkW\":[\"登入時間\"],\"ij+Elv\":[\"图片预览\"],\"ilIWp7\":[\"切换通知\"],\"iuaqvB\":[\"使用 * 作為萬用字元。範例:baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"机器人\"],\"j2DGR0\":[\"依主機遮罩封禁\"],\"jA4uoI\":[\"話題:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"原因(可选)\"],\"jUV7CU\":[\"上傳頭像\"],\"jW5Uwh\":[\"控制載入的外部媒體量。關閉 / 安全 / 可信來源 / 所有內容。\"],\"jXzms5\":[\"附件选项\"],\"jZlrte\":[\"颜色\"],\"jfC/xh\":[\"聯絡方式\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"載入較早的訊息\"],\"k3ID0F\":[\"筛选成员…\"],\"k65gsE\":[\"深入查看\"],\"k7Zgob\":[\"取消连接\"],\"kAVx5h\":[\"未找到邀請\"],\"kCLEPU\":[\"已連線至\"],\"kF5LKb\":[\"已忽略的模式:\"],\"kGeOx/\":[\"加入 \",[\"0\"]],\"kITKr8\":[\"正在載入頻道模式...\"],\"kPpPsw\":[\"您是 IRC Operator\"],\"kWJmRL\":[\"您\"],\"kfcRb0\":[\"头像\"],\"kjMqSj\":[\"複製 JSON\"],\"krViRy\":[\"點擊以 JSON 格式複製\"],\"ks71ra\":[\"例外\"],\"kw4lRv\":[\"頻道半操作員\"],\"kxgIRq\":[\"选择或添加频道以开始使用。\"],\"ky6dWe\":[\"头像预览\"],\"l+GxCv\":[\"正在載入頻道...\"],\"l+IUVW\":[\"帳戶 \",[\"account\"],\" 驗證成功:\",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"other\":[\"已重新連線 \",[\"reconnectCount\"],\" 次\"]}]],\"l1l8sj\":[[\"0\"],\" 天前\"],\"l5NhnV\":[\"#頻道(選填)\"],\"l5jmzx\":[[\"0\"],\" 和 \",[\"1\"],\" 正在输入...\"],\"lCF0wC\":[\"重新整理\"],\"lHy8N5\":[\"正在載入更多頻道...\"],\"lasgrr\":[\"已使用\"],\"lbpf14\":[\"加入 \",[\"value\"]],\"lfFsZ4\":[\"頻道\"],\"lkNdiH\":[\"帳戶名稱\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"上传图片\"],\"loQxaJ\":[\"我回来了\"],\"lvfaxv\":[\"首頁\"],\"m16xKo\":[\"新增\"],\"m8flAk\":[\"預覽(尚未上傳)\"],\"mEPxTp\":[\"<0>⚠️ 请注意!0> 仅打开来自可信来源的链接。恶意链接可能危害您的安全或隐私。\"],\"mHGdhG\":[\"伺服器資訊\"],\"mHS8lb\":[\"发消息到 #\",[\"0\"]],\"mMYBD9\":[\"寬泛 – 更廣的保護範圍\"],\"mTGsPd\":[\"頻道主題\"],\"mU8j6O\":[\"禁止外部訊息 (+n)\"],\"mZp8FL\":[\"自動回退至單行\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"評論 (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"用户已认证\"],\"mwtcGl\":[\"关闭评论\"],\"mzI/c+\":[\"下载\"],\"n3fGRk\":[\"由 \",[\"0\"],\" 設定\"],\"nE9jsU\":[\"寬鬆 – 較弱的保護\"],\"nNflMD\":[\"离开频道\"],\"nPXkBi\":[\"正在載入 WHOIS 資料...\"],\"nQnxxF\":[\"发消息到 #\",[\"0\"],\"(Shift+Enter 换行)\"],\"nWMRxa\":[\"取消置顶\"],\"nkC032\":[\"无洪水防护配置\"],\"o69z4d\":[\"向 \",[\"username\"],\" 发送警告消息\"],\"o9ylQi\":[\"搜尋 GIF 以開始\"],\"oFGkER\":[\"服务器通知\"],\"oOi11l\":[\"滚动到底部\"],\"oPYIL5\":[\"網路\"],\"oQEzQR\":[\"新私訊\"],\"oXOSPE\":[\"在线\"],\"oal760\":[\"服务器链路可能遭受中间人攻击\"],\"oeqmmJ\":[\"受信任来源\"],\"optX0N\":[[\"0\"],\" 小時前\"],\"ovBPCi\":[\"默认\"],\"p0Z69r\":[\"模式不能為空\"],\"p1KgtK\":[\"音频加载失败\"],\"p59pEv\":[\"更多詳情\"],\"p7sRI6\":[\"讓其他人知道您正在輸入\"],\"pBm1od\":[\"秘密频道\"],\"pNmiXx\":[\"所有伺服器的預設暱稱\"],\"pUUo9G\":[\"主機名:\"],\"pVGPmz\":[\"账户密码\"],\"peNE68\":[\"永久\"],\"plhHQt\":[\"無資料\"],\"pm6+q5\":[\"安全警告\"],\"pn5qSs\":[\"其他資訊\"],\"q0cR4S\":[\"現在稱為 **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"頻道不會出現在 LIST 或 NAMES 指令中\"],\"qLpTm/\":[\"移除表情 \",[\"emoji\"]],\"qVkGWK\":[\"置顶\"],\"qY8wNa\":[\"主页\"],\"qb0xJ7\":[\"萬用字元:* 符合任意字元,? 符合單一字元。範例:nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"頻道金鑰 (+k)\"],\"qtoOYG\":[\"无限制\"],\"r1W2AS\":[\"檔案託管圖片\"],\"rIPR2O\":[\"主題設定時間早於(分鐘前)\"],\"rMMSYo\":[\"最大長度為 \",[\"0\"]],\"rWtzQe\":[\"網路已分裂並重新連接。✅\"],\"rYG2u6\":[\"请稍候...\"],\"rdUucN\":[\"预览\"],\"rjGI/Q\":[\"隐私\"],\"rk8iDX\":[\"正在載入 GIF...\"],\"rn6SBY\":[\"取消靜音\"],\"s/UKqq\":[\"已被踢出频道\"],\"s8cATI\":[\"加入了 \",[\"channelName\"]],\"sCO9ue\":[\"与 <0>\",[\"serverName\"],\"0> 的连接存在以下安全问题:\"],\"sGH11W\":[\"伺服器\"],\"sHI1H+\":[\"現在稱為 **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" 邀請您加入 \",[\"channel\"]],\"sby+1/\":[\"點擊複製\"],\"sfN25C\":[\"您的真實姓名或全名\"],\"sliuzR\":[\"打开链接\"],\"sqrO9R\":[\"自訂提及\"],\"sr6RdJ\":[\"Shift+Enter 換行\"],\"swrCpB\":[\"頻道已由 \",[\"user\"],\" 從 \",[\"oldName\"],\" 重新命名為 \",[\"newName\"],[\"0\"]],\"sxkWRg\":[\"進階\"],\"t/YqKh\":[\"移除\"],\"t47eHD\":[\"您在此伺服器上的唯一識別碼\"],\"tAkAh0\":[\"含可選 \",[\"size\"],\" 替換的 URL。範例:https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"显示或隐藏频道列表侧栏\"],\"tfDRzk\":[\"保存\"],\"tiBsJk\":[\"離開了 \",[\"channelName\"]],\"tt4/UD\":[\"退出了 (\",[\"reason\"],\")\"],\"u0TcnO\":[\"暱稱 {nick} 已被使用,正在用 {newNick} 重試\"],\"u0a8B4\":[\"以 IRC 操作員身分進行管理存取認證\"],\"u0rWFU\":[\"建立時間晚於(分鐘前)\"],\"u72w3t\":[\"要忽略的使用者和模式\"],\"u7jc2L\":[\"退出了\"],\"uAQUqI\":[\"状态\"],\"uB85T3\":[\"儲存失敗:\",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC 伺服器:\"],\"ukyW4o\":[\"您的邀請連結\"],\"usSSr/\":[\"缩放级别\"],\"v7uvcf\":[\"軟體:\"],\"vE8kb+\":[\"Shift+Enter 換行(Enter 傳送)\"],\"vERlcd\":[\"个人资料\"],\"vK0RL8\":[\"無主題\"],\"vSJd18\":[\"影片\"],\"vXIe7J\":[\"语言\"],\"vaHYxN\":[\"真实姓名\"],\"vhjbKr\":[\"离开\"],\"w4NYox\":[[\"title\"],\" 客戶端\"],\"w8xQRx\":[\"值無效\"],\"wFjjxZ\":[\"被 \",[\"username\"],\" 踢出 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"未找到封禁例外\"],\"wPrGnM\":[\"頻道管理員\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"顯示使用者加入或離開頻道的事件\"],\"whqZ9r\":[\"要醒目提示的額外詞語或短語\"],\"wm7RV4\":[\"通知聲音\"],\"wz/Yoq\":[\"您的消息在服务器之间转发时可能被截获\"],\"x3+y8b\":[\"已透過此連結註冊的人數\"],\"xCJdfg\":[\"清除\"],\"xOTzt5\":[\"剛剛\"],\"xUHRTR\":[\"連線時自動以操作員身分認證\"],\"xWHwwQ\":[\"封禁\"],\"xYilR2\":[\"媒体\"],\"xbi8D6\":[\"此伺服器不支援邀請連結(未公告<0>obby.world/invitation0>功能)。您仍可正常聊天;此面板僅適用於採用 obbyircd 的網路。\"],\"xceQrO\":[\"仅支持安全的 WebSocket 连接\"],\"xdtXa+\":[\"頻道名稱\"],\"xfXC7q\":[\"文字頻道\"],\"xlCYOE\":[\"正在載入更多訊息...\"],\"xlhswE\":[\"最小值為 \",[\"0\"]],\"xq97Ci\":[\"添加词语或短语...\"],\"xuRqRq\":[\"用戶端限制 (+l)\"],\"xwF+7J\":[[\"0\"],\" 正在输入...\"],\"y1eoq1\":[\"複製連結\"],\"yNeucF\":[\"此伺服器不支援擴充個人資料元資料(IRCv3 METADATA 擴充功能)。頭像、顯示名稱和狀態等欄位不可用。\"],\"yPlrca\":[\"頻道頭像\"],\"yQE2r9\":[\"載入中\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Oper 使用者名稱\"],\"yYOzWD\":[\"日志\"],\"yfx9Re\":[\"IRC 操作員密碼\"],\"ygCKqB\":[\"停止\"],\"ymDxJx\":[\"IRC 操作員使用者名稱\"],\"yrpRsQ\":[\"依名稱排序\"],\"yz7wBu\":[\"关闭\"],\"zJw+jA\":[\"設定模式:\",[\"0\"]],\"zbymaY\":[[\"0\"],\" 分鐘前\"],\"zebeLu\":[\"输入 oper 用户名\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"無效的模式格式,請使用 nick!user@host 格式(允許萬用字元 *)\"],\"+6NQQA\":[\"通用支持频道\"],\"+6NyRG\":[\"客戶端\"],\"+K0AvT\":[\"断开连接\"],\"+cyFdH\":[\"標記為離開時的預設訊息\"],\"+mVPqU\":[\"在訊息中渲染 Markdown 格式\"],\"+vqCJH\":[\"您的帳戶認證使用者名稱\"],\"+yPBXI\":[\"選擇檔案\"],\"+zy2Nq\":[\"類型\"],\"/09cao\":[\"链路安全级别较低(级别 \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"頻道外的使用者無法傳送訊息\"],\"/4C8U0\":[\"å
¨é¨è¤è£½\"],\"/6BzZF\":[\"切换成员列表\"],\"/AkXyp\":[\"確èªï¼\"],\"/TNOPk\":[\"使用者已離開\"],\"/XQgft\":[\"探索\"],\"/cF7Rs\":[\"音量\"],\"/dqduX\":[\"下一页\"],\"/fc3q4\":[\"所有内容\"],\"/kISDh\":[\"啟用通知聲音\"],\"/n04sB\":[\"踢出\"],\"/rTz0M\":[\"音訊\"],\"/rfkZe\":[\"提及和訊息時播放聲音\"],\"0/0ZGA\":[\"頻道名稱遮罩\"],\"0D6j7U\":[\"了解更多自訂規則 →\"],\"0XsHcR\":[\"踢出用户\"],\"0ZpE//\":[\"依使用者數排序\"],\"0bEPwz\":[\"设置为离开\"],\"0dGkPt\":[\"展开频道列表\"],\"0gS7M5\":[\"显示名称\"],\"0kS+M8\":[\"範例網路\"],\"0rgoY7\":[\"僅連線至您選擇的伺服器\"],\"0wdd7X\":[\"加入\"],\"0wkVYx\":[\"私人訊息\"],\"111uHX\":[\"链接预览\"],\"196EG4\":[\"删除私聊\"],\"1DSr1i\":[\"注册账户\"],\"1O/24y\":[\"切换频道列表\"],\"1TNIig\":[\"開啟\"],\"1VPJJ2\":[\"外部链接警告\"],\"1ZC/dv\":[\"沒有未讀提及或訊息\"],\"1pO1zi\":[\"服务器名称为必填项\"],\"1uwfzQ\":[\"查看频道主题\"],\"268g7c\":[\"输入显示名称\"],\"2CEOW6\":[\"透過 soju bouncer 綁定的網路\"],\"2F9+AZ\":[\"å°æªæ·åå°ä»»ä½åå§ IRC æµéãè«å試é£ç·æå³éè¨æ¯ã\"],\"2FOFq1\":[\"网络上的服务器管理员可能读取您的消息\"],\"2FYpfJ\":[\"更多\"],\"2HF1Y2\":[[\"inviter\"],\" 邀請了 \",[\"target\"],\" 加入 \",[\"channel\"]],\"2I70QL\":[\"查看用户资料信息\"],\"2QYdmE\":[\"用戶:\"],\"2QpEjG\":[\"已離開\"],\"2YE223\":[\"发消息到 #\",[\"0\"],\"(Enter 换行,Shift+Enter 发送)\"],\"2bimFY\":[\"使用服务器密码\"],\"2iTmdZ\":[\"本機儲存:\"],\"2odkwe\":[\"嚴格 – 更強的保護\"],\"2uDhbA\":[\"输入要邀请的用户名\"],\"2ygf/L\":[\"← 返回\"],\"2zEgxj\":[\"搜索 GIF...\"],\"3RdPhl\":[\"重命名频道\"],\"3THokf\":[\"有發言權的使用者\"],\"3TSz9S\":[\"最小化\"],\"3jBDvM\":[\"頻道顯示名稱\"],\"3ryuFU\":[\"可選的當機報告以改善應用程式\"],\"3uBF/8\":[\"关闭查看器\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"輸入帳戶名稱...\"],\"4/Rr0R\":[\"邀请用户加入当前频道\"],\"4EZrJN\":[\"規則\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"洪水設定檔 (+F)\"],\"4RZQRK\":[\"你在做什麼?\"],\"4hfTrB\":[\"昵称\"],\"4n99LO\":[\"已在 \",[\"0\"]],\"4t6vMV\":[\"短訊息自動切換至單行\"],\"4vsHmf\":[\"時間(分鐘)\"],\"4x/Axu\":[\"您的 bouncer 還沒有任何網路。新增一個以開始使用。\"],\"5+INAX\":[\"醒目提示提及您的訊息\"],\"5R5Pv/\":[\"Oper 用户名\"],\"678PKt\":[\"网络名称\"],\"6Aih4U\":[\"离线\"],\"6CO3WE\":[\"加入頻道需要密碼,留空可移除金鑰。\"],\"6HhMs3\":[\"退出消息\"],\"6V3Ea3\":[\"已复制\"],\"6lGV3K\":[\"收起\"],\"6yFOEi\":[\"輸入 oper 密碼...\"],\"7+IHTZ\":[\"未選擇檔案\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host(例如:spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"发送私信\"],\"7U1W7c\":[\"非常宽松\"],\"7Y1YQj\":[\"真實姓名:\"],\"7YHArF\":[\"— 在檢視器中開啟\"],\"7fjnVl\":[\"搜索用户...\"],\"7jL88x\":[\"刪除此訊息?此操作無法復原。\"],\"7nGhhM\":[\"您在想什么?\"],\"7sEpu1\":[\"成员 — \",[\"0\"]],\"7sNhEz\":[\"用户名\"],\"8H0Q+x\":[\"了解更多設定檔 →\"],\"8Phu0A\":[\"顯示使用者更改暱稱的事件\"],\"8XTG9e\":[\"输入 oper 密码\"],\"8XsV2J\":[\"重新傳送\"],\"8ZsakT\":[\"密码\"],\"8kR84m\":[\"您即将打开一个外部链接:\"],\"8lCgih\":[\"移除規則\"],\"8o3dPc\":[\"ææ¾æªæ¡ä»¥ä¸å³\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"other\":[\"已加入 \",[\"joinCount\"],\" 次\"]}]],\"9BMLnJ\":[\"重新连接服务器\"],\"9OEgyT\":[\"添加表情\"],\"9PQ8m2\":[\"G-Line(全域封禁)\"],\"9Qs99X\":[\"電子郵件:\"],\"9QupBP\":[\"删除规则\"],\"9W7tl5\":[\"(未變更)\"],\"9bG48P\":[\"傳送中\"],\"9f5f0u\":[\"有隱私問題?請聯絡我們:\"],\"9iweoP\":[[\"0\"],\" 上的網路\"],\"9unqs3\":[\"離開:\"],\"9v3hwv\":[\"未找到服务器。\"],\"9zb2WA\":[\"连接中\"],\"A1taO8\":[\"搜索\"],\"A2adVi\":[\"傳送正在輸入通知\"],\"A9Rhec\":[\"頻道名稱\"],\"AWOSPo\":[\"放大\"],\"AXSpEQ\":[\"連線時獲取 Oper\"],\"AdKRCX\":[\"已連接到 <0>\",[\"0\"],\"0>。\"],\"AeXO77\":[\"账户\"],\"AhNP40\":[\"跳转\"],\"Ai2U7L\":[\"主機\"],\"AjBQnf\":[\"已更改昵称\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"取消回复\"],\"ApSx0O\":[\"找到 \",[\"0\"],\" 則符合「\",[\"searchQuery\"],\"」的訊息\"],\"AxPAXW\":[\"未找到結果\"],\"AyNqAB\":[\"在聊天中顯示所有伺服器事件\"],\"B/QqGw\":[\"暂时离开\"],\"B0sB2k\":[\"明文\"],\"B8AaMI\":[\"此欄位為必填\"],\"BA2c49\":[\"伺服器不支援進階 LIST 篩選\"],\"BDKt3I\":[[\"0\"],\"、\",[\"1\"],\"、\",[\"2\"],\" 以及另外 \",[\"3\"],\" 人正在输入...\"],\"BGul2A\":[\"您有未保存的更改。确定要关闭而不保存吗?\"],\"BIf9fi\":[\"您的狀態訊息\"],\"BOJWfb\":[\"中斷與 soju bouncer 的連線?\"],\"BPm98R\":[\"å°æªé¸æä¼ºæå¨ãè«å
å¾å´éæ¬é¸æä¸å伺æå¨ï¼éè«é£çµæ¯ä¾ä¼ºæå¨ç®¡ççã\"],\"BZz3md\":[\"您的個人網站\"],\"Bgm/H7\":[\"允許輸入多行文字\"],\"BiQIl1\":[\"置顶此私信对话\"],\"BlNZZ2\":[\"点击跳转到消息\"],\"Bowq3c\":[\"只有管理員可以更改頻道主題\"],\"Btozzp\":[\"此图片已过期\"],\"Bycfjm\":[\"總計:\",[\"0\"]],\"C6IBQc\":[\"複製完整 JSON\"],\"C9L9wL\":[\"資料收集\"],\"CDq4wC\":[\"管理用户\"],\"CHVRxG\":[\"发消息给 @\",[\"0\"],\"(Shift+Enter 换行)\"],\"CN9zdR\":[\"Oper 用户名和密码为必填项\"],\"CW3sYa\":[\"添加表情 \",[\"emoji\"]],\"CaAkqd\":[\"顯示退出事件\"],\"CbvaYj\":[\"依暱稱封禁\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"选择频道\"],\"CsekCi\":[\"普通\"],\"D+NlUC\":[\"系统\"],\"D28t6+\":[\"已加入並退出\"],\"DB8zMK\":[\"套用\"],\"DBcWHr\":[\"自訂通知音效檔\"],\"DTy9Xw\":[\"媒體預覽\"],\"Dj4pSr\":[\"选择一个安全密码\"],\"Du+zn+\":[\"搜尋中...\"],\"Du2T2f\":[\"未找到設定\"],\"DwsSVQ\":[\"套用篩選並重新整理\"],\"E3W/zd\":[\"預設暱稱\"],\"E6nRW7\":[\"复制链接\"],\"E703RG\":[\"模式:\"],\"EAeu1Z\":[\"傳送邀請\"],\"EFKJQT\":[\"設定\"],\"EGPQBv\":[\"自訂洪水規則 (+f)\"],\"ELik0r\":[\"查看完整隱私權政策\"],\"EPbeC2\":[\"查看或编辑频道主题\"],\"EQCDNT\":[\"輸入 oper 用戶名...\"],\"EUvulZ\":[\"找到 1 則符合「\",[\"searchQuery\"],\"」的訊息\"],\"EatZYJ\":[\"下一张图片\"],\"EdQY6l\":[\"無\"],\"EnqLYU\":[\"搜索服务器...\"],\"F0OKMc\":[\"编辑服务器\"],\"F6Int2\":[\"啟用醒目提示\"],\"FDoLyE\":[\"最多使用者數\"],\"FUU/hZ\":[\"控制聊天中載入的外部媒體數量。\"],\"Fdp03t\":[\"開啟\"],\"FfPWR0\":[\"弹窗\"],\"FjkaiT\":[\"缩小\"],\"FlqOE9\":[\"这意味着:\"],\"FolHNl\":[\"管理您的账户和身份验证\"],\"Fp2Dif\":[\"已退出服务器\"],\"G5KmCc\":[\"GZ-Line(全域 Z-Line)\"],\"GDs0lz\":[\"<0>风险:0> 敏感信息(消息、私人对话、身份验证详情)可能会暴露给网络管理员或位于 IRC 服务器之间的攻击者。\"],\"GR+2I3\":[\"新增邀請遮罩(例如 nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"关闭弹出的服务器通知\"],\"GdhD7H\":[\"åæä¸æ¬¡ä»¥ç¢ºèª\"],\"GjRZex\":[\"中斷網路連線?\"],\"GlHnXw\":[\"暱稱修改失敗: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"預覽:\"],\"GtmO8/\":[\"来自\"],\"GtuHUQ\":[\"在伺服器上重新命名此頻道,所有使用者都會看到新名稱。\"],\"GuGfFX\":[\"切换搜索\"],\"GxkJXS\":[\"上傳中...\"],\"GzbwnK\":[\"已加入频道\"],\"GzsUDB\":[\"擴充個人資料\"],\"H/PnT8\":[\"插入表情\"],\"H6Izzl\":[\"您的首選顏色代碼\"],\"H9jIv+\":[\"顯示加入/離開\"],\"HAKBY9\":[\"上傳檔案\"],\"HdE1If\":[\"頻道\"],\"Hk4AW9\":[\"您的首選顯示名稱\"],\"HmHDk7\":[\"选择成员\"],\"HrQzPU\":[[\"networkName\"],\" 上的頻道\"],\"I2tXQ5\":[\"发消息给 @\",[\"0\"],\"(Enter 换行,Shift+Enter 发送)\"],\"I6bw/h\":[\"封禁使用者\"],\"I92Z+b\":[\"启用通知\"],\"I9D72S\":[\"您確定要刪除此訊息嗎?此操作無法復原。\"],\"IA+1wo\":[\"顯示使用者被踢出頻道的事件\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"資訊:\"],\"IUwGEM\":[\"保存更改\"],\"IVeGK6\":[[\"0\"],\"、\",[\"1\"],\" 和 \",[\"2\"],\" 正在输入...\"],\"IgrLD/\":[\"暫停\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"回复\"],\"IoHMnl\":[\"最大值為 \",[\"0\"]],\"IvMj+0\":[\"管理员\"],\"J28zul\":[\"正在連線...\"],\"J5T9NW\":[\"使用者資訊\"],\"J8Y5+z\":[\"哎呀!網路分裂!⚠️\"],\"JBHkBA\":[\"已离开频道\"],\"JCwL0Q\":[\"输入原因(可选)\"],\"JFciKP\":[\"切换\"],\"JXGkhG\":[\"更改频道名称(仅限管理员)\"],\"JcD7qf\":[\"更多操作\"],\"JdkA+c\":[\"隱密 (+s)\"],\"Jmu12l\":[\"服务器频道\"],\"JoQY+E\":[\"這會將網路從您的 soju bouncer 中移除。若要再次使用,您需要重新新增。\"],\"JvQ++s\":[\"啟用 Markdown\"],\"K2jwh/\":[\"無可用 WHOIS 資料\"],\"KAXSwC\":[\"语音权限\"],\"KDfTdX\":[\"删除消息\"],\"KKBlUU\":[\"嵌入\"],\"KM0pLb\":[\"欢迎来到本频道!\"],\"KR6W2h\":[\"取消忽略使用者\"],\"KV+Bi1\":[\"僅限邀請 (+i)\"],\"KdCtwE\":[\"在重置計數器之前監控洪水活動的秒數\"],\"Kkezga\":[\"服务器密码\"],\"KsiQ/8\":[\"使用者必須受邀才能加入頻道\"],\"L+gB/D\":[\"頻道資訊\"],\"LC1a7n\":[\"IRC 服务器报告其服务器间链路的安全级别较低。这意味着当您的消息在网络中的 IRC 服务器之间转发时,可能未经过妥善加密,或者 SSL/TLS 证书未被正确验证。\"],\"LEwpeL\":[\"soju bouncer(控制)\"],\"LNfLR5\":[\"顯示踢出事件\"],\"LP+1Z7\":[\"新增網路\"],\"LQb0W/\":[\"顯示所有事件\"],\"LU7/yA\":[\"顯示用的別名,可包含空格、表情符號和特殊字元。真實頻道名(\",[\"channelName\"],\")仍用於 IRC 指令。\"],\"LUb9O7\":[\"需要有效的服务器端口\"],\"LV4fT6\":[\"æè¿°ï¼é¸å¡«ï¼ä¾å¦ã第ä¸å£ Beta 測試è
ãï¼\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"隱私權政策\"],\"LcuSDR\":[\"管理您的个人资料信息和元数据\"],\"LqLS9B\":[\"顯示暱稱變更\"],\"LsDQt2\":[\"频道设置\"],\"LtI9AS\":[\"频道所有者\"],\"LuNhhL\":[\"对此消息做出了回应\"],\"M/AZNG\":[\"頭像圖片 URL\"],\"M/WIer\":[\"傳送訊息\"],\"M8er/5\":[\"名稱:\"],\"MHk+7g\":[\"上一张图片\"],\"MRorGe\":[\"私信用户\"],\"MVbSGP\":[\"時間視窗(秒)\"],\"MkpcsT\":[\"您的訊息和設定儲存在本機裝置上\"],\"MzPdC2\":[\"伺服器密碼 (PASS)\"],\"N/hDSy\":[\"標記為機器人——通常為 'on' 或空白\"],\"N6j2JH\":[\"編輯 \",[\"0\"]],\"N7TQbE\":[\"邀請使用者加入 \",[\"channelName\"]],\"NCca/o\":[\"輸入預設暱稱...\"],\"Nqs6B9\":[\"显示所有外部媒体。任何 URL 都可能向未知服务器发送请求。\"],\"Nt+9O7\":[\"使用 WebSocket 而非原始 TCP\"],\"NxIHzc\":[\"踢出用戶\"],\"O+v/cL\":[\"浏览服务器上的所有频道\"],\"OCGpR4\":[\"(繼承)\"],\"ODwSCk\":[\"发送 GIF\"],\"OGQ5kK\":[\"配置通知声音和高亮提示\"],\"OIPt1Z\":[\"显示或隐藏成员列表侧栏\"],\"OKSNq/\":[\"非常严格\"],\"ONWvwQ\":[\"上傳\"],\"OVKoQO\":[\"您的帳戶認證密碼\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - 將 IRC 帶入未來\"],\"OhCpra\":[\"设置主题…\"],\"OkltoQ\":[\"通过昵称封禁 \",[\"username\"],\"(阻止其使用相同昵称重新加入)\"],\"P+t/Te\":[\"無其他資料\"],\"P42Wcc\":[\"安全\"],\"PD38l0\":[\"频道头像预览\"],\"PD9mEt\":[\"输入消息...\"],\"PPqfdA\":[\"打开频道配置设置\"],\"PSCjfZ\":[\"此頻道顯示的主題,所有使用者均可查看。\"],\"PZCecv\":[\"PDF 預覽\"],\"PeLgsC\":[[\"c\",\"plural\",{\"other\":[[\"c\"],\" 次\"]}]],\"PguS2C\":[\"新增例外遮罩(例如 nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"顯示 \",[\"displayedChannelsCount\"],\",共 \",[\"0\"],\" 個頻道\"],\"PqhVlJ\":[\"封禁用户(按 Hostmask)\"],\"Q+chwU\":[\"用戶名:\"],\"Q2QY4/\":[\"åªé¤æ¤éè«\"],\"Q3v9Wc\":[\"是,刪除\"],\"Q6hhn8\":[\"偏好设置\"],\"QF4a34\":[\"請輸入使用者名稱\"],\"QGqSZ2\":[\"颜色与格式\"],\"QJQd1J\":[\"編輯資料\"],\"QSzGDE\":[\"閒置\"],\"QUlny5\":[\"欢迎来到 \",[\"0\"],\"!\"],\"Qoq+GP\":[\"阅读更多\"],\"QuSkCF\":[\"筛选频道...\"],\"QwUrDZ\":[\"將主題更改為:\",[\"topic\"]],\"R0UH07\":[\"第 \",[\"0\"],\" 張,共 \",[\"1\"],\" 張\"],\"R7SsBE\":[\"靜音\"],\"R8rf1X\":[\"点击设置主题\"],\"RArB3D\":[\"被 \",[\"username\"],\" 踢出 \",[\"channelName\"]],\"RI3cWd\":[\"使用 ObsidianIRC 探索 IRC 的世界\"],\"RIfHS5\":[\"å»ºç«æ°çéè«é£çµ\"],\"RMMaN5\":[\"已審核 (+m)\"],\"RWw9Lg\":[\"關閉視窗\"],\"RZ2BuZ\":[\"帳戶 \",[\"account\"],\" 註冊需要驗證:\",[\"message\"]],\"RySp6q\":[\"隱藏評論\"],\"S5Togi\":[\"正在從您的 bouncer 載入網路…\"],\"SPKQTd\":[\"昵称为必填项\"],\"SPVjfj\":[\"留空将默认显示\\\"无原因\\\"\"],\"SQKPvQ\":[\"邀请用户\"],\"STmlpb\":[\"返回網路列表\"],\"SkZcl+\":[\"選擇預定義的洪水保護設定檔。這些設定檔為不同使用場景提供均衡的保護設定。\"],\"Slr+3C\":[\"最少使用者數\"],\"Spnlre\":[\"您邀請了 \",[\"target\"],\" 加入 \",[\"channel\"]],\"T/ckN5\":[\"在查看器中打开\"],\"T91vKp\":[\"播放\"],\"TV2Wdu\":[\"了解我們如何處理您的資料並保護您的隱私。\"],\"TgFpwD\":[\"正在套用...\"],\"TkzSFB\":[\"无更改\"],\"TtserG\":[\"输入真实姓名\"],\"Ttz9J1\":[\"輸入密碼...\"],\"Tz0i8g\":[\"設定\"],\"U3pytU\":[\"管理员\"],\"UDb2YD\":[\"添加表情\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"æ¨å°æªå»ºç«ä»»ä½éè«é£çµãè«ä½¿ç¨ä¸æ¹è¡¨å®å»ºç«ç¬¬ä¸åã\"],\"UGT5vp\":[\"儲存設定\"],\"UV5hLB\":[\"未找到封禁\"],\"Uaj3Nd\":[\"状态消息\"],\"Ue3uny\":[\"預設(無設定檔)\"],\"UkARhe\":[\"普通 – 標準保護\"],\"Umn7Cj\":[\"尚無評論,成為第一個吧!\"],\"UtUIRh\":[[\"0\"],\" 則較早的訊息\"],\"UwzP+U\":[\"安全連線\"],\"V0/A4O\":[\"頻道擁有者\"],\"V0zZWc\":[\"這也會關閉下方綁定的網路。\"],\"V4qgxE\":[\"建立時間早於(分鐘前)\"],\"V8yTm6\":[\"清除搜索\"],\"VJMMyz\":[\"ObsidianIRC - 将 IRC 带入未来\"],\"VJScHU\":[\"原因\"],\"VLsmVV\":[\"静音通知\"],\"VbyRUy\":[\"評論\"],\"Vmx0mQ\":[\"設定者:\"],\"VqnIZz\":[\"查看我们的隐私政策和数据使用规范\"],\"VrMygG\":[\"最小長度為 \",[\"0\"]],\"VrnTui\":[\"您的代名詞,顯示在個人資料中\"],\"W8E3qn\":[\"已驗證帳戶\"],\"WAakm9\":[\"删除频道\"],\"WFxTHC\":[\"新增封禁遮罩(例如 nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"服务器地址为必填项\"],\"WRYdXW\":[\"音频进度\"],\"WUOH5B\":[\"忽略使用者\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"other\":[\"顯示另外 \",[\"1\"],\" 個項目\"]}]],\"WYxRzo\":[\"建ç«ä¸¦ç®¡çæ¨çéè«é£çµ\"],\"Wd38W1\":[\"é\xA0»éç空å¯å»ºç«ä¸è¬ç¶²è·¯éè«ãæè¿°å
便¨èªå·±è¨é使ç¨ââåªææ¨è½å¨æ¤æ¸
å®ä¸çå°ã\"],\"Weq9zb\":[\"一般\"],\"Wfj7Sk\":[\"开启或关闭通知声音\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"用户资料\"],\"X6S3lt\":[\"搜索设置、频道、服务器...\"],\"XEHan5\":[\"仍然继续\"],\"XI1+wb\":[\"格式無效\"],\"XIXeuC\":[\"发消息给 @\",[\"0\"]],\"XMS+k4\":[\"发起私信\"],\"XWgxXq\":[\"相簿\"],\"Xd7+IT\":[\"取消置顶私聊\"],\"Xm/s+u\":[\"顯示\"],\"Xp2n93\":[\"显示来自服务器受信任文件主机的媒体。不会向外部服务发送任何请求。\"],\"XvjC4F\":[\"正在儲存...\"],\"Y/qryO\":[\"未找到与搜索条件匹配的用户\"],\"YAqRpI\":[\"帳戶 \",[\"account\"],\" 註冊成功:\",[\"message\"]],\"YEfzvP\":[\"受保護主題 (+t)\"],\"YQOn6a\":[\"收起成员列表\"],\"YRCoE9\":[\"頻道操作員\"],\"YURQaF\":[\"查看個人資料\"],\"YdBSvr\":[\"控制媒体显示和外部内容\"],\"Yj6U3V\":[\"無中央伺服器:\"],\"YjvpGx\":[\"代词\"],\"YqH4l4\":[\"无密钥\"],\"YyUPpV\":[\"帳戶:\"],\"ZJSWfw\":[\"中斷伺服器連線時顯示的訊息\"],\"ZR1dJ4\":[\"邀請\"],\"ZdWg0V\":[\"在浏览器中打开\"],\"ZhRBbl\":[\"搜索消息…\"],\"Zmcu3y\":[\"進階篩選\"],\"a0bHay\":[\"中斷 <0>\",[\"0\"],\"0> 的連線?\"],\"a2/8e5\":[\"主題設定時間晚於(分鐘前)\"],\"aHKcKc\":[\"上一页\"],\"aJTbXX\":[\"Oper 密码\"],\"aQryQv\":[\"模式已存在\"],\"aW9pLN\":[\"頻道允許的最大使用者數,留空表示無限制。\"],\"ah4fmZ\":[\"同时显示来自 YouTube、Vimeo、SoundCloud 及类似知名服务的预览。\"],\"aifXak\":[\"此頻道沒有媒體\"],\"ap2zBz\":[\"宽松\"],\"az8lvo\":[\"关\"],\"azXSNo\":[\"展开成员列表\"],\"azdliB\":[\"登录账户\"],\"b26wlF\":[\"她/她的\"],\"bD/+Ei\":[\"严格\"],\"bQ6BJn\":[\"設定詳細的洪水保護規則。每條規則指定要監控的活動類型以及超過閾值時採取的操作。\"],\"beV7+y\":[\"使用者將收到加入 \",[\"channelName\"],\" 的邀請。\"],\"bk84cH\":[\"离开消息\"],\"bkHdLj\":[\"添加 IRC 服务器\"],\"bmQLn5\":[\"新增規則\"],\"bv4cFj\":[\"傳輸方式\"],\"bwRvnp\":[\"操作\"],\"c8+EVZ\":[\"已验证账户\"],\"cGYUlD\":[\"未加载任何媒体预览。\"],\"cLF98o\":[\"顯示評論 (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"没有可用用户\"],\"cSgpoS\":[\"置顶私聊\"],\"cXeEKu\":[\"這也會關閉下方 \",[\"0\"],\" 個綁定的網路。\"],\"cde3ce\":[\"发消息给 <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"複製格式化輸出\"],\"cl/A5J\":[\"欢迎来到 \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"删除\"],\"coPLXT\":[\"我們不在伺服器上儲存您的 IRC 通訊\"],\"crYH/6\":[\"SoundCloud 播放器\"],\"cv5DQb\":[\"未設定主機\"],\"d3sis4\":[\"添加服务器\"],\"d9aN5k\":[\"将 \",[\"username\"],\" 移出频道\"],\"dEgA5A\":[\"取消\"],\"dGi1We\":[\"取消置顶此私信对话\"],\"dJVuyC\":[\"離開了 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"至\"],\"dXqxlh\":[\"<0>⚠️ 安全风险!0> 此连接可能容易遭受窃听或中间人攻击。\"],\"da9Q/R\":[\"已更改频道模式\"],\"dhJN3N\":[\"顯示評論\"],\"dj2xTE\":[\"关闭通知\"],\"dpCzmC\":[\"洪水保護設定\"],\"e9dQpT\":[\"是否在新标签页中打开此链接?\"],\"ePK91l\":[\"编辑\"],\"eYBDuB\":[\"上傳圖片或提供含可選 \",[\"size\"],\" 替換的 URL\"],\"edBbee\":[\"通过 hostmask 封禁 \",[\"username\"],\"(阻止其从相同 IP/主机重新加入)\"],\"ekfzWq\":[\"使用者設定\"],\"elPDWs\":[\"自定义您的 IRC 客户端体验\"],\"eu2osY\":[\"<0>💡 建议:0> 仅在您信任此服务器并了解相关风险的情况下继续操作。避免通过此连接共享敏感信息或密码。\"],\"euEhbr\":[\"點擊加入 \",[\"channel\"]],\"ez3vLd\":[\"啟用多行輸入\"],\"f0J5Ki\":[\"服务器之间的通信可能使用未加密的连接\"],\"f9BHJk\":[\"警告用户\"],\"fDOLLd\":[\"未找到頻道。\"],\"ffzDkB\":[\"匿名分析:\"],\"fq1GF9\":[\"顯示使用者中斷伺服器連線的事件\"],\"gCldcN\":[\"變更強調色\"],\"gEF57C\":[\"此伺服器僅支援一種連線類型\"],\"gJuLUI\":[\"忽略清單\"],\"gNzMrk\":[\"目前頭像\"],\"gjPWyO\":[\"輸入暱稱...\"],\"gz6UQ3\":[\"最大化\"],\"h6/IMX\":[\"新增您的第一個網路\"],\"h6razj\":[\"排除頻道名稱遮罩\"],\"hG6jnw\":[\"未设置主题\"],\"hG89Ed\":[\"圖片\"],\"hYgDIe\":[\"建ç«\"],\"hZ6znB\":[\"端口\"],\"ha+Bz5\":[\"例如:100:1440\"],\"he3ygx\":[\"è¤è£½\"],\"hehnjM\":[\"數量\"],\"hzdLuQ\":[\"只有獲得發言權或更高權限的使用者才能發言\"],\"i0qMbr\":[\"主页\"],\"iDNBZe\":[\"通知\"],\"iH8pgl\":[\"返回\"],\"iL9SZg\":[\"封禁用户(按昵称)\"],\"iNt+3c\":[\"返回图片\"],\"iQvi+a\":[\"不再提醒我此服务器的低链路安全问题\"],\"iSLIjg\":[\"連線\"],\"iWXkHH\":[\"半管理员\"],\"iZeTtp\":[\"服务器地址\"],\"idD8Ev\":[\"已儲存\"],\"iivqkW\":[\"登入時間\"],\"ij+Elv\":[\"图片预览\"],\"ilIWp7\":[\"切换通知\"],\"iuaqvB\":[\"使用 * 作為萬用字元。範例:baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"机器人\"],\"j2DGR0\":[\"依主機遮罩封禁\"],\"jA4uoI\":[\"話題:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"原因(可选)\"],\"jUV7CU\":[\"上傳頭像\"],\"jW5Uwh\":[\"控制載入的外部媒體量。關閉 / 安全 / 可信來源 / 所有內容。\"],\"jXzms5\":[\"附件选项\"],\"jZlrte\":[\"颜色\"],\"jfC/xh\":[\"聯絡方式\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"載入較早的訊息\"],\"k3ID0F\":[\"筛选成员…\"],\"k65gsE\":[\"深入查看\"],\"k7Zgob\":[\"取消连接\"],\"kAVx5h\":[\"未找到邀請\"],\"kCLEPU\":[\"已連線至\"],\"kF5LKb\":[\"已忽略的模式:\"],\"kGeOx/\":[\"加入 \",[\"0\"]],\"kITKr8\":[\"正在載入頻道模式...\"],\"kPpPsw\":[\"您是 IRC Operator\"],\"kWJmRL\":[\"您\"],\"kfcRb0\":[\"头像\"],\"kjMqSj\":[\"複製 JSON\"],\"krViRy\":[\"點擊以 JSON 格式複製\"],\"ks71ra\":[\"例外\"],\"kw4lRv\":[\"頻道半操作員\"],\"kxgIRq\":[\"选择或添加频道以开始使用。\"],\"ky6dWe\":[\"头像预览\"],\"l+GxCv\":[\"正在載入頻道...\"],\"l+IUVW\":[\"帳戶 \",[\"account\"],\" 驗證成功:\",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"other\":[\"已重新連線 \",[\"reconnectCount\"],\" 次\"]}]],\"l1l8sj\":[[\"0\"],\" 天å\"],\"l5NhnV\":[\"#é\xA0»éï¼é¸å¡«ï¼\"],\"l5jmzx\":[[\"0\"],\" 和 \",[\"1\"],\" 正在输入...\"],\"lCF0wC\":[\"éæ°æ´ç\"],\"lHy8N5\":[\"正在載入更多頻道...\"],\"lasgrr\":[\"已使ç¨\"],\"lbpf14\":[\"加入 \",[\"value\"]],\"lfFsZ4\":[\"頻道\"],\"lkNdiH\":[\"帳戶名稱\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"上传图片\"],\"loQxaJ\":[\"我回来了\"],\"lvfaxv\":[\"首頁\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"新增\"],\"m8flAk\":[\"預覽(尚未上傳)\"],\"mEPxTp\":[\"<0>⚠️ 请注意!0> 仅打开来自可信来源的链接。恶意链接可能危害您的安全或隐私。\"],\"mHGdhG\":[\"伺服器資訊\"],\"mHS8lb\":[\"发消息到 #\",[\"0\"]],\"mMYBD9\":[\"寬泛 – 更廣的保護範圍\"],\"mTGsPd\":[\"頻道主題\"],\"mU8j6O\":[\"禁止外部訊息 (+n)\"],\"mZp8FL\":[\"自動回退至單行\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"評論 (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"用户已认证\"],\"mwtcGl\":[\"关闭评论\"],\"myL0MR\":[\"刪除此網路?\"],\"mzI/c+\":[\"下载\"],\"n3fGRk\":[\"由 \",[\"0\"],\" 設定\"],\"n5+j9l\":[\"例如 <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"寬鬆 – 較弱的保護\"],\"nNflMD\":[\"离开频道\"],\"nPXkBi\":[\"正在載入 WHOIS 資料...\"],\"nQnxxF\":[\"发消息到 #\",[\"0\"],\"(Shift+Enter 换行)\"],\"nWMRxa\":[\"取消置顶\"],\"nkC032\":[\"无洪水防护配置\"],\"o69z4d\":[\"向 \",[\"username\"],\" 发送警告消息\"],\"o9ylQi\":[\"搜尋 GIF 以開始\"],\"oFGkER\":[\"服务器通知\"],\"oOi11l\":[\"滚动到底部\"],\"oPYIL5\":[\"網路\"],\"oQEzQR\":[\"新私訊\"],\"oXOSPE\":[\"在线\"],\"oal760\":[\"服务器链路可能遭受中间人攻击\"],\"oeqmmJ\":[\"受信任来源\"],\"optX0N\":[[\"0\"],\" å°æå\"],\"ovBPCi\":[\"默认\"],\"p0Z69r\":[\"模式不能為空\"],\"p1KgtK\":[\"音频加载失败\"],\"p59pEv\":[\"更多詳情\"],\"p7sRI6\":[\"讓其他人知道您正在輸入\"],\"pBm1od\":[\"秘密频道\"],\"pNmiXx\":[\"所有伺服器的預設暱稱\"],\"pUUo9G\":[\"主機名:\"],\"pVGPmz\":[\"账户密码\"],\"peNE68\":[\"永久\"],\"plhHQt\":[\"無資料\"],\"pm6+q5\":[\"安全警告\"],\"pn5qSs\":[\"其他資訊\"],\"q0cR4S\":[\"現在稱為 **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"頻道不會出現在 LIST 或 NAMES 指令中\"],\"qLpTm/\":[\"移除表情 \",[\"emoji\"]],\"qVkGWK\":[\"置顶\"],\"qY8wNa\":[\"主页\"],\"qb0xJ7\":[\"萬用字元:* 符合任意字元,? 符合單一字元。範例:nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"頻道金鑰 (+k)\"],\"qtoOYG\":[\"无限制\"],\"r1W2AS\":[\"檔案託管圖片\"],\"rIPR2O\":[\"主題設定時間早於(分鐘前)\"],\"rMMSYo\":[\"最大長度為 \",[\"0\"]],\"rWtzQe\":[\"網路已分裂並重新連接。✅\"],\"rYG2u6\":[\"请稍候...\"],\"rdUucN\":[\"预览\"],\"rjGI/Q\":[\"隐私\"],\"rk8iDX\":[\"正在載入 GIF...\"],\"rn6SBY\":[\"取消靜音\"],\"s/UKqq\":[\"已被踢出频道\"],\"s7oqXR\":[\"選擇網路\"],\"s8cATI\":[\"加入了 \",[\"channelName\"]],\"sCO9ue\":[\"与 <0>\",[\"serverName\"],\"0> 的连接存在以下安全问题:\"],\"sGH11W\":[\"伺服器\"],\"sHI1H+\":[\"現在稱為 **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" 邀請您加入 \",[\"channel\"]],\"sUBSbK\":[\"尚無上游網路。\"],\"sby+1/\":[\"點擊複製\"],\"sfN25C\":[\"您的真實姓名或全名\"],\"sliuzR\":[\"打开链接\"],\"sqrO9R\":[\"自訂提及\"],\"sr6RdJ\":[\"Shift+Enter 換行\"],\"swrCpB\":[\"頻道已由 \",[\"user\"],\" 從 \",[\"oldName\"],\" 重新命名為 \",[\"newName\"],[\"0\"]],\"sxkWRg\":[\"進階\"],\"t/YqKh\":[\"移除\"],\"t47eHD\":[\"您在此伺服器上的唯一識別碼\"],\"tAkAh0\":[\"含可選 \",[\"size\"],\" 替換的 URL。範例:https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"显示或隐藏频道列表侧栏\"],\"tfDRzk\":[\"保存\"],\"tiBsJk\":[\"離開了 \",[\"channelName\"]],\"tt4/UD\":[\"退出了 (\",[\"reason\"],\")\"],\"u0TcnO\":[\"暱稱 {nick} 已被使用,正在用 {newNick} 重試\"],\"u0a8B4\":[\"以 IRC 操作員身分進行管理存取認證\"],\"u0rWFU\":[\"建立時間晚於(分鐘前)\"],\"u72w3t\":[\"要忽略的使用者和模式\"],\"u7jc2L\":[\"退出了\"],\"uAQUqI\":[\"状态\"],\"uB85T3\":[\"儲存失敗:\",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC 伺服器:\"],\"ukyW4o\":[\"æ¨çéè«é£çµ\"],\"usSSr/\":[\"缩放级别\"],\"v7uvcf\":[\"軟體:\"],\"vE8kb+\":[\"Shift+Enter 換行(Enter 傳送)\"],\"vERlcd\":[\"个人资料\"],\"vK0RL8\":[\"無主題\"],\"vSJd18\":[\"影片\"],\"vXIe7J\":[\"语言\"],\"vaHYxN\":[\"真实姓名\"],\"vhjbKr\":[\"离开\"],\"w/nogd\":[[\"0\"],\" 個網路\",[\"1\"],\" — 選擇一個加入\"],\"w4NYox\":[[\"title\"],\" 客戶端\"],\"w8xQRx\":[\"值無效\"],\"wFjjxZ\":[\"被 \",[\"username\"],\" 踢出 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"未找到封禁例外\"],\"wPrGnM\":[\"頻道管理員\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"顯示使用者加入或離開頻道的事件\"],\"whqZ9r\":[\"要醒目提示的額外詞語或短語\"],\"wm7RV4\":[\"通知聲音\"],\"wz/Yoq\":[\"您的消息在服务器之间转发时可能被截获\"],\"x3+y8b\":[\"å·²é鿤é£çµè¨»åç人æ¸\"],\"xCJdfg\":[\"清除\"],\"xOTzt5\":[\"åå\"],\"xUHRTR\":[\"連線時自動以操作員身分認證\"],\"xWHwwQ\":[\"封禁\"],\"xYilR2\":[\"媒体\"],\"xbi8D6\":[\"æ¤ä¼ºæå¨ä¸æ¯æ´éè«é£çµï¼æªå
¬å<0>obby.world/invitation0>åè½ï¼ãæ¨ä»å¯æ£å¸¸èå¤©ï¼æ¤é¢æ¿å
é©ç¨æ¼æ¡ç¨ obbyircd ç網路ã\"],\"xceQrO\":[\"仅支持安全的 WebSocket 连接\"],\"xdtXa+\":[\"頻道名稱\"],\"xfXC7q\":[\"文字頻道\"],\"xlCYOE\":[\"正在載入更多訊息...\"],\"xlhswE\":[\"最小值為 \",[\"0\"]],\"xq97Ci\":[\"添加词语或短语...\"],\"xuRqRq\":[\"用戶端限制 (+l)\"],\"xwF+7J\":[[\"0\"],\" 正在输入...\"],\"y1eoq1\":[\"è¤è£½é£çµ\"],\"yJztBY\":[\"刪除網路\"],\"yNeucF\":[\"此伺服器不支援擴充個人資料元資料(IRCv3 METADATA 擴充功能)。頭像、顯示名稱和狀態等欄位不可用。\"],\"yPlrca\":[\"頻道頭像\"],\"yQE2r9\":[\"載入中\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Oper 使用者名稱\"],\"yYOzWD\":[\"日志\"],\"yfx9Re\":[\"IRC 操作員密碼\"],\"ygCKqB\":[\"停止\"],\"ymDxJx\":[\"IRC 操作員使用者名稱\"],\"yrpRsQ\":[\"依名稱排序\"],\"yz7wBu\":[\"关闭\"],\"zJw+jA\":[\"設定模式:\",[\"0\"]],\"zbymaY\":[[\"0\"],\" åéå\"],\"zebeLu\":[\"输入 oper 用户名\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/zh-TW/messages.po b/src/locales/zh-TW/messages.po
index ec370e24..0204255e 100644
--- a/src/locales/zh-TW/messages.po
+++ b/src/locales/zh-TW/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - 將 IRC 帶入未來"
msgid "— open in viewer"
msgstr "— 在檢視器中開啟"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(繼承)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(未變更)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} 和 {1} 正在输入..."
msgid "{0} is typing..."
msgstr "{0} 正在输入..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} 個網路{1} — 選擇一個加入"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -69,17 +85,17 @@ msgstr "{0}、{1}、{2} 以及另外 {3} 人正在输入..."
#. placeholder {0}: Math.floor(secs / 86400)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}d ago"
-msgstr "{0} 天前"
+msgstr "{0} 天å"
#. placeholder {0}: Math.floor(secs / 3600)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}h ago"
-msgstr "{0} 小時前"
+msgstr "{0} å°æå"
#. placeholder {0}: Math.floor(secs / 60)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}m ago"
-msgstr "{0} 分鐘前"
+msgstr "{0} åéå"
#: src/lib/eventGrouping.ts
msgid "{c, plural, one {1 time} other {{c} times}}"
@@ -115,7 +131,7 @@ msgstr "*spam*"
#: src/components/ui/InvitationsPanel.tsx
msgid "#channel (optional)"
-msgstr "#頻道(選填)"
+msgstr "#é »éï¼é¸å¡«ï¼"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "#new-channel-name"
@@ -205,6 +221,12 @@ msgstr "新增邀請遮罩(例如 nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "添加 IRC 服务器"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "新增網路"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "新增規則"
msgid "Add Server"
msgstr "添加服务器"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "新增您的第一個網路"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "更多詳情"
@@ -377,6 +403,10 @@ msgstr "返回"
msgid "Back to image"
msgstr "返回图片"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "返回網路列表"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "通过 hostmask 封禁 {username}(阻止其从相同 IP/主机重新加入)"
@@ -424,6 +454,10 @@ msgstr "浏览服务器上的所有频道"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "取消连接"
msgid "Cancel reply"
msgstr "取消回复"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "變更強調色"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "更改频道名称(仅限管理员)"
@@ -564,7 +603,7 @@ msgstr "清除搜索"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "再按一次以確認"
+msgstr "åæä¸æ¬¡ä»¥ç¢ºèª"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "用戶端限制 (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -666,9 +707,10 @@ msgstr "配置通知声音和高亮提示"
#: src/components/ui/InvitationsPanel.tsx
msgid "Confirm?"
-msgstr "確認?"
+msgstr "確èªï¼"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "連線"
@@ -711,11 +753,11 @@ msgstr "已复制"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy"
-msgstr "複製"
+msgstr "è¤è£½"
#: src/components/ui/RawLogViewer.tsx
msgid "Copy all"
-msgstr "全部複製"
+msgstr "å
¨é¨è¤è£½"
#: src/components/message/JsonLogMessage.tsx
msgid "Copy entire JSON"
@@ -731,7 +773,7 @@ msgstr "複製 JSON"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy link"
-msgstr "複製連結"
+msgstr "è¤è£½é£çµ"
#: src/components/ui/ExternalLinkWarningModal.tsx
msgid "Copy URL"
@@ -739,15 +781,15 @@ msgstr "复制链接"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create"
-msgstr "建立"
+msgstr "建ç«"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "建立新的邀請連結"
+msgstr "å»ºç«æ°çéè«é£çµ"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "建立並管理您的邀請連結"
+msgstr "建ç«ä¸¦ç®¡çæ¨çéè«é£çµ"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,27 +856,52 @@ msgstr "删除频道"
msgid "Delete message"
msgstr "删除消息"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "刪除網路"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "删除私聊"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "刪除此邀請"
+msgstr "åªé¤æ¤éè«"
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "刪除此訊息?此操作無法復原。"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "刪除此網路?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "描述(選填,例如「第三季 Beta 測試者」)"
+msgstr "æè¿°ï¼é¸å¡«ï¼ä¾å¦ã第ä¸å£ Beta 測試è
ãï¼"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "断开连接"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "中斷 <0>{0}0> 的連線?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "中斷與 soju bouncer 的連線?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "中斷網路連線?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "探索"
@@ -891,20 +958,31 @@ msgstr "下载"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "拖放檔案以上傳"
+msgstr "ææ¾æªæ¡ä»¥ä¸å³"
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "例如 <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "例如:100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "编辑"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "編輯 {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "編輯資料"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "首頁"
msgid "Homepage"
msgstr "主页"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "主機"
@@ -1285,7 +1364,7 @@ msgstr "已加入频道"
#: src/components/ui/InvitationsPanel.tsx
msgid "just now"
-msgstr "剛剛"
+msgstr "åå"
#: src/components/ui/ModerationModal.tsx
#: src/components/ui/UserContextMenu.tsx
@@ -1323,7 +1402,7 @@ msgstr "离开频道"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "頻道留空可建立一般網路邀請。描述僅供您自己記錄使用——只有您能在此清單中看到。"
+msgstr "é »éç空å¯å»ºç«ä¸è¬ç¶²è·¯éè«ãæè¿°å
便¨èªå·±è¨é使ç¨ââåªææ¨è½å¨æ¤æ¸
å®ä¸çå°ã"
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "已离开频道"
msgid "Let others know when you are typing"
msgstr "讓其他人知道您正在輸入"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "链接预览"
@@ -1375,6 +1458,10 @@ msgstr "正在載入 GIF..."
msgid "Loading more channels..."
msgstr "正在載入更多頻道..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "正在從您的 bouncer 載入網路…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "正在載入 WHOIS 資料..."
@@ -1563,12 +1650,23 @@ msgstr "名稱:"
#: src/components/ui/InvitationsPanel.tsx
msgid "network"
-msgstr "網路"
+msgstr "網路"
+
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "透過 soju bouncer 綁定的網路"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "网络名称"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "{0} 上的網路"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "新私訊"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host(例如:spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "未選擇檔案"
msgid "No flood profile"
msgstr "无洪水防护配置"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "未設定主機"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "未找到邀請"
@@ -1672,7 +1775,7 @@ msgstr "未加载任何媒体预览。"
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "尚未擷取到任何原始 IRC 流量。請嘗試連線或傳送訊息。"
+msgstr "å°æªæ·åå°ä»»ä½åå§ IRC æµéãè«å試é£ç·æå³éè¨æ¯ã"
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "未找到結果"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "尚未選擇伺服器。請先從側邊欄選擇一個伺服器;邀請連結是依伺服器管理的。"
+msgstr "å°æªé¸æä¼ºæå¨ãè«å
å¾å´éæ¬é¸æä¸å伺æå¨ï¼éè«é£çµæ¯ä¾ä¼ºæå¨ç®¡ççã"
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "未设置主题"
msgid "No unread mentions or messages"
msgstr "沒有未讀提及或訊息"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "尚無上游網路。"
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "没有可用用户"
@@ -1784,6 +1891,10 @@ msgstr "哎呀!網路分裂!⚠️"
msgid "Op"
msgstr "管理员"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "開啟"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "打开频道配置设置"
@@ -1887,6 +1998,10 @@ msgstr "置顶私聊"
msgid "Pin this private message conversation"
msgstr "置顶此私信对话"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "明文"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "私信用户"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "端口"
@@ -2006,6 +2122,7 @@ msgstr "对此消息做出了回应"
msgid "Read more"
msgstr "阅读更多"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "原因"
msgid "Reason (optional)"
msgstr "原因(可选)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "重新连接服务器"
@@ -2031,7 +2149,7 @@ msgstr "重新连接服务器"
#: src/components/ui/InvitationsPanel.tsx
#: src/components/ui/InvitationsPanel.tsx
msgid "Refresh"
-msgstr "重新整理"
+msgstr "éæ°æ´ç"
#: src/components/ui/AddServerModal.tsx
msgid "Register for an account"
@@ -2095,6 +2213,7 @@ msgstr "規則"
msgid "Safe"
msgstr "安全"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "跳转"
msgid "Select a channel"
msgstr "选择频道"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "選擇網路"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "选择成员"
@@ -2276,6 +2399,10 @@ msgstr "网络上的服务器管理员可能读取您的消息"
msgid "Server Password"
msgstr "服务器密码"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "伺服器密碼 (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "服务器之间的通信可能使用未加密的连接"
@@ -2380,6 +2507,12 @@ msgstr "登入時間"
msgid "Software:"
msgstr "軟體:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "soju bouncer(控制)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "依名稱排序"
@@ -2457,7 +2590,11 @@ msgstr "此图片已过期"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "已透過此連結註冊的人數"
+msgstr "å·²é鿤é£çµè¨»åç人æ¸"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "這會將網路從您的 soju bouncer 中移除。若要再次使用,您需要重新新增。"
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "此伺服器不支援擴充個人資料元資料(IRCv3 METADATA 擴充
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "此伺服器不支援邀請連結(未公告<0>obby.world/invitation0>功能)。您仍可正常聊天;此面板僅適用於採用 obbyircd 的網路。"
+msgstr "æ¤ä¼ºæå¨ä¸æ¯æ´éè«é£çµï¼æªå
¬å<0>obby.world/invitation0>åè½ï¼ãæ¨ä»å¯æ£å¸¸èå¤©ï¼æ¤é¢æ¿å
é©ç¨æ¼æ¡ç¨ obbyircd ç網路ã"
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "此伺服器僅支援一種連線類型"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "這也會關閉下方 {0} 個綁定的網路。"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "這也會關閉下方綁定的網路。"
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "時間(分鐘)"
@@ -2479,6 +2625,10 @@ msgstr "時間(分鐘)"
msgid "Time Window (seconds)"
msgstr "時間視窗(秒)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "話題:"
msgid "Total: {0}"
msgstr "總計:{0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "傳輸方式"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "受信任来源"
@@ -2615,7 +2769,7 @@ msgstr "萬用字元:* 符合任意字元,? 符合單一字元。範例:ni
#: src/components/ui/InvitationsPanel.tsx
msgid "used"
-msgstr "已使用"
+msgstr "已使ç¨"
#: src/components/message/JsonLogMessage.tsx
#: src/components/ui/UserProfileModal.tsx
@@ -2641,6 +2795,7 @@ msgstr "用户资料"
msgid "User Settings"
msgstr "使用者設定"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "寬泛 – 更廣的保護範圍"
msgid "Will default to 'no reason' if left empty"
msgstr "留空将默认显示\"无原因\""
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "是,刪除"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "您有未保存的更改。确定要关闭而不保存吗?"
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "您尚未建立任何邀請連結。請使用上方表單建立第一個。"
+msgstr "æ¨å°æªå»ºç«ä»»ä½éè«é£çµãè«ä½¿ç¨ä¸æ¹è¡¨å®å»ºç«ç¬¬ä¸åã"
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "您邀請了 {target} 加入 {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "已連接到 <0>{0}0>。"
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "您的帳戶認證密碼"
@@ -2822,13 +2986,17 @@ msgstr "您的帳戶認證密碼"
msgid "Your account username for authentication"
msgstr "您的帳戶認證使用者名稱"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "您的 bouncer 還沒有任何網路。新增一個以開始使用。"
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "所有伺服器的預設暱稱"
#: src/components/ui/InvitationsPanel.tsx
msgid "Your invite links"
-msgstr "您的邀請連結"
+msgstr "æ¨çéè«é£çµ"
#: src/components/ui/UserSettings.tsx
msgid "Your messages and settings are stored locally on your device"
diff --git a/src/locales/zh/messages.mjs b/src/locales/zh/messages.mjs
index 4d7c83a5..b6fe5858 100644
--- a/src/locales/zh/messages.mjs
+++ b/src/locales/zh/messages.mjs
@@ -1 +1 @@
-/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"无效的模式格式,请使用 nick!user@host 格式(允许通配符 *)\"],\"+6NQQA\":[\"通用支持频道\"],\"+6NyRG\":[\"客户端\"],\"+K0AvT\":[\"断开连接\"],\"+cyFdH\":[\"标记为离开时的默认消息\"],\"+mVPqU\":[\"在消息中渲染 Markdown 格式\"],\"+vqCJH\":[\"您的账户认证用户名\"],\"+yPBXI\":[\"选择文件\"],\"+zy2Nq\":[\"类型\"],\"/09cao\":[\"链路安全级别较低(级别 \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"频道外的用户无法发送消息\"],\"/4C8U0\":[\"全部复制\"],\"/6BzZF\":[\"切换成员列表\"],\"/AkXyp\":[\"确认?\"],\"/TNOPk\":[\"用户已离开\"],\"/XQgft\":[\"发现\"],\"/cF7Rs\":[\"音量\"],\"/dqduX\":[\"下一页\"],\"/fc3q4\":[\"所有内容\"],\"/kISDh\":[\"启用通知声音\"],\"/n04sB\":[\"踢出\"],\"/rTz0M\":[\"音频\"],\"/rfkZe\":[\"提及和消息时播放声音\"],\"0/0ZGA\":[\"频道名称掩码\"],\"0D6j7U\":[\"了解更多自定义规则 →\"],\"0XsHcR\":[\"踢出用户\"],\"0ZpE//\":[\"按用户数排序\"],\"0bEPwz\":[\"设置为离开\"],\"0dGkPt\":[\"展开频道列表\"],\"0gS7M5\":[\"显示名称\"],\"0kS+M8\":[\"示例网络\"],\"0rgoY7\":[\"仅连接您选择的服务器\"],\"0wdd7X\":[\"加入\"],\"0wkVYx\":[\"私信\"],\"111uHX\":[\"链接预览\"],\"196EG4\":[\"删除私聊\"],\"1DSr1i\":[\"注册账户\"],\"1O/24y\":[\"切换频道列表\"],\"1VPJJ2\":[\"外部链接警告\"],\"1ZC/dv\":[\"没有未读提及或消息\"],\"1pO1zi\":[\"服务器名称为必填项\"],\"1uwfzQ\":[\"查看频道主题\"],\"268g7c\":[\"输入显示名称\"],\"2F9+AZ\":[\"尚未捕获任何原始 IRC 流量。请尝试连接或发送一条消息。\"],\"2FOFq1\":[\"网络上的服务器管理员可能读取您的消息\"],\"2FYpfJ\":[\"更多\"],\"2HF1Y2\":[[\"inviter\"],\" 邀请了 \",[\"target\"],\" 加入 \",[\"channel\"]],\"2I70QL\":[\"查看用户资料信息\"],\"2QYdmE\":[\"用户:\"],\"2QpEjG\":[\"已离开\"],\"2YE223\":[\"发消息到 #\",[\"0\"],\"(Enter 换行,Shift+Enter 发送)\"],\"2bimFY\":[\"使用服务器密码\"],\"2iTmdZ\":[\"本地存储:\"],\"2odkwe\":[\"严格 – 更强的保护\"],\"2uDhbA\":[\"输入要邀请的用户名\"],\"2ygf/L\":[\"← 返回\"],\"2zEgxj\":[\"搜索 GIF...\"],\"3RdPhl\":[\"重命名频道\"],\"3THokf\":[\"有发言权的用户\"],\"3TSz9S\":[\"最小化\"],\"3jBDvM\":[\"频道显示名称\"],\"3ryuFU\":[\"可选的崩溃报告以改善应用\"],\"3uBF/8\":[\"关闭查看器\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"输入账户名称...\"],\"4/Rr0R\":[\"邀请用户加入当前频道\"],\"4EZrJN\":[\"规则\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"洪水配置文件 (+F)\"],\"4RZQRK\":[\"你在做什么?\"],\"4hfTrB\":[\"昵称\"],\"4n99LO\":[\"已在 \",[\"0\"]],\"4t6vMV\":[\"短消息自动切换到单行\"],\"4vsHmf\":[\"时间(分钟)\"],\"5+INAX\":[\"高亮提及您的消息\"],\"5R5Pv/\":[\"Oper 用户名\"],\"678PKt\":[\"网络名称\"],\"6Aih4U\":[\"离线\"],\"6CO3WE\":[\"加入频道需要密码,留空可移除密钥。\"],\"6HhMs3\":[\"退出消息\"],\"6V3Ea3\":[\"已复制\"],\"6lGV3K\":[\"收起\"],\"6yFOEi\":[\"输入 oper 密码...\"],\"7+IHTZ\":[\"未选择文件\"],\"73hrRi\":[\"nick!user@host(例如:spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"发送私信\"],\"7U1W7c\":[\"非常宽松\"],\"7Y1YQj\":[\"真实姓名:\"],\"7YHArF\":[\"— 在查看器中打开\"],\"7fjnVl\":[\"搜索用户...\"],\"7jL88x\":[\"删除此消息?此操作无法撤销。\"],\"7nGhhM\":[\"您在想什么?\"],\"7sEpu1\":[\"成员 — \",[\"0\"]],\"7sNhEz\":[\"用户名\"],\"8H0Q+x\":[\"了解更多配置文件 →\"],\"8Phu0A\":[\"显示用户更改昵称的事件\"],\"8XTG9e\":[\"输入 oper 密码\"],\"8XsV2J\":[\"重新发送\"],\"8ZsakT\":[\"密码\"],\"8kR84m\":[\"您即将打开一个外部链接:\"],\"8lCgih\":[\"删除规则\"],\"8o3dPc\":[\"拖放文件以上传\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"other\":[\"已加入 \",[\"joinCount\"],\" 次\"]}]],\"9BMLnJ\":[\"重新连接服务器\"],\"9OEgyT\":[\"添加表情\"],\"9PQ8m2\":[\"G-Line(全局封禁)\"],\"9Qs99X\":[\"邮箱:\"],\"9QupBP\":[\"删除规则\"],\"9bG48P\":[\"发送中\"],\"9f5f0u\":[\"有隐私问题?联系我们:\"],\"9unqs3\":[\"离开:\"],\"9v3hwv\":[\"未找到服务器。\"],\"9zb2WA\":[\"连接中\"],\"A1taO8\":[\"搜索\"],\"A2adVi\":[\"发送正在输入通知\"],\"A9Rhec\":[\"频道名称\"],\"AWOSPo\":[\"放大\"],\"AXSpEQ\":[\"连接时获取 Oper\"],\"AeXO77\":[\"账户\"],\"AhNP40\":[\"跳转\"],\"Ai2U7L\":[\"主机\"],\"AjBQnf\":[\"已更改昵称\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"取消回复\"],\"ApSx0O\":[\"找到 \",[\"0\"],\" 条匹配\\\"\",[\"searchQuery\"],\"\\\"的消息\"],\"AxPAXW\":[\"未找到结果\"],\"AyNqAB\":[\"在聊天中显示所有服务器事件\"],\"B/QqGw\":[\"暂时离开\"],\"B8AaMI\":[\"此字段为必填项\"],\"BA2c49\":[\"服务器不支持高级 LIST 筛选\"],\"BDKt3I\":[[\"0\"],\"、\",[\"1\"],\"、\",[\"2\"],\" 以及另外 \",[\"3\"],\" 人正在输入...\"],\"BGul2A\":[\"您有未保存的更改。确定要关闭而不保存吗?\"],\"BIf9fi\":[\"您的状态消息\"],\"BPm98R\":[\"未选择服务器。请先从侧边栏选择一个服务器;邀请链接按服务器分别管理。\"],\"BZz3md\":[\"您的个人网站\"],\"Bgm/H7\":[\"允许输入多行文本\"],\"BiQIl1\":[\"置顶此私信对话\"],\"BlNZZ2\":[\"点击跳转到消息\"],\"Bowq3c\":[\"只有管理员可以更改频道主题\"],\"Btozzp\":[\"此图片已过期\"],\"Bycfjm\":[\"总计:\",[\"0\"]],\"C6IBQc\":[\"复制完整 JSON\"],\"C9L9wL\":[\"数据收集\"],\"CDq4wC\":[\"管理用户\"],\"CHVRxG\":[\"发消息给 @\",[\"0\"],\"(Shift+Enter 换行)\"],\"CN9zdR\":[\"Oper 用户名和密码为必填项\"],\"CW3sYa\":[\"添加表情 \",[\"emoji\"]],\"CaAkqd\":[\"显示退出事件\"],\"CbvaYj\":[\"按昵称封禁\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"选择频道\"],\"CsekCi\":[\"普通\"],\"D+NlUC\":[\"系统\"],\"D28t6+\":[\"已加入并退出\"],\"DB8zMK\":[\"应用\"],\"DBcWHr\":[\"自定义通知音频文件\"],\"DTy9Xw\":[\"媒体预览\"],\"Dj4pSr\":[\"选择一个安全密码\"],\"Du+zn+\":[\"搜索中...\"],\"Du2T2f\":[\"未找到设置\"],\"DwsSVQ\":[\"应用筛选并刷新\"],\"E3W/zd\":[\"默认昵称\"],\"E6nRW7\":[\"复制链接\"],\"E703RG\":[\"模式:\"],\"EAeu1Z\":[\"发送邀请\"],\"EFKJQT\":[\"设置\"],\"EGPQBv\":[\"自定义洪水规则 (+f)\"],\"ELik0r\":[\"查看完整隐私政策\"],\"EPbeC2\":[\"查看或编辑频道主题\"],\"EQCDNT\":[\"输入 oper 用户名...\"],\"EUvulZ\":[\"找到 1 条匹配\\\"\",[\"searchQuery\"],\"\\\"的消息\"],\"EatZYJ\":[\"下一张图片\"],\"EdQY6l\":[\"无\"],\"EnqLYU\":[\"搜索服务器...\"],\"F0OKMc\":[\"编辑服务器\"],\"F6Int2\":[\"启用高亮\"],\"FDoLyE\":[\"最大用户数\"],\"FUU/hZ\":[\"控制聊天中加载的外部媒体数量。\"],\"Fdp03t\":[\"开启\"],\"FfPWR0\":[\"弹窗\"],\"FjkaiT\":[\"缩小\"],\"FlqOE9\":[\"这意味着:\"],\"FolHNl\":[\"管理您的账户和身份验证\"],\"Fp2Dif\":[\"已退出服务器\"],\"G5KmCc\":[\"GZ-Line(全局 Z-Line)\"],\"GDs0lz\":[\"<0>风险:0> 敏感信息(消息、私人对话、身份验证详情)可能会暴露给网络管理员或位于 IRC 服务器之间的攻击者。\"],\"GR+2I3\":[\"添加邀请掩码(例如 nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"关闭弹出的服务器通知\"],\"GdhD7H\":[\"再次点击以确认\"],\"GlHnXw\":[\"昵称修改失败: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"预览:\"],\"GtmO8/\":[\"来自\"],\"GtuHUQ\":[\"在服务器上重命名此频道,所有用户都会看到新名称。\"],\"GuGfFX\":[\"切换搜索\"],\"GxkJXS\":[\"上传中...\"],\"GzbwnK\":[\"已加入频道\"],\"GzsUDB\":[\"扩展资料\"],\"H/PnT8\":[\"插入表情\"],\"H6Izzl\":[\"您的首选颜色代码\"],\"H9jIv+\":[\"显示加入/离开\"],\"HAKBY9\":[\"上传文件\"],\"HdE1If\":[\"频道\"],\"Hk4AW9\":[\"您的首选显示名称\"],\"HmHDk7\":[\"选择成员\"],\"HrQzPU\":[[\"networkName\"],\" 上的频道\"],\"I2tXQ5\":[\"发消息给 @\",[\"0\"],\"(Enter 换行,Shift+Enter 发送)\"],\"I6bw/h\":[\"封禁用户\"],\"I92Z+b\":[\"启用通知\"],\"I9D72S\":[\"您确定要删除此消息吗?此操作无法撤销。\"],\"IA+1wo\":[\"显示用户被踢出频道的事件\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"信息:\"],\"IUwGEM\":[\"保存更改\"],\"IVeGK6\":[[\"0\"],\"、\",[\"1\"],\" 和 \",[\"2\"],\" 正在输入...\"],\"IgrLD/\":[\"暂停\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"回复\"],\"IoHMnl\":[\"最大值为 \",[\"0\"]],\"IvMj+0\":[\"管理员\"],\"J28zul\":[\"正在连接...\"],\"J5T9NW\":[\"用户信息\"],\"J8Y5+z\":[\"哎呀!网络分裂!⚠️\"],\"JBHkBA\":[\"已离开频道\"],\"JCwL0Q\":[\"输入原因(可选)\"],\"JFciKP\":[\"切换\"],\"JXGkhG\":[\"更改频道名称(仅限管理员)\"],\"JcD7qf\":[\"更多操作\"],\"JdkA+c\":[\"隐秘 (+s)\"],\"Jmu12l\":[\"服务器频道\"],\"JvQ++s\":[\"启用 Markdown\"],\"K2jwh/\":[\"无可用 WHOIS 数据\"],\"KAXSwC\":[\"语音权限\"],\"KDfTdX\":[\"删除消息\"],\"KKBlUU\":[\"嵌入\"],\"KM0pLb\":[\"欢迎来到本频道!\"],\"KR6W2h\":[\"取消忽略用户\"],\"KV+Bi1\":[\"仅限邀请 (+i)\"],\"KdCtwE\":[\"在重置计数器之前监控洪水活动的秒数\"],\"Kkezga\":[\"服务器密码\"],\"KsiQ/8\":[\"用户必须受邀才能加入频道\"],\"L+gB/D\":[\"频道信息\"],\"LC1a7n\":[\"IRC 服务器报告其服务器间链路的安全级别较低。这意味着当您的消息在网络中的 IRC 服务器之间转发时,可能未经过妥善加密,或者 SSL/TLS 证书未被正确验证。\"],\"LNfLR5\":[\"显示踢出事件\"],\"LQb0W/\":[\"显示所有事件\"],\"LU7/yA\":[\"显示用的别名,可包含空格、表情和特殊字符。真实频道名(\",[\"channelName\"],\")仍用于 IRC 命令。\"],\"LUb9O7\":[\"需要有效的服务器端口\"],\"LV4fT6\":[\"描述(可选,例如 \\\"第三季度 Beta 测试者\\\")\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"隐私政策\"],\"LcuSDR\":[\"管理您的个人资料信息和元数据\"],\"LqLS9B\":[\"显示昵称变更\"],\"LsDQt2\":[\"频道设置\"],\"LtI9AS\":[\"频道所有者\"],\"LuNhhL\":[\"对此消息做出了回应\"],\"M/AZNG\":[\"头像图片 URL\"],\"M/WIer\":[\"发送消息\"],\"M8er/5\":[\"名称:\"],\"MHk+7g\":[\"上一张图片\"],\"MRorGe\":[\"私信用户\"],\"MVbSGP\":[\"时间窗口(秒)\"],\"MkpcsT\":[\"您的消息和设置存储在本地设备上\"],\"N/hDSy\":[\"标记为机器人——通常为 'on' 或空\"],\"N7TQbE\":[\"邀请用户加入 \",[\"channelName\"]],\"NCca/o\":[\"输入默认昵称...\"],\"Nqs6B9\":[\"显示所有外部媒体。任何 URL 都可能向未知服务器发送请求。\"],\"Nt+9O7\":[\"使用 WebSocket 而非原始 TCP\"],\"NxIHzc\":[\"踢出用户\"],\"O+v/cL\":[\"浏览服务器上的所有频道\"],\"ODwSCk\":[\"发送 GIF\"],\"OGQ5kK\":[\"配置通知声音和高亮提示\"],\"OIPt1Z\":[\"显示或隐藏成员列表侧栏\"],\"OKSNq/\":[\"非常严格\"],\"ONWvwQ\":[\"上传\"],\"OVKoQO\":[\"您的账户认证密码\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - 将 IRC 带入未来\"],\"OhCpra\":[\"设置主题…\"],\"OkltoQ\":[\"通过昵称封禁 \",[\"username\"],\"(阻止其使用相同昵称重新加入)\"],\"P+t/Te\":[\"无其他数据\"],\"P42Wcc\":[\"安全\"],\"PD38l0\":[\"频道头像预览\"],\"PD9mEt\":[\"输入消息...\"],\"PPqfdA\":[\"打开频道配置设置\"],\"PSCjfZ\":[\"此频道显示的主题,所有用户均可查看。\"],\"PZCecv\":[\"PDF 预览\"],\"PeLgsC\":[[\"c\",\"plural\",{\"other\":[[\"c\"],\" 次\"]}]],\"PguS2C\":[\"添加例外掩码(例如 nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"显示 \",[\"displayedChannelsCount\"],\",共 \",[\"0\"],\" 个频道\"],\"PqhVlJ\":[\"封禁用户(按 Hostmask)\"],\"Q+chwU\":[\"用户名:\"],\"Q2QY4/\":[\"删除此邀请\"],\"Q6hhn8\":[\"偏好设置\"],\"QF4a34\":[\"请输入用户名\"],\"QGqSZ2\":[\"颜色与格式\"],\"QJQd1J\":[\"编辑资料\"],\"QSzGDE\":[\"闲置\"],\"QUlny5\":[\"欢迎来到 \",[\"0\"],\"!\"],\"Qoq+GP\":[\"阅读更多\"],\"QuSkCF\":[\"筛选频道...\"],\"QwUrDZ\":[\"将话题更改为:\",[\"topic\"]],\"R0UH07\":[\"第 \",[\"0\"],\" 张,共 \",[\"1\"],\" 张\"],\"R7SsBE\":[\"静音\"],\"R8rf1X\":[\"点击设置主题\"],\"RArB3D\":[\"被 \",[\"username\"],\" 踢出 \",[\"channelName\"]],\"RI3cWd\":[\"使用 ObsidianIRC 探索 IRC 的世界\"],\"RIfHS5\":[\"创建新的邀请链接\"],\"RMMaN5\":[\"受管理 (+m)\"],\"RWw9Lg\":[\"关闭窗口\"],\"RZ2BuZ\":[\"账户 \",[\"account\"],\" 注册需要验证:\",[\"message\"]],\"RySp6q\":[\"隐藏评论\"],\"SPKQTd\":[\"昵称为必填项\"],\"SPVjfj\":[\"留空将默认显示\\\"无原因\\\"\"],\"SQKPvQ\":[\"邀请用户\"],\"SkZcl+\":[\"选择预定义的洪水保护配置文件。这些配置文件为不同使用场景提供均衡的保护设置。\"],\"Slr+3C\":[\"最小用户数\"],\"Spnlre\":[\"您邀请了 \",[\"target\"],\" 加入 \",[\"channel\"]],\"T/ckN5\":[\"在查看器中打开\"],\"T91vKp\":[\"播放\"],\"TV2Wdu\":[\"了解我们如何处理您的数据并保护您的隐私。\"],\"TgFpwD\":[\"正在应用...\"],\"TkzSFB\":[\"无更改\"],\"TtserG\":[\"输入真实姓名\"],\"Ttz9J1\":[\"输入密码...\"],\"Tz0i8g\":[\"设置\"],\"U3pytU\":[\"管理员\"],\"UDb2YD\":[\"添加表情\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"您还没有创建任何邀请链接。使用上面的表单来创建您的第一个。\"],\"UGT5vp\":[\"保存设置\"],\"UV5hLB\":[\"未找到封禁\"],\"Uaj3Nd\":[\"状态消息\"],\"Ue3uny\":[\"默认(无配置文件)\"],\"UkARhe\":[\"普通 – 标准保护\"],\"Umn7Cj\":[\"暂无评论,成为第一个吧!\"],\"UtUIRh\":[[\"0\"],\" 条旧消息\"],\"UwzP+U\":[\"安全连接\"],\"V0/A4O\":[\"频道所有者\"],\"V4qgxE\":[\"创建时间早于(分钟前)\"],\"V8yTm6\":[\"清除搜索\"],\"VJMMyz\":[\"ObsidianIRC - 将 IRC 带入未来\"],\"VJScHU\":[\"原因\"],\"VLsmVV\":[\"静音通知\"],\"VbyRUy\":[\"评论\"],\"Vmx0mQ\":[\"设置者:\"],\"VqnIZz\":[\"查看我们的隐私政策和数据使用规范\"],\"VrMygG\":[\"最小长度为 \",[\"0\"]],\"VrnTui\":[\"您的代词,显示在个人资料中\"],\"W8E3qn\":[\"已验证账户\"],\"WAakm9\":[\"删除频道\"],\"WFxTHC\":[\"添加封禁掩码(例如 nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"服务器地址为必填项\"],\"WRYdXW\":[\"音频进度\"],\"WUOH5B\":[\"忽略用户\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"other\":[\"显示另外 \",[\"1\"],\" 个项目\"]}]],\"WYxRzo\":[\"创建并管理您的邀请链接\"],\"Wd38W1\":[\"频道留空可用于通用网络邀请。描述仅用于您自己的记录——只有您能在此列表中看到。\"],\"Weq9zb\":[\"常规\"],\"Wfj7Sk\":[\"开启或关闭通知声音\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"用户资料\"],\"X6S3lt\":[\"搜索设置、频道、服务器...\"],\"XEHan5\":[\"仍然继续\"],\"XI1+wb\":[\"格式无效\"],\"XIXeuC\":[\"发消息给 @\",[\"0\"]],\"XMS+k4\":[\"发起私信\"],\"XWgxXq\":[\"相册\"],\"Xd7+IT\":[\"取消置顶私聊\"],\"Xm/s+u\":[\"显示\"],\"Xp2n93\":[\"显示来自服务器受信任文件主机的媒体。不会向外部服务发送任何请求。\"],\"XvjC4F\":[\"正在保存...\"],\"Y/qryO\":[\"未找到与搜索条件匹配的用户\"],\"YAqRpI\":[\"账户 \",[\"account\"],\" 注册成功:\",[\"message\"]],\"YEfzvP\":[\"受保护主题 (+t)\"],\"YQOn6a\":[\"收起成员列表\"],\"YRCoE9\":[\"频道操作员\"],\"YURQaF\":[\"查看资料\"],\"YdBSvr\":[\"控制媒体显示和外部内容\"],\"Yj6U3V\":[\"无中央服务器:\"],\"YjvpGx\":[\"代词\"],\"YqH4l4\":[\"无密钥\"],\"YyUPpV\":[\"账户:\"],\"ZJSWfw\":[\"断开服务器时显示的消息\"],\"ZR1dJ4\":[\"邀请\"],\"ZdWg0V\":[\"在浏览器中打开\"],\"ZhRBbl\":[\"搜索消息…\"],\"Zmcu3y\":[\"高级筛选\"],\"a2/8e5\":[\"主题设置时间晚于(分钟前)\"],\"aHKcKc\":[\"上一页\"],\"aJTbXX\":[\"Oper 密码\"],\"aQryQv\":[\"模式已存在\"],\"aW9pLN\":[\"频道允许的最大用户数,留空表示无限制。\"],\"ah4fmZ\":[\"同时显示来自 YouTube、Vimeo、SoundCloud 及类似知名服务的预览。\"],\"aifXak\":[\"此频道没有媒体\"],\"ap2zBz\":[\"宽松\"],\"az8lvo\":[\"关\"],\"azXSNo\":[\"展开成员列表\"],\"azdliB\":[\"登录账户\"],\"b26wlF\":[\"她/她的\"],\"bD/+Ei\":[\"严格\"],\"bQ6BJn\":[\"配置详细的洪水保护规则。每条规则指定要监控的活动类型以及超过阈值时采取的操作。\"],\"beV7+y\":[\"用户将收到加入 \",[\"channelName\"],\" 的邀请。\"],\"bk84cH\":[\"离开消息\"],\"bkHdLj\":[\"添加 IRC 服务器\"],\"bmQLn5\":[\"添加规则\"],\"bwRvnp\":[\"操作\"],\"c8+EVZ\":[\"已验证账户\"],\"cGYUlD\":[\"未加载任何媒体预览。\"],\"cLF98o\":[\"显示评论 (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"没有可用用户\"],\"cSgpoS\":[\"置顶私聊\"],\"cde3ce\":[\"发消息给 <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"复制格式化输出\"],\"cl/A5J\":[\"欢迎来到 \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"删除\"],\"coPLXT\":[\"我们不在服务器上存储您的 IRC 通信\"],\"crYH/6\":[\"SoundCloud 播放器\"],\"d3sis4\":[\"添加服务器\"],\"d9aN5k\":[\"将 \",[\"username\"],\" 移出频道\"],\"dEgA5A\":[\"取消\"],\"dGi1We\":[\"取消置顶此私信对话\"],\"dJVuyC\":[\"离开了 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"至\"],\"dXqxlh\":[\"<0>⚠️ 安全风险!0> 此连接可能容易遭受窃听或中间人攻击。\"],\"da9Q/R\":[\"已更改频道模式\"],\"dhJN3N\":[\"显示评论\"],\"dj2xTE\":[\"关闭通知\"],\"dpCzmC\":[\"洪水保护设置\"],\"e9dQpT\":[\"是否在新标签页中打开此链接?\"],\"ePK91l\":[\"编辑\"],\"eYBDuB\":[\"上传图片或提供带可选 \",[\"size\"],\" 替换的 URL\"],\"edBbee\":[\"通过 hostmask 封禁 \",[\"username\"],\"(阻止其从相同 IP/主机重新加入)\"],\"ekfzWq\":[\"用户设置\"],\"elPDWs\":[\"自定义您的 IRC 客户端体验\"],\"eu2osY\":[\"<0>💡 建议:0> 仅在您信任此服务器并了解相关风险的情况下继续操作。避免通过此连接共享敏感信息或密码。\"],\"euEhbr\":[\"点击加入 \",[\"channel\"]],\"ez3vLd\":[\"启用多行输入\"],\"f0J5Ki\":[\"服务器之间的通信可能使用未加密的连接\"],\"f9BHJk\":[\"警告用户\"],\"fDOLLd\":[\"未找到频道。\"],\"ffzDkB\":[\"匿名分析:\"],\"fq1GF9\":[\"显示用户断开服务器连接的事件\"],\"gEF57C\":[\"此服务器仅支持一种连接类型\"],\"gJuLUI\":[\"忽略列表\"],\"gNzMrk\":[\"当前头像\"],\"gjPWyO\":[\"输入昵称...\"],\"gz6UQ3\":[\"最大化\"],\"h6razj\":[\"排除频道名称掩码\"],\"hG6jnw\":[\"未设置主题\"],\"hG89Ed\":[\"图片\"],\"hYgDIe\":[\"创建\"],\"hZ6znB\":[\"端口\"],\"ha+Bz5\":[\"例如:100:1440\"],\"he3ygx\":[\"复制\"],\"hehnjM\":[\"数量\"],\"hzdLuQ\":[\"只有获得发言权或更高权限的用户才能发言\"],\"i0qMbr\":[\"主页\"],\"iDNBZe\":[\"通知\"],\"iH8pgl\":[\"返回\"],\"iL9SZg\":[\"封禁用户(按昵称)\"],\"iNt+3c\":[\"返回图片\"],\"iQvi+a\":[\"不再提醒我此服务器的低链路安全问题\"],\"iSLIjg\":[\"连接\"],\"iWXkHH\":[\"半管理员\"],\"iZeTtp\":[\"服务器地址\"],\"idD8Ev\":[\"已保存\"],\"iivqkW\":[\"登录时间\"],\"ij+Elv\":[\"图片预览\"],\"ilIWp7\":[\"切换通知\"],\"iuaqvB\":[\"用 * 作通配符。示例:baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"机器人\"],\"j2DGR0\":[\"按主机掩码封禁\"],\"jA4uoI\":[\"话题:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"原因(可选)\"],\"jUV7CU\":[\"上传头像\"],\"jW5Uwh\":[\"控制加载的外部媒体量。关闭 / 安全 / 可信来源 / 所有内容。\"],\"jXzms5\":[\"附件选项\"],\"jZlrte\":[\"颜色\"],\"jfC/xh\":[\"联系方式\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"加载旧消息\"],\"k3ID0F\":[\"筛选成员…\"],\"k65gsE\":[\"深入查看\"],\"k7Zgob\":[\"取消连接\"],\"kAVx5h\":[\"未找到邀请\"],\"kCLEPU\":[\"已连接到\"],\"kF5LKb\":[\"已忽略的模式:\"],\"kGeOx/\":[\"加入 \",[\"0\"]],\"kITKr8\":[\"正在加载频道模式...\"],\"kPpPsw\":[\"您是 IRC Operator\"],\"kWJmRL\":[\"您\"],\"kfcRb0\":[\"头像\"],\"kjMqSj\":[\"复制 JSON\"],\"krViRy\":[\"点击以 JSON 格式复制\"],\"ks71ra\":[\"例外\"],\"kw4lRv\":[\"频道半操作员\"],\"kxgIRq\":[\"选择或添加频道以开始使用。\"],\"ky6dWe\":[\"头像预览\"],\"l+GxCv\":[\"正在加载频道...\"],\"l+IUVW\":[\"账户 \",[\"account\"],\" 验证成功:\",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"other\":[\"已重新连接 \",[\"reconnectCount\"],\" 次\"]}]],\"l1l8sj\":[[\"0\"],\" 天前\"],\"l5NhnV\":[\"#频道(可选)\"],\"l5jmzx\":[[\"0\"],\" 和 \",[\"1\"],\" 正在输入...\"],\"lCF0wC\":[\"刷新\"],\"lHy8N5\":[\"正在加载更多频道...\"],\"lasgrr\":[\"已使用\"],\"lbpf14\":[\"加入 \",[\"value\"]],\"lfFsZ4\":[\"频道\"],\"lkNdiH\":[\"账户名称\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"上传图片\"],\"loQxaJ\":[\"我回来了\"],\"lvfaxv\":[\"主页\"],\"m16xKo\":[\"添加\"],\"m8flAk\":[\"预览(尚未上传)\"],\"mEPxTp\":[\"<0>⚠️ 请注意!0> 仅打开来自可信来源的链接。恶意链接可能危害您的安全或隐私。\"],\"mHGdhG\":[\"服务器信息\"],\"mHS8lb\":[\"发消息到 #\",[\"0\"]],\"mMYBD9\":[\"宽泛 – 更广的保护范围\"],\"mTGsPd\":[\"频道主题\"],\"mU8j6O\":[\"禁止外部消息 (+n)\"],\"mZp8FL\":[\"自动回退到单行\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"评论 (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"用户已认证\"],\"mwtcGl\":[\"关闭评论\"],\"mzI/c+\":[\"下载\"],\"n3fGRk\":[\"由 \",[\"0\"],\" 设置\"],\"nE9jsU\":[\"宽松 – 较弱的保护\"],\"nNflMD\":[\"离开频道\"],\"nPXkBi\":[\"正在加载 WHOIS 数据...\"],\"nQnxxF\":[\"发消息到 #\",[\"0\"],\"(Shift+Enter 换行)\"],\"nWMRxa\":[\"取消置顶\"],\"nkC032\":[\"无洪水防护配置\"],\"o69z4d\":[\"向 \",[\"username\"],\" 发送警告消息\"],\"o9ylQi\":[\"搜索 GIF 以开始\"],\"oFGkER\":[\"服务器通知\"],\"oOi11l\":[\"滚动到底部\"],\"oPYIL5\":[\"网络\"],\"oQEzQR\":[\"新私信\"],\"oXOSPE\":[\"在线\"],\"oal760\":[\"服务器链路可能遭受中间人攻击\"],\"oeqmmJ\":[\"受信任来源\"],\"optX0N\":[[\"0\"],\" 小时前\"],\"ovBPCi\":[\"默认\"],\"p0Z69r\":[\"模式不能为空\"],\"p1KgtK\":[\"音频加载失败\"],\"p59pEv\":[\"更多详情\"],\"p7sRI6\":[\"让其他人知道您正在输入\"],\"pBm1od\":[\"秘密频道\"],\"pNmiXx\":[\"所有服务器的默认昵称\"],\"pUUo9G\":[\"主机名:\"],\"pVGPmz\":[\"账户密码\"],\"peNE68\":[\"永久\"],\"plhHQt\":[\"无数据\"],\"pm6+q5\":[\"安全警告\"],\"pn5qSs\":[\"附加信息\"],\"q0cR4S\":[\"现在称为 **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"频道不会出现在 LIST 或 NAMES 命令中\"],\"qLpTm/\":[\"移除表情 \",[\"emoji\"]],\"qVkGWK\":[\"置顶\"],\"qY8wNa\":[\"主页\"],\"qb0xJ7\":[\"通配符:* 匹配任意字符,? 匹配单个字符。示例:nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"频道密钥 (+k)\"],\"qtoOYG\":[\"无限制\"],\"r1W2AS\":[\"文件托管图片\"],\"rIPR2O\":[\"主题设置时间早于(分钟前)\"],\"rMMSYo\":[\"最大长度为 \",[\"0\"]],\"rWtzQe\":[\"网络已分裂并重新连接。✅\"],\"rYG2u6\":[\"请稍候...\"],\"rdUucN\":[\"预览\"],\"rjGI/Q\":[\"隐私\"],\"rk8iDX\":[\"正在加载 GIF...\"],\"rn6SBY\":[\"取消静音\"],\"s/UKqq\":[\"已被踢出频道\"],\"s8cATI\":[\"加入了 \",[\"channelName\"]],\"sCO9ue\":[\"与 <0>\",[\"serverName\"],\"0> 的连接存在以下安全问题:\"],\"sGH11W\":[\"服务器\"],\"sHI1H+\":[\"现在称为 **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" 邀请您加入 \",[\"channel\"]],\"sby+1/\":[\"点击复制\"],\"sfN25C\":[\"您的真实姓名或全名\"],\"sliuzR\":[\"打开链接\"],\"sqrO9R\":[\"自定义提及\"],\"sr6RdJ\":[\"Shift+Enter 换行\"],\"swrCpB\":[\"频道已由 \",[\"user\"],\" 从 \",[\"oldName\"],\" 重命名为 \",[\"newName\"],[\"0\"]],\"sxkWRg\":[\"高级\"],\"t/YqKh\":[\"移除\"],\"t47eHD\":[\"您在此服务器上的唯一标识符\"],\"tAkAh0\":[\"可选 \",[\"size\"],\" 替换的 URL。示例:https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"显示或隐藏频道列表侧栏\"],\"tfDRzk\":[\"保存\"],\"tiBsJk\":[\"离开了 \",[\"channelName\"]],\"tt4/UD\":[\"退出了 (\",[\"reason\"],\")\"],\"u0TcnO\":[\"昵称 {nick} 已被使用,正在用 {newNick} 重试\"],\"u0a8B4\":[\"以 IRC 运营商身份进行管理访问认证\"],\"u0rWFU\":[\"创建时间晚于(分钟前)\"],\"u72w3t\":[\"要忽略的用户和模式\"],\"u7jc2L\":[\"退出了\"],\"uAQUqI\":[\"状态\"],\"uB85T3\":[\"保存失败:\",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC 服务器:\"],\"ukyW4o\":[\"您的邀请链接\"],\"usSSr/\":[\"缩放级别\"],\"v7uvcf\":[\"软件:\"],\"vE8kb+\":[\"Shift+Enter 换行(Enter 发送)\"],\"vERlcd\":[\"个人资料\"],\"vK0RL8\":[\"无主题\"],\"vSJd18\":[\"视频\"],\"vXIe7J\":[\"语言\"],\"vaHYxN\":[\"真实姓名\"],\"vhjbKr\":[\"离开\"],\"w4NYox\":[[\"title\"],\" 客户端\"],\"w8xQRx\":[\"值无效\"],\"wFjjxZ\":[\"被 \",[\"username\"],\" 踢出 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"未找到封禁例外\"],\"wPrGnM\":[\"频道管理员\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"显示用户加入或离开频道的事件\"],\"whqZ9r\":[\"要高亮的额外词语或短语\"],\"wm7RV4\":[\"通知声音\"],\"wz/Yoq\":[\"您的消息在服务器之间转发时可能被截获\"],\"x3+y8b\":[\"通过此链接注册的人数\"],\"xCJdfg\":[\"清除\"],\"xOTzt5\":[\"刚刚\"],\"xUHRTR\":[\"连接时自动以操作员身份认证\"],\"xWHwwQ\":[\"封禁\"],\"xYilR2\":[\"媒体\"],\"xbi8D6\":[\"此服务器不支持邀请链接(未声明 <0>obby.world/invitation0> 能力)。您仍可正常聊天;此面板适用于由 obbyircd 提供支持的网络。\"],\"xceQrO\":[\"仅支持安全的 WebSocket 连接\"],\"xdtXa+\":[\"频道名称\"],\"xfXC7q\":[\"文字频道\"],\"xlCYOE\":[\"正在加载更多消息...\"],\"xlhswE\":[\"最小值为 \",[\"0\"]],\"xq97Ci\":[\"添加词语或短语...\"],\"xuRqRq\":[\"客户端限制 (+l)\"],\"xwF+7J\":[[\"0\"],\" 正在输入...\"],\"y1eoq1\":[\"复制链接\"],\"yNeucF\":[\"此服务器不支持扩展个人资料元数据(IRCv3 METADATA 扩展)。头像、显示名称和状态等字段不可用。\"],\"yPlrca\":[\"频道头像\"],\"yQE2r9\":[\"加载中\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Oper 用户名\"],\"yYOzWD\":[\"日志\"],\"yfx9Re\":[\"IRC 运营商密码\"],\"ygCKqB\":[\"停止\"],\"ymDxJx\":[\"IRC 运营商用户名\"],\"yrpRsQ\":[\"按名称排序\"],\"yz7wBu\":[\"关闭\"],\"zJw+jA\":[\"设置模式:\",[\"0\"]],\"zbymaY\":[[\"0\"],\" 分钟前\"],\"zebeLu\":[\"输入 oper 用户名\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
+/*eslint-disable*/export const messages=JSON.parse("{\"+5VMrz\":[\"无效的模式格式,请使用 nick!user@host 格式(允许通配符 *)\"],\"+6NQQA\":[\"通用支持频道\"],\"+6NyRG\":[\"客户端\"],\"+K0AvT\":[\"断开连接\"],\"+cyFdH\":[\"标记为离开时的默认消息\"],\"+mVPqU\":[\"在消息中渲染 Markdown 格式\"],\"+vqCJH\":[\"您的账户认证用户名\"],\"+yPBXI\":[\"选择文件\"],\"+zy2Nq\":[\"类型\"],\"/09cao\":[\"链路安全级别较低(级别 \",[\"securityLevel\"],\")\"],\"/3BQ4J\":[\"频道外的用户无法发送消息\"],\"/4C8U0\":[\"å
¨é¨å¤å¶\"],\"/6BzZF\":[\"切换成员列表\"],\"/AkXyp\":[\"确认ï¼\"],\"/TNOPk\":[\"用户已离开\"],\"/XQgft\":[\"发现\"],\"/cF7Rs\":[\"音量\"],\"/dqduX\":[\"下一页\"],\"/fc3q4\":[\"所有内容\"],\"/kISDh\":[\"启用通知声音\"],\"/n04sB\":[\"踢出\"],\"/rTz0M\":[\"音频\"],\"/rfkZe\":[\"提及和消息时播放声音\"],\"0/0ZGA\":[\"频道名称掩码\"],\"0D6j7U\":[\"了解更多自定义规则 →\"],\"0XsHcR\":[\"踢出用户\"],\"0ZpE//\":[\"按用户数排序\"],\"0bEPwz\":[\"设置为离开\"],\"0dGkPt\":[\"展开频道列表\"],\"0gS7M5\":[\"显示名称\"],\"0kS+M8\":[\"示例网络\"],\"0rgoY7\":[\"仅连接您选择的服务器\"],\"0wdd7X\":[\"加入\"],\"0wkVYx\":[\"私信\"],\"111uHX\":[\"链接预览\"],\"196EG4\":[\"删除私聊\"],\"1DSr1i\":[\"注册账户\"],\"1O/24y\":[\"切换频道列表\"],\"1TNIig\":[\"打开\"],\"1VPJJ2\":[\"外部链接警告\"],\"1ZC/dv\":[\"没有未读提及或消息\"],\"1pO1zi\":[\"服务器名称为必填项\"],\"1uwfzQ\":[\"查看频道主题\"],\"268g7c\":[\"输入显示名称\"],\"2CEOW6\":[\"通过 soju bouncer 绑定的网络\"],\"2F9+AZ\":[\"å°æªæè·ä»»ä½åå§ IRC æµéã请å°è¯è¿æ¥æåé䏿¡æ¶æ¯ã\"],\"2FOFq1\":[\"网络上的服务器管理员可能读取您的消息\"],\"2FYpfJ\":[\"更多\"],\"2HF1Y2\":[[\"inviter\"],\" 邀请了 \",[\"target\"],\" 加入 \",[\"channel\"]],\"2I70QL\":[\"查看用户资料信息\"],\"2QYdmE\":[\"用户:\"],\"2QpEjG\":[\"已离开\"],\"2YE223\":[\"发消息到 #\",[\"0\"],\"(Enter 换行,Shift+Enter 发送)\"],\"2bimFY\":[\"使用服务器密码\"],\"2iTmdZ\":[\"本地存储:\"],\"2odkwe\":[\"严格 – 更强的保护\"],\"2uDhbA\":[\"输入要邀请的用户名\"],\"2ygf/L\":[\"← 返回\"],\"2zEgxj\":[\"搜索 GIF...\"],\"3RdPhl\":[\"重命名频道\"],\"3THokf\":[\"有发言权的用户\"],\"3TSz9S\":[\"最小化\"],\"3jBDvM\":[\"频道显示名称\"],\"3ryuFU\":[\"可选的崩溃报告以改善应用\"],\"3uBF/8\":[\"关闭查看器\"],\"3uwW8F\":[\"https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"3xf8Kz\":[\"输入账户名称...\"],\"4/Rr0R\":[\"邀请用户加入当前频道\"],\"4EZrJN\":[\"规则\"],\"4JJtW9\":[\"#overflow\"],\"4NqeT4\":[\"洪水配置文件 (+F)\"],\"4RZQRK\":[\"你在做什么?\"],\"4hfTrB\":[\"昵称\"],\"4n99LO\":[\"已在 \",[\"0\"]],\"4t6vMV\":[\"短消息自动切换到单行\"],\"4vsHmf\":[\"时间(分钟)\"],\"4x/Axu\":[\"您的 bouncer 尚未添加任何网络。添加一个以开始使用。\"],\"5+INAX\":[\"高亮提及您的消息\"],\"5R5Pv/\":[\"Oper 用户名\"],\"678PKt\":[\"网络名称\"],\"6Aih4U\":[\"离线\"],\"6CO3WE\":[\"加入频道需要密码,留空可移除密钥。\"],\"6HhMs3\":[\"退出消息\"],\"6V3Ea3\":[\"已复制\"],\"6lGV3K\":[\"收起\"],\"6yFOEi\":[\"输入 oper 密码...\"],\"7+IHTZ\":[\"未选择文件\"],\"73fnil\":[\"TLS\"],\"73hrRi\":[\"nick!user@host(例如:spam*!*@*, *!*@badhost.com)\"],\"7QkKyN\":[\"发送私信\"],\"7U1W7c\":[\"非常宽松\"],\"7Y1YQj\":[\"真实姓名:\"],\"7YHArF\":[\"— 在查看器中打开\"],\"7fjnVl\":[\"搜索用户...\"],\"7jL88x\":[\"删除此消息?此操作无法撤销。\"],\"7nGhhM\":[\"您在想什么?\"],\"7sEpu1\":[\"成员 — \",[\"0\"]],\"7sNhEz\":[\"用户名\"],\"8H0Q+x\":[\"了解更多配置文件 →\"],\"8Phu0A\":[\"显示用户更改昵称的事件\"],\"8XTG9e\":[\"输入 oper 密码\"],\"8XsV2J\":[\"重新发送\"],\"8ZsakT\":[\"密码\"],\"8kR84m\":[\"您即将打开一个外部链接:\"],\"8lCgih\":[\"删除规则\"],\"8o3dPc\":[\"ææ¾æä»¶ä»¥ä¸ä¼\xA0\"],\"8p/xVT\":[[\"0\",\"plural\",{\"one\":[[\"1\"]],\"other\":[[\"2\"]]}]],\"8wRzac\":[[\"joinCount\",\"plural\",{\"other\":[\"已加入 \",[\"joinCount\"],\" 次\"]}]],\"9BMLnJ\":[\"重新连接服务器\"],\"9OEgyT\":[\"添加表情\"],\"9PQ8m2\":[\"G-Line(全局封禁)\"],\"9Qs99X\":[\"邮箱:\"],\"9QupBP\":[\"删除规则\"],\"9W7tl5\":[\"(未更改)\"],\"9bG48P\":[\"发送中\"],\"9f5f0u\":[\"有隐私问题?联系我们:\"],\"9iweoP\":[[\"0\"],\" 上的网络\"],\"9unqs3\":[\"离开:\"],\"9v3hwv\":[\"未找到服务器。\"],\"9zb2WA\":[\"连接中\"],\"A1taO8\":[\"搜索\"],\"A2adVi\":[\"发送正在输入通知\"],\"A9Rhec\":[\"频道名称\"],\"AWOSPo\":[\"放大\"],\"AXSpEQ\":[\"连接时获取 Oper\"],\"AdKRCX\":[\"已连接到 <0>\",[\"0\"],\"0>。\"],\"AeXO77\":[\"账户\"],\"AhNP40\":[\"跳转\"],\"Ai2U7L\":[\"主机\"],\"AjBQnf\":[\"已更改昵称\"],\"AmXVh6\":[\"https://example.com/avatar.png\"],\"AnRu/j\":[\"取消回复\"],\"ApSx0O\":[\"找到 \",[\"0\"],\" 条匹配\\\"\",[\"searchQuery\"],\"\\\"的消息\"],\"AxPAXW\":[\"未找到结果\"],\"AyNqAB\":[\"在聊天中显示所有服务器事件\"],\"B/QqGw\":[\"暂时离开\"],\"B0sB2k\":[\"明文\"],\"B8AaMI\":[\"此字段为必填项\"],\"BA2c49\":[\"服务器不支持高级 LIST 筛选\"],\"BDKt3I\":[[\"0\"],\"、\",[\"1\"],\"、\",[\"2\"],\" 以及另外 \",[\"3\"],\" 人正在输入...\"],\"BGul2A\":[\"您有未保存的更改。确定要关闭而不保存吗?\"],\"BIf9fi\":[\"您的状态消息\"],\"BOJWfb\":[\"断开与 soju bouncer 的连接?\"],\"BPm98R\":[\"æªéæ©æå¡å¨ã请å
ä»ä¾§è¾¹æ\xA0éæ©ä¸ä¸ªæå¡å¨ï¼éè¯·é¾æ¥ææå¡å¨åå«ç®¡çã\"],\"BZz3md\":[\"您的个人网站\"],\"Bgm/H7\":[\"允许输入多行文本\"],\"BiQIl1\":[\"置顶此私信对话\"],\"BlNZZ2\":[\"点击跳转到消息\"],\"Bowq3c\":[\"只有管理员可以更改频道主题\"],\"Btozzp\":[\"此图片已过期\"],\"Bycfjm\":[\"总计:\",[\"0\"]],\"C6IBQc\":[\"复制完整 JSON\"],\"C9L9wL\":[\"数据收集\"],\"CDq4wC\":[\"管理用户\"],\"CHVRxG\":[\"发消息给 @\",[\"0\"],\"(Shift+Enter 换行)\"],\"CN9zdR\":[\"Oper 用户名和密码为必填项\"],\"CW3sYa\":[\"添加表情 \",[\"emoji\"]],\"CaAkqd\":[\"显示退出事件\"],\"CbvaYj\":[\"按昵称封禁\"],\"CcK+Ft\":[\"PDF\"],\"Ce8q3L\":[\"选择频道\"],\"CsekCi\":[\"普通\"],\"D+NlUC\":[\"系统\"],\"D28t6+\":[\"已加入并退出\"],\"DB8zMK\":[\"应用\"],\"DBcWHr\":[\"自定义通知音频文件\"],\"DTy9Xw\":[\"媒体预览\"],\"Dj4pSr\":[\"选择一个安全密码\"],\"Du+zn+\":[\"搜索中...\"],\"Du2T2f\":[\"未找到设置\"],\"DwsSVQ\":[\"应用筛选并刷新\"],\"E3W/zd\":[\"默认昵称\"],\"E6nRW7\":[\"复制链接\"],\"E703RG\":[\"模式:\"],\"EAeu1Z\":[\"发送邀请\"],\"EFKJQT\":[\"设置\"],\"EGPQBv\":[\"自定义洪水规则 (+f)\"],\"ELik0r\":[\"查看完整隐私政策\"],\"EPbeC2\":[\"查看或编辑频道主题\"],\"EQCDNT\":[\"输入 oper 用户名...\"],\"EUvulZ\":[\"找到 1 条匹配\\\"\",[\"searchQuery\"],\"\\\"的消息\"],\"EatZYJ\":[\"下一张图片\"],\"EdQY6l\":[\"无\"],\"EnqLYU\":[\"搜索服务器...\"],\"F0OKMc\":[\"编辑服务器\"],\"F6Int2\":[\"启用高亮\"],\"FDoLyE\":[\"最大用户数\"],\"FUU/hZ\":[\"控制聊天中加载的外部媒体数量。\"],\"Fdp03t\":[\"开启\"],\"FfPWR0\":[\"弹窗\"],\"FjkaiT\":[\"缩小\"],\"FlqOE9\":[\"这意味着:\"],\"FolHNl\":[\"管理您的账户和身份验证\"],\"Fp2Dif\":[\"已退出服务器\"],\"G5KmCc\":[\"GZ-Line(全局 Z-Line)\"],\"GDs0lz\":[\"<0>风险:0> 敏感信息(消息、私人对话、身份验证详情)可能会暴露给网络管理员或位于 IRC 服务器之间的攻击者。\"],\"GR+2I3\":[\"添加邀请掩码(例如 nick!*@*, *!*@host.com)\"],\"GRLyMU\":[\"关闭弹出的服务器通知\"],\"GdhD7H\":[\"忬¡ç¹å»ä»¥ç¡®è®¤\"],\"GjRZex\":[\"断开网络连接?\"],\"GlHnXw\":[\"昵称修改失败: \",[\"error\"],\" \",[\"0\"]],\"GswZF3\":[\"预览:\"],\"GtmO8/\":[\"来自\"],\"GtuHUQ\":[\"在服务器上重命名此频道,所有用户都会看到新名称。\"],\"GuGfFX\":[\"切换搜索\"],\"GxkJXS\":[\"上传中...\"],\"GzbwnK\":[\"已加入频道\"],\"GzsUDB\":[\"扩展资料\"],\"H/PnT8\":[\"插入表情\"],\"H6Izzl\":[\"您的首选颜色代码\"],\"H9jIv+\":[\"显示加入/离开\"],\"HAKBY9\":[\"上传文件\"],\"HdE1If\":[\"频道\"],\"Hk4AW9\":[\"您的首选显示名称\"],\"HmHDk7\":[\"选择成员\"],\"HrQzPU\":[[\"networkName\"],\" 上的频道\"],\"I2tXQ5\":[\"发消息给 @\",[\"0\"],\"(Enter 换行,Shift+Enter 发送)\"],\"I6bw/h\":[\"封禁用户\"],\"I92Z+b\":[\"启用通知\"],\"I9D72S\":[\"您确定要删除此消息吗?此操作无法撤销。\"],\"IA+1wo\":[\"显示用户被踢出频道的事件\"],\"IDwkJx\":[\"IRC Operator\"],\"ILlU+s\":[\"信息:\"],\"IUwGEM\":[\"保存更改\"],\"IVeGK6\":[[\"0\"],\"、\",[\"1\"],\" 和 \",[\"2\"],\" 正在输入...\"],\"IgrLD/\":[\"暂停\"],\"Im6JED\":[\"WHISPER\"],\"ImOQa9\":[\"回复\"],\"IoHMnl\":[\"最大值为 \",[\"0\"]],\"IvMj+0\":[\"管理员\"],\"J28zul\":[\"正在连接...\"],\"J5T9NW\":[\"用户信息\"],\"J8Y5+z\":[\"哎呀!网络分裂!⚠️\"],\"JBHkBA\":[\"已离开频道\"],\"JCwL0Q\":[\"输入原因(可选)\"],\"JFciKP\":[\"切换\"],\"JXGkhG\":[\"更改频道名称(仅限管理员)\"],\"JcD7qf\":[\"更多操作\"],\"JdkA+c\":[\"隐秘 (+s)\"],\"Jmu12l\":[\"服务器频道\"],\"JoQY+E\":[\"这会将网络从您的 soju bouncer 中移除。若要再次使用,您需要重新添加。\"],\"JvQ++s\":[\"启用 Markdown\"],\"K2jwh/\":[\"无可用 WHOIS 数据\"],\"KAXSwC\":[\"语音权限\"],\"KDfTdX\":[\"删除消息\"],\"KKBlUU\":[\"嵌入\"],\"KM0pLb\":[\"欢迎来到本频道!\"],\"KR6W2h\":[\"取消忽略用户\"],\"KV+Bi1\":[\"仅限邀请 (+i)\"],\"KdCtwE\":[\"在重置计数器之前监控洪水活动的秒数\"],\"Kkezga\":[\"服务器密码\"],\"KsiQ/8\":[\"用户必须受邀才能加入频道\"],\"L+gB/D\":[\"频道信息\"],\"LC1a7n\":[\"IRC 服务器报告其服务器间链路的安全级别较低。这意味着当您的消息在网络中的 IRC 服务器之间转发时,可能未经过妥善加密,或者 SSL/TLS 证书未被正确验证。\"],\"LEwpeL\":[\"soju bouncer(控制)\"],\"LNfLR5\":[\"显示踢出事件\"],\"LP+1Z7\":[\"添加网络\"],\"LQb0W/\":[\"显示所有事件\"],\"LU7/yA\":[\"显示用的别名,可包含空格、表情和特殊字符。真实频道名(\",[\"channelName\"],\")仍用于 IRC 命令。\"],\"LUb9O7\":[\"需要有效的服务器端口\"],\"LV4fT6\":[\"æè¿°ï¼å¯éï¼ä¾å¦ \\\"第ä¸å£åº¦ Beta æµè¯è
\\\"ï¼\"],\"Lb+BUl\":[\"https://example.com/avatar.jpg\"],\"LcET2C\":[\"隐私政策\"],\"LcuSDR\":[\"管理您的个人资料信息和元数据\"],\"LqLS9B\":[\"显示昵称变更\"],\"LsDQt2\":[\"频道设置\"],\"LtI9AS\":[\"频道所有者\"],\"LuNhhL\":[\"对此消息做出了回应\"],\"M/AZNG\":[\"头像图片 URL\"],\"M/WIer\":[\"发送消息\"],\"M8er/5\":[\"名称:\"],\"MHk+7g\":[\"上一张图片\"],\"MRorGe\":[\"私信用户\"],\"MVbSGP\":[\"时间窗口(秒)\"],\"MkpcsT\":[\"您的消息和设置存储在本地设备上\"],\"MzPdC2\":[\"服务器密码 (PASS)\"],\"N/hDSy\":[\"标记为机器人——通常为 'on' 或空\"],\"N6j2JH\":[\"编辑 \",[\"0\"]],\"N7TQbE\":[\"邀请用户加入 \",[\"channelName\"]],\"NCca/o\":[\"输入默认昵称...\"],\"Nqs6B9\":[\"显示所有外部媒体。任何 URL 都可能向未知服务器发送请求。\"],\"Nt+9O7\":[\"使用 WebSocket 而非原始 TCP\"],\"NxIHzc\":[\"踢出用户\"],\"O+v/cL\":[\"浏览服务器上的所有频道\"],\"OCGpR4\":[\"(继承)\"],\"ODwSCk\":[\"发送 GIF\"],\"OGQ5kK\":[\"配置通知声音和高亮提示\"],\"OIPt1Z\":[\"显示或隐藏成员列表侧栏\"],\"OKSNq/\":[\"非常严格\"],\"ONWvwQ\":[\"上传\"],\"OVKoQO\":[\"您的账户认证密码\"],\"ObsidianIRC - Bringing IRC to the future\":[\"ObsidianIRC - 将 IRC 带入未来\"],\"OhCpra\":[\"设置主题…\"],\"OkltoQ\":[\"通过昵称封禁 \",[\"username\"],\"(阻止其使用相同昵称重新加入)\"],\"P+t/Te\":[\"无其他数据\"],\"P42Wcc\":[\"安全\"],\"PD38l0\":[\"频道头像预览\"],\"PD9mEt\":[\"输入消息...\"],\"PPqfdA\":[\"打开频道配置设置\"],\"PSCjfZ\":[\"此频道显示的主题,所有用户均可查看。\"],\"PZCecv\":[\"PDF 预览\"],\"PeLgsC\":[[\"c\",\"plural\",{\"other\":[[\"c\"],\" 次\"]}]],\"PguS2C\":[\"添加例外掩码(例如 nick!*@*, *!*@host.com)\"],\"Pil5Ty\":[\"显示 \",[\"displayedChannelsCount\"],\",共 \",[\"0\"],\" 个频道\"],\"PqhVlJ\":[\"封禁用户(按 Hostmask)\"],\"Q+chwU\":[\"用户名:\"],\"Q2QY4/\":[\"å\xA0餿¤é请\"],\"Q3v9Wc\":[\"是,删除\"],\"Q6hhn8\":[\"偏好设置\"],\"QF4a34\":[\"请输入用户名\"],\"QGqSZ2\":[\"颜色与格式\"],\"QJQd1J\":[\"编辑资料\"],\"QSzGDE\":[\"闲置\"],\"QUlny5\":[\"欢迎来到 \",[\"0\"],\"!\"],\"Qoq+GP\":[\"阅读更多\"],\"QuSkCF\":[\"筛选频道...\"],\"QwUrDZ\":[\"将话题更改为:\",[\"topic\"]],\"R0UH07\":[\"第 \",[\"0\"],\" 张,共 \",[\"1\"],\" 张\"],\"R7SsBE\":[\"静音\"],\"R8rf1X\":[\"点击设置主题\"],\"RArB3D\":[\"被 \",[\"username\"],\" 踢出 \",[\"channelName\"]],\"RI3cWd\":[\"使用 ObsidianIRC 探索 IRC 的世界\"],\"RIfHS5\":[\"å建æ°çéè¯·é¾æ¥\"],\"RMMaN5\":[\"受管理 (+m)\"],\"RWw9Lg\":[\"关闭窗口\"],\"RZ2BuZ\":[\"账户 \",[\"account\"],\" 注册需要验证:\",[\"message\"]],\"RySp6q\":[\"隐藏评论\"],\"S5Togi\":[\"正在从您的 bouncer 加载网络…\"],\"SPKQTd\":[\"昵称为必填项\"],\"SPVjfj\":[\"留空将默认显示\\\"无原因\\\"\"],\"SQKPvQ\":[\"邀请用户\"],\"STmlpb\":[\"返回网络列表\"],\"SkZcl+\":[\"选择预定义的洪水保护配置文件。这些配置文件为不同使用场景提供均衡的保护设置。\"],\"Slr+3C\":[\"最小用户数\"],\"Spnlre\":[\"您邀请了 \",[\"target\"],\" 加入 \",[\"channel\"]],\"T/ckN5\":[\"在查看器中打开\"],\"T91vKp\":[\"播放\"],\"TV2Wdu\":[\"了解我们如何处理您的数据并保护您的隐私。\"],\"TgFpwD\":[\"正在应用...\"],\"TkzSFB\":[\"无更改\"],\"TtserG\":[\"输入真实姓名\"],\"Ttz9J1\":[\"输入密码...\"],\"Tz0i8g\":[\"设置\"],\"U3pytU\":[\"管理员\"],\"UDb2YD\":[\"添加表情\"],\"UE4KO5\":[\"*channel*\"],\"UETAwW\":[\"æ¨è¿æ²¡æå建任ä½éè¯·é¾æ¥ã使ç¨ä¸é¢çè¡¨åæ¥å建æ¨ç第ä¸ä¸ªã\"],\"UGT5vp\":[\"保存设置\"],\"UV5hLB\":[\"未找到封禁\"],\"Uaj3Nd\":[\"状态消息\"],\"Ue3uny\":[\"默认(无配置文件)\"],\"UkARhe\":[\"普通 – 标准保护\"],\"Umn7Cj\":[\"暂无评论,成为第一个吧!\"],\"UtUIRh\":[[\"0\"],\" 条旧消息\"],\"UwzP+U\":[\"安全连接\"],\"V0/A4O\":[\"频道所有者\"],\"V0zZWc\":[\"这还将关闭下方绑定的网络。\"],\"V4qgxE\":[\"创建时间早于(分钟前)\"],\"V8yTm6\":[\"清除搜索\"],\"VJMMyz\":[\"ObsidianIRC - 将 IRC 带入未来\"],\"VJScHU\":[\"原因\"],\"VLsmVV\":[\"静音通知\"],\"VbyRUy\":[\"评论\"],\"Vmx0mQ\":[\"设置者:\"],\"VqnIZz\":[\"查看我们的隐私政策和数据使用规范\"],\"VrMygG\":[\"最小长度为 \",[\"0\"]],\"VrnTui\":[\"您的代词,显示在个人资料中\"],\"W8E3qn\":[\"已验证账户\"],\"WAakm9\":[\"删除频道\"],\"WFxTHC\":[\"添加封禁掩码(例如 nick!*@*, *!*@host.com)\"],\"WN1g9F\":[\"服务器地址为必填项\"],\"WRYdXW\":[\"音频进度\"],\"WUOH5B\":[\"忽略用户\"],\"WWEXnZ\":[[\"0\",\"plural\",{\"other\":[\"显示另外 \",[\"1\"],\" 个项目\"]}]],\"WYxRzo\":[\"åå»ºå¹¶ç®¡çæ¨çéè¯·é¾æ¥\"],\"Wd38W1\":[\"é¢éç空å¯ç¨äºéç¨ç½ç»é请ãæè¿°ä»
ç¨äºæ¨èªå·±çè®°å½ââåªææ¨è½å¨æ¤å表ä¸çå°ã\"],\"Weq9zb\":[\"常规\"],\"Wfj7Sk\":[\"开启或关闭通知声音\"],\"Wm7gbG\":[\"GitHub:\"],\"WyeHWY\":[\"*spam*\"],\"WzMCru\":[\"用户资料\"],\"X6S3lt\":[\"搜索设置、频道、服务器...\"],\"XEHan5\":[\"仍然继续\"],\"XI1+wb\":[\"格式无效\"],\"XIXeuC\":[\"发消息给 @\",[\"0\"]],\"XMS+k4\":[\"发起私信\"],\"XWgxXq\":[\"相册\"],\"Xd7+IT\":[\"取消置顶私聊\"],\"Xm/s+u\":[\"显示\"],\"Xp2n93\":[\"显示来自服务器受信任文件主机的媒体。不会向外部服务发送任何请求。\"],\"XvjC4F\":[\"正在保存...\"],\"Y/qryO\":[\"未找到与搜索条件匹配的用户\"],\"YAqRpI\":[\"账户 \",[\"account\"],\" 注册成功:\",[\"message\"]],\"YEfzvP\":[\"受保护主题 (+t)\"],\"YQOn6a\":[\"收起成员列表\"],\"YRCoE9\":[\"频道操作员\"],\"YURQaF\":[\"查看资料\"],\"YdBSvr\":[\"控制媒体显示和外部内容\"],\"Yj6U3V\":[\"无中央服务器:\"],\"YjvpGx\":[\"代词\"],\"YqH4l4\":[\"无密钥\"],\"YyUPpV\":[\"账户:\"],\"ZJSWfw\":[\"断开服务器时显示的消息\"],\"ZR1dJ4\":[\"邀请\"],\"ZdWg0V\":[\"在浏览器中打开\"],\"ZhRBbl\":[\"搜索消息…\"],\"Zmcu3y\":[\"高级筛选\"],\"a0bHay\":[\"断开 <0>\",[\"0\"],\"0> 的连接?\"],\"a2/8e5\":[\"主题设置时间晚于(分钟前)\"],\"aHKcKc\":[\"上一页\"],\"aJTbXX\":[\"Oper 密码\"],\"aQryQv\":[\"模式已存在\"],\"aW9pLN\":[\"频道允许的最大用户数,留空表示无限制。\"],\"ah4fmZ\":[\"同时显示来自 YouTube、Vimeo、SoundCloud 及类似知名服务的预览。\"],\"aifXak\":[\"此频道没有媒体\"],\"ap2zBz\":[\"宽松\"],\"az8lvo\":[\"关\"],\"azXSNo\":[\"展开成员列表\"],\"azdliB\":[\"登录账户\"],\"b26wlF\":[\"她/她的\"],\"bD/+Ei\":[\"严格\"],\"bQ6BJn\":[\"配置详细的洪水保护规则。每条规则指定要监控的活动类型以及超过阈值时采取的操作。\"],\"beV7+y\":[\"用户将收到加入 \",[\"channelName\"],\" 的邀请。\"],\"bk84cH\":[\"离开消息\"],\"bkHdLj\":[\"添加 IRC 服务器\"],\"bmQLn5\":[\"添加规则\"],\"bv4cFj\":[\"传输方式\"],\"bwRvnp\":[\"操作\"],\"c8+EVZ\":[\"已验证账户\"],\"cGYUlD\":[\"未加载任何媒体预览。\"],\"cLF98o\":[\"显示评论 (\",[\"commentCount\"],\")\"],\"cLKIDO\":[\"没有可用用户\"],\"cSgpoS\":[\"置顶私聊\"],\"cXeEKu\":[\"这还将关闭下方 \",[\"0\"],\" 个绑定的网络。\"],\"cde3ce\":[\"发消息给 <0>\",[\"0\"],\"0>\"],\"chQsxg\":[\"复制格式化输出\"],\"cl/A5J\":[\"欢迎来到 \",[\"__DEFAULT_IRC_SERVER_NAME__\"],\"!\"],\"cnGeoo\":[\"删除\"],\"coPLXT\":[\"我们不在服务器上存储您的 IRC 通信\"],\"crYH/6\":[\"SoundCloud 播放器\"],\"cv5DQb\":[\"未设置主机\"],\"d3sis4\":[\"添加服务器\"],\"d9aN5k\":[\"将 \",[\"username\"],\" 移出频道\"],\"dEgA5A\":[\"取消\"],\"dGi1We\":[\"取消置顶此私信对话\"],\"dJVuyC\":[\"离开了 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"dMtLDE\":[\"至\"],\"dXqxlh\":[\"<0>⚠️ 安全风险!0> 此连接可能容易遭受窃听或中间人攻击。\"],\"da9Q/R\":[\"已更改频道模式\"],\"dhJN3N\":[\"显示评论\"],\"dj2xTE\":[\"关闭通知\"],\"dpCzmC\":[\"洪水保护设置\"],\"e9dQpT\":[\"是否在新标签页中打开此链接?\"],\"ePK91l\":[\"编辑\"],\"eYBDuB\":[\"上传图片或提供带可选 \",[\"size\"],\" 替换的 URL\"],\"edBbee\":[\"通过 hostmask 封禁 \",[\"username\"],\"(阻止其从相同 IP/主机重新加入)\"],\"ekfzWq\":[\"用户设置\"],\"elPDWs\":[\"自定义您的 IRC 客户端体验\"],\"eu2osY\":[\"<0>💡 建议:0> 仅在您信任此服务器并了解相关风险的情况下继续操作。避免通过此连接共享敏感信息或密码。\"],\"euEhbr\":[\"点击加入 \",[\"channel\"]],\"ez3vLd\":[\"启用多行输入\"],\"f0J5Ki\":[\"服务器之间的通信可能使用未加密的连接\"],\"f9BHJk\":[\"警告用户\"],\"fDOLLd\":[\"未找到频道。\"],\"ffzDkB\":[\"匿名分析:\"],\"fq1GF9\":[\"显示用户断开服务器连接的事件\"],\"gCldcN\":[\"更改强调色\"],\"gEF57C\":[\"此服务器仅支持一种连接类型\"],\"gJuLUI\":[\"忽略列表\"],\"gNzMrk\":[\"当前头像\"],\"gjPWyO\":[\"输入昵称...\"],\"gz6UQ3\":[\"最大化\"],\"h6/IMX\":[\"添加您的第一个网络\"],\"h6razj\":[\"排除频道名称掩码\"],\"hG6jnw\":[\"未设置主题\"],\"hG89Ed\":[\"图片\"],\"hYgDIe\":[\"å建\"],\"hZ6znB\":[\"端口\"],\"ha+Bz5\":[\"例如:100:1440\"],\"he3ygx\":[\"å¤å¶\"],\"hehnjM\":[\"数量\"],\"hzdLuQ\":[\"只有获得发言权或更高权限的用户才能发言\"],\"i0qMbr\":[\"主页\"],\"iDNBZe\":[\"通知\"],\"iH8pgl\":[\"返回\"],\"iL9SZg\":[\"封禁用户(按昵称)\"],\"iNt+3c\":[\"返回图片\"],\"iQvi+a\":[\"不再提醒我此服务器的低链路安全问题\"],\"iSLIjg\":[\"连接\"],\"iWXkHH\":[\"半管理员\"],\"iZeTtp\":[\"服务器地址\"],\"idD8Ev\":[\"已保存\"],\"iivqkW\":[\"登录时间\"],\"ij+Elv\":[\"图片预览\"],\"ilIWp7\":[\"切换通知\"],\"iuaqvB\":[\"用 * 作通配符。示例:baduser!*@*, *!*@spammer.com, troll*!*@*\"],\"ixkTse\":[\"机器人\"],\"j2DGR0\":[\"按主机掩码封禁\"],\"jA4uoI\":[\"话题:\"],\"jLXxGK\":[\"https://example.com\"],\"jPSk57\":[\"原因(可选)\"],\"jUV7CU\":[\"上传头像\"],\"jW5Uwh\":[\"控制加载的外部媒体量。关闭 / 安全 / 可信来源 / 所有内容。\"],\"jXzms5\":[\"附件选项\"],\"jZlrte\":[\"颜色\"],\"jfC/xh\":[\"联系方式\"],\"jywMpv\":[\"#new-channel-name\"],\"k112DD\":[\"加载旧消息\"],\"k3ID0F\":[\"筛选成员…\"],\"k65gsE\":[\"深入查看\"],\"k7Zgob\":[\"取消连接\"],\"kAVx5h\":[\"未找到邀请\"],\"kCLEPU\":[\"已连接到\"],\"kF5LKb\":[\"已忽略的模式:\"],\"kGeOx/\":[\"加入 \",[\"0\"]],\"kITKr8\":[\"正在加载频道模式...\"],\"kPpPsw\":[\"您是 IRC Operator\"],\"kWJmRL\":[\"您\"],\"kfcRb0\":[\"头像\"],\"kjMqSj\":[\"复制 JSON\"],\"krViRy\":[\"点击以 JSON 格式复制\"],\"ks71ra\":[\"例外\"],\"kw4lRv\":[\"频道半操作员\"],\"kxgIRq\":[\"选择或添加频道以开始使用。\"],\"ky6dWe\":[\"头像预览\"],\"l+GxCv\":[\"正在加载频道...\"],\"l+IUVW\":[\"账户 \",[\"account\"],\" 验证成功:\",[\"message\"]],\"l/siQz\":[[\"reconnectCount\",\"plural\",{\"other\":[\"已重新连接 \",[\"reconnectCount\"],\" 次\"]}]],\"l1l8sj\":[[\"0\"],\" 天å\"],\"l5NhnV\":[\"#é¢éï¼å¯éï¼\"],\"l5jmzx\":[[\"0\"],\" 和 \",[\"1\"],\" 正在输入...\"],\"lCF0wC\":[\"å·æ°\"],\"lHy8N5\":[\"正在加载更多频道...\"],\"lasgrr\":[\"已使ç¨\"],\"lbpf14\":[\"加入 \",[\"value\"]],\"lfFsZ4\":[\"频道\"],\"lkNdiH\":[\"账户名称\"],\"ln500L\":[\"ObsidianIRC\"],\"lnCMdg\":[\"上传图片\"],\"loQxaJ\":[\"我回来了\"],\"lvfaxv\":[\"主页\"],\"m0oxpP\":[\"Libera Chat\"],\"m16xKo\":[\"添加\"],\"m8flAk\":[\"预览(尚未上传)\"],\"mEPxTp\":[\"<0>⚠️ 请注意!0> 仅打开来自可信来源的链接。恶意链接可能危害您的安全或隐私。\"],\"mHGdhG\":[\"服务器信息\"],\"mHS8lb\":[\"发消息到 #\",[\"0\"]],\"mMYBD9\":[\"宽泛 – 更广的保护范围\"],\"mTGsPd\":[\"频道主题\"],\"mU8j6O\":[\"禁止外部消息 (+n)\"],\"mZp8FL\":[\"自动回退到单行\"],\"mdQu8G\":[\"YourNickname\"],\"miSSBQ\":[\"评论 (\",[\"commentCount\"],\")\"],\"mvyLSy\":[\"用户已认证\"],\"mwtcGl\":[\"关闭评论\"],\"myL0MR\":[\"删除此网络?\"],\"mzI/c+\":[\"下载\"],\"n3fGRk\":[\"由 \",[\"0\"],\" 设置\"],\"n5+j9l\":[\"例如 <0>wss://host:port/socket0>\"],\"nE9jsU\":[\"宽松 – 较弱的保护\"],\"nNflMD\":[\"离开频道\"],\"nPXkBi\":[\"正在加载 WHOIS 数据...\"],\"nQnxxF\":[\"发消息到 #\",[\"0\"],\"(Shift+Enter 换行)\"],\"nWMRxa\":[\"取消置顶\"],\"nkC032\":[\"无洪水防护配置\"],\"o69z4d\":[\"向 \",[\"username\"],\" 发送警告消息\"],\"o9ylQi\":[\"搜索 GIF 以开始\"],\"oFGkER\":[\"服务器通知\"],\"oOi11l\":[\"滚动到底部\"],\"oPYIL5\":[\"ç½ç»\"],\"oQEzQR\":[\"新私信\"],\"oXOSPE\":[\"在线\"],\"oal760\":[\"服务器链路可能遭受中间人攻击\"],\"oeqmmJ\":[\"受信任来源\"],\"optX0N\":[[\"0\"],\" å°æ¶å\"],\"ovBPCi\":[\"默认\"],\"p0Z69r\":[\"模式不能为空\"],\"p1KgtK\":[\"音频加载失败\"],\"p59pEv\":[\"更多详情\"],\"p7sRI6\":[\"让其他人知道您正在输入\"],\"pBm1od\":[\"秘密频道\"],\"pNmiXx\":[\"所有服务器的默认昵称\"],\"pUUo9G\":[\"主机名:\"],\"pVGPmz\":[\"账户密码\"],\"peNE68\":[\"永久\"],\"plhHQt\":[\"无数据\"],\"pm6+q5\":[\"安全警告\"],\"pn5qSs\":[\"附加信息\"],\"q0cR4S\":[\"现在称为 **\",[\"newNick\"],\"**\"],\"qFcunY\":[\"频道不会出现在 LIST 或 NAMES 命令中\"],\"qLpTm/\":[\"移除表情 \",[\"emoji\"]],\"qVkGWK\":[\"置顶\"],\"qY8wNa\":[\"主页\"],\"qb0xJ7\":[\"通配符:* 匹配任意字符,? 匹配单个字符。示例:nick!*@*, *!*@host.com, *!*user@*\"],\"qhzpRq\":[\"频道密钥 (+k)\"],\"qtoOYG\":[\"无限制\"],\"r1W2AS\":[\"文件托管图片\"],\"rIPR2O\":[\"主题设置时间早于(分钟前)\"],\"rMMSYo\":[\"最大长度为 \",[\"0\"]],\"rWtzQe\":[\"网络已分裂并重新连接。✅\"],\"rYG2u6\":[\"请稍候...\"],\"rdUucN\":[\"预览\"],\"rjGI/Q\":[\"隐私\"],\"rk8iDX\":[\"正在加载 GIF...\"],\"rn6SBY\":[\"取消静音\"],\"s/UKqq\":[\"已被踢出频道\"],\"s7oqXR\":[\"选择网络\"],\"s8cATI\":[\"加入了 \",[\"channelName\"]],\"sCO9ue\":[\"与 <0>\",[\"serverName\"],\"0> 的连接存在以下安全问题:\"],\"sGH11W\":[\"服务器\"],\"sHI1H+\":[\"现在称为 **\",[\"newNick\"],\"**\"],\"sJyV04\":[[\"inviter\"],\" 邀请您加入 \",[\"channel\"]],\"sUBSbK\":[\"尚无上游网络。\"],\"sby+1/\":[\"点击复制\"],\"sfN25C\":[\"您的真实姓名或全名\"],\"sliuzR\":[\"打开链接\"],\"sqrO9R\":[\"自定义提及\"],\"sr6RdJ\":[\"Shift+Enter 换行\"],\"swrCpB\":[\"频道已由 \",[\"user\"],\" 从 \",[\"oldName\"],\" 重命名为 \",[\"newName\"],[\"0\"]],\"sxkWRg\":[\"高级\"],\"t/YqKh\":[\"移除\"],\"t47eHD\":[\"您在此服务器上的唯一标识符\"],\"tAkAh0\":[\"可选 \",[\"size\"],\" 替换的 URL。示例:https://example.com/avatar/\",[\"size\"],\"/channel.jpg\"],\"tXLJS3\":[\"显示或隐藏频道列表侧栏\"],\"tfDRzk\":[\"保存\"],\"tiBsJk\":[\"离开了 \",[\"channelName\"]],\"tt4/UD\":[\"退出了 (\",[\"reason\"],\")\"],\"u0TcnO\":[\"昵称 {nick} 已被使用,正在用 {newNick} 重试\"],\"u0a8B4\":[\"以 IRC 运营商身份进行管理访问认证\"],\"u0rWFU\":[\"创建时间晚于(分钟前)\"],\"u72w3t\":[\"要忽略的用户和模式\"],\"u7jc2L\":[\"退出了\"],\"uAQUqI\":[\"状态\"],\"uB85T3\":[\"保存失败:\",[\"msg\"]],\"uV3DOL\":[\"G-Line\"],\"uW3lLI\":[\"IRC 服务器:\"],\"ukyW4o\":[\"æ¨çéè¯·é¾æ¥\"],\"usSSr/\":[\"缩放级别\"],\"v7uvcf\":[\"软件:\"],\"vE8kb+\":[\"Shift+Enter 换行(Enter 发送)\"],\"vERlcd\":[\"个人资料\"],\"vK0RL8\":[\"无主题\"],\"vSJd18\":[\"视频\"],\"vXIe7J\":[\"语言\"],\"vaHYxN\":[\"真实姓名\"],\"vhjbKr\":[\"离开\"],\"w/nogd\":[[\"0\"],\" 个网络\",[\"1\"],\" — 选择一个加入\"],\"w4NYox\":[[\"title\"],\" 客户端\"],\"w8xQRx\":[\"值无效\"],\"wFjjxZ\":[\"被 \",[\"username\"],\" 踢出 \",[\"channelName\"],\" (\",[\"reason\"],\")\"],\"wGjaGl\":[\"未找到封禁例外\"],\"wPrGnM\":[\"频道管理员\"],\"wRkP2d\":[\"GIF\"],\"wbm86v\":[\"显示用户加入或离开频道的事件\"],\"whqZ9r\":[\"要高亮的额外词语或短语\"],\"wm7RV4\":[\"通知声音\"],\"wz/Yoq\":[\"您的消息在服务器之间转发时可能被截获\"],\"x3+y8b\":[\"éè¿æ¤é¾æ¥æ³¨åç人æ°\"],\"xCJdfg\":[\"清除\"],\"xOTzt5\":[\"åå\"],\"xUHRTR\":[\"连接时自动以操作员身份认证\"],\"xWHwwQ\":[\"封禁\"],\"xYilR2\":[\"媒体\"],\"xbi8D6\":[\"æ¤æå¡å¨ä¸æ¯æéè¯·é¾æ¥ï¼æªå£°æ <0>obby.world/invitation0> è½åï¼ãæ¨ä»å¯æ£å¸¸èå¤©ï¼æ¤é¢æ¿éç¨äºç± obbyircd æä¾æ¯æçç½ç»ã\"],\"xceQrO\":[\"仅支持安全的 WebSocket 连接\"],\"xdtXa+\":[\"频道名称\"],\"xfXC7q\":[\"文字频道\"],\"xlCYOE\":[\"正在加载更多消息...\"],\"xlhswE\":[\"最小值为 \",[\"0\"]],\"xq97Ci\":[\"添加词语或短语...\"],\"xuRqRq\":[\"客户端限制 (+l)\"],\"xwF+7J\":[[\"0\"],\" 正在输入...\"],\"y1eoq1\":[\"å¤å¶é¾æ¥\"],\"yJztBY\":[\"删除网络\"],\"yNeucF\":[\"此服务器不支持扩展个人资料元数据(IRCv3 METADATA 扩展)。头像、显示名称和状态等字段不可用。\"],\"yPlrca\":[\"频道头像\"],\"yQE2r9\":[\"加载中\"],\"ySU+JY\":[\"your@email.com\"],\"yTX1Rt\":[\"Oper 用户名\"],\"yYOzWD\":[\"日志\"],\"yfx9Re\":[\"IRC 运营商密码\"],\"ygCKqB\":[\"停止\"],\"ymDxJx\":[\"IRC 运营商用户名\"],\"yrpRsQ\":[\"按名称排序\"],\"yz7wBu\":[\"关闭\"],\"zJw+jA\":[\"设置模式:\",[\"0\"]],\"zbymaY\":[[\"0\"],\" åéå\"],\"zebeLu\":[\"输入 oper 用户名\"],\"zpr0Bw\":[\"GZ-Line\"]}");
\ No newline at end of file
diff --git a/src/locales/zh/messages.po b/src/locales/zh/messages.po
index 84acb396..a04b4f63 100644
--- a/src/locales/zh/messages.po
+++ b/src/locales/zh/messages.po
@@ -22,6 +22,16 @@ msgstr "ObsidianIRC - 将 IRC 带入未来"
msgid "— open in viewer"
msgstr "— 在查看器中打开"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(inherit)"
+msgstr "(继承)"
+
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "(unchanged)"
+msgstr "(未更改)"
+
#. placeholder {0}: filteredMessages.length
#. placeholder {1}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
#. placeholder {2}: import { plural } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; import type * as React from "react"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { SCROLL_TOLERANCE, useScrollToBottom, } from "../../hooks/useScrollToBottom"; import { groupConsecutiveEvents } from "../../lib/eventGrouping"; import ircClient from "../../lib/ircClient"; import useStore from "../../store"; import type { Message as MessageType } from "../../types"; import { CollapsedEventMessage } from "../message/CollapsedEventMessage"; import { MessageItem } from "../message/MessageItem"; import LoadingSpinner from "../ui/LoadingSpinner"; import { ScrollToBottomButton } from "../ui/ScrollToBottomButton"; export const DEFAULT_VISIBLE_MESSAGE_COUNT = 100; // Stable empty array — prevents selector from returning a new [] on every render // when the channel has no messages yet (undefined ?? [] would create a new ref each time). const EMPTY_MESSAGES: import("../../types").Message[] = []; export interface ChannelMessageListHandle { setAtBottom: () => void; scrollToBottom: () => void; getScrollState: () => { scrollTop: number; isAtBottom: boolean; visibleCount: number; }; } interface ChannelMessageListProps { channelKey: string; serverId: string; channelId: string | null; privateChatId: string | null; isActive: boolean; searchQuery: string; isMemberListVisible: boolean; onReply: (msg: MessageType | null) => void; onUsernameContextMenu: ( e: React.MouseEvent, username: string, serverId: string, channelId: string, avatarEl?: Element | null, ) => void; onIrcLinkClick: (url: string) => void; onReactClick: (msg: MessageType, el: Element) => void; onReactionUnreact: (emoji: string, msg: MessageType) => void; onOpenReactionModal: ( msg: MessageType, position: { x: number; y: number }, ) => void; onDirectReaction: (emoji: string, msg: MessageType) => void; onRedactMessage: (msg: MessageType) => void; onOpenProfile: (username: string) => void; joinChannel: (serverId: string, channelName: string) => void; onClearSearch: () => void; highlightedMessageId?: string; // undefined = first visit; null = was at bottom; object = restore to saved position initialScrollState?: { scrollTop: number; visibleCount: number } | null; } export const ChannelMessageList = forwardRef< ChannelMessageListHandle, ChannelMessageListProps >( ( { channelKey, serverId, channelId, privateChatId, isActive, searchQuery, isMemberListVisible, onReply, onUsernameContextMenu, onIrcLinkClick, onReactClick, onReactionUnreact, onOpenReactionModal, onDirectReaction, onRedactMessage, onOpenProfile, joinChannel, onClearSearch, highlightedMessageId, initialScrollState, }, ref, ) => { const { t } = useLingui(); const [visibleMessageCount, setVisibleMessageCount] = useState( initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT, ); // Ref mirror so getScrollState closure always reads the current value without needing it as a dep. const visibleMessageCountRef = useRef(visibleMessageCount); visibleMessageCountRef.current = visibleMessageCount; // Distinguishes initial join (full-screen spinner) from subsequent "load more" (button spinner). const [isFetchingMore, setIsFetchingMore] = useState(false); const isFetchingMoreRef = useRef(false); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messagesInnerRef = useRef(null); // prev scrollHeight for prepend delta-correction. const prevScrollHeightRef = useRef(0); // Ref mirror of isScrolledUp — lets useLayoutEffect closures read current value // without listing isScrolledUp as a dep (which would re-run effects on every scroll). const isScrolledUpRef = useRef(false); const prevFilteredLengthRef = useRef(0); const prevFirstMsgIdRef = useRef(null); // Set by the window-growth layoutEffect (or button handler) when a true prepend is detected. // Consumed by the delta-correction layoutEffect one render later (after visibleCount grows). // Using a flag instead of tracking displayedMessages[0]?.id because slice(-N) slides the // window on every bottom append, changing displayedMessages[0] even for non-prepend renders. const pendingPrependRef = useRef(false); // Shared scrollHeight baseline between the delta-correction layout effect and the inner // ResizeObserver. When scrollTop is corrected after a prepend, we update this so the RO's // "was at bottom" check is not fooled by the adjusted scrollTop vs its stale prevSH. const resizeObserverPrevSHRef = useRef(0); const channelMessages = useStore( useCallback( (state) => state.messages[channelKey] ?? EMPTY_MESSAGES, [channelKey], ), ); const servers = useStore((state) => state.servers); const mobileViewActiveColumn = useStore( (state) => state.ui.mobileViewActiveColumn, ); const channel = useMemo( () => channelId ? (servers .find((s) => s.id === serverId) ?.channels.find((c) => c.id === channelId) ?? null) : null, [servers, serverId, channelId], ); const { isScrolledUp, wasAtBottomRef, scrollToBottom } = useScrollToBottom( messagesContainerRef, messagesEndRef, { channelId: `${channelId || privateChatId}-${isMemberListVisible}` }, ); // Snapshot of the last known scroll position captured while the container was visible. // getScrollState() reads this instead of the live DOM because React commits display:none // before running cleanup effects, collapsing scrollTop/scrollHeight/clientHeight to 0. const lastScrollTopRef = useRef(initialScrollState?.scrollTop ?? 0); useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const onScroll = () => { if (container.clientHeight > 0) lastScrollTopRef.current = container.scrollTop; }; container.addEventListener("scroll", onScroll, { passive: true }); return () => container.removeEventListener("scroll", onScroll); }, []); // Restore scroll position when a keep-alive channel transitions from hidden to visible. // display:none may reset scrollTop to 0; lastScrollTopRef was captured while visible. const prevActiveRef = useRef(isActive); useLayoutEffect(() => { if (isActive && !prevActiveRef.current) { const container = messagesContainerRef.current; if (container && lastScrollTopRef.current > 0) { container.scrollTop = lastScrollTopRef.current; } } prevActiveRef.current = isActive; }, [isActive]); useImperativeHandle(ref, () => ({ setAtBottom: () => { wasAtBottomRef.current = true; }, scrollToBottom, getScrollState: () => ({ scrollTop: lastScrollTopRef.current, isAtBottom: wasAtBottomRef.current, visibleCount: visibleMessageCountRef.current, }), })); const filteredMessages = useMemo(() => { if (!searchQuery.trim()) return channelMessages; const query = searchQuery.toLowerCase(); return channelMessages.filter( (msg) => msg.content.toLowerCase().includes(query) || msg.userId.toLowerCase().includes(query), ); }, [channelMessages, searchQuery]); useEffect(() => { isScrolledUpRef.current = isScrolledUp; // When the user returns to the bottom, shrink the window back to the base so // slice(-N) resumes trimming old messages from the top (memory optimization). // Only shrink if we grew above the base — preserves a sub-default saved visibleCount. if (!isScrolledUp) { setVisibleMessageCount((prev) => prev > DEFAULT_VISIBLE_MESSAGE_COUNT ? DEFAULT_VISIBLE_MESSAGE_COUNT : prev, ); } }, [isScrolledUp]); // Reset ref-tracked windowing state when switching channels. // visibleMessageCount is NOT reset here — useState(initialScrollState?.visibleCount ?? DEFAULT_VISIBLE_MESSAGE_COUNT) // already initializes it correctly on mount, and this effect runs once on mount for the // same channelKey (each instance is bound to exactly one channel by the parent key={}). // biome-ignore lint/correctness/useExhaustiveDependencies: intentional full reset on channel change useEffect(() => { prevFilteredLengthRef.current = 0; prevFirstMsgIdRef.current = null; prevScrollHeightRef.current = 0; pendingPrependRef.current = false; resizeObserverPrevSHRef.current = 0; }, [channelKey]); const displayedMessages = useMemo(() => { if (searchQuery.trim()) return filteredMessages; return filteredMessages.slice(-visibleMessageCount); }, [filteredMessages, visibleMessageCount, searchQuery]); const locallyHidden = filteredMessages.length > displayedMessages.length; const serverHasMore = channel?.hasMoreHistory === true; const hasMoreMessages = locallyHidden || serverHasMore; const eventGroups = useMemo( () => groupConsecutiveEvents(displayedMessages), [displayedMessages], ); const isLoadingHistory = channel?.isLoadingHistory ?? false; // Scroll to bottom on initial mount, unless a saved position was passed in. // biome-ignore lint/correctness/useExhaustiveDependencies: run once on mount only useEffect(() => { const container = messagesContainerRef.current; if (!container) return; if (initialScrollState) { container.scrollTop = initialScrollState.scrollTop; lastScrollTopRef.current = initialScrollState.scrollTop; wasAtBottomRef.current = false; } else { container.scrollTop = container.scrollHeight; lastScrollTopRef.current = container.scrollHeight; wasAtBottomRef.current = true; } }, []); // Scroll to bottom after initial join history loads; clear fetch spinner at batch end. const wasLoadingHistoryRef = useRef(false); // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is stable via useCallback; refs and setters are stable useLayoutEffect(() => { if (wasLoadingHistoryRef.current && !isLoadingHistory) { if (isFetchingMoreRef.current) { // delta correction for scroll position is handled by useLayoutEffect([displayedMessages]) isFetchingMoreRef.current = false; setIsFetchingMore(false); } else { scrollToBottom(); wasAtBottomRef.current = true; } } wasLoadingHistoryRef.current = isLoadingHistory; }, [isLoadingHistory]); // When older messages are prepended, grow the window so they enter displayedMessages. // When new messages arrive at the bottom while the user is scrolled up, also grow the // window to keep the current top messages visible — slice(-N) otherwise slides the // window forward and hides them, incrementing the "N older messages" counter on every // incoming message. Only let the slice trim from the top when the user is at the bottom // (where auto-scroll handles keeping them current). useLayoutEffect(() => { const newLength = filteredMessages.length; const newFirstId = filteredMessages[0]?.id ?? null; const delta = newLength - prevFilteredLengthRef.current; if (prevFilteredLengthRef.current > 0 && delta > 0) { if (newFirstId !== prevFirstMsgIdRef.current) { // Messages prepended (load-more): signal delta-correction to compensate scrollTop. pendingPrependRef.current = true; setVisibleMessageCount((prev) => prev + delta); } else if (isScrolledUpRef.current) { // Messages appended at bottom while user is scrolled up reading history. // Expand the window to prevent top messages from dropping out of the slice. setVisibleMessageCount((prev) => prev + delta); } } prevFilteredLengthRef.current = newLength; prevFirstMsgIdRef.current = newFirstId; }, [filteredMessages]); // Compensate scrollTop when content is prepended above the viewport. // biome-ignore lint/correctness/useExhaustiveDependencies: runs on every displayedMessages render to capture the resulting scrollHeight; refs are stable useLayoutEffect(() => { const container = messagesContainerRef.current; if (!container) return; // Skip while container is display:none — scrollHeight collapses to 0 and would // poison prevScrollHeightRef, causing a huge spurious delta on the next visible render. if (container.clientHeight === 0) return; const prevHeight = prevScrollHeightRef.current; const newHeight = container.scrollHeight; // Only correct when a true load-more prepend happened (flag set by the window-growth // layoutEffect or button handler). Bottom appends slide the slice(-N) window which also // changes displayedMessages[0] — ID-comparison can't distinguish the two cases. const wasPrepend = pendingPrependRef.current; // Only consume the flag when scrollHeight actually changed — the server-side load-more // path goes through two renders: Render A (filteredMessages grows, visibleCount unchanged, // same displayedMessages content, same scrollHeight) then Render B (visibleCount grows, // new messages enter displayedMessages, scrollHeight grows). The flag must survive Render A // so it's still set when Render B fires the actual correction. if (wasPrepend && newHeight !== prevHeight) { pendingPrependRef.current = false; } if ( isScrolledUpRef.current && prevHeight > 0 && newHeight > prevHeight && wasPrepend ) { const delta = newHeight - prevHeight; container.scrollTop += delta; resizeObserverPrevSHRef.current = newHeight; } prevScrollHeightRef.current = newHeight; }, [displayedMessages]); // Re-stick to bottom when inner message content grows (media/audio previews loading). // Uses prevScrollHeight instead of wasAtBottomRef to avoid stale-flag race where the // ref is true while the user is actively scrolling up. // When the container width changes (member list toggle, window resize), text reflows // and scrollHeight changes; preserve proportional scroll position for scrolled-up users. // biome-ignore lint/correctness/useExhaustiveDependencies: scrollToBottom is a stable ref useEffect(() => { const container = messagesContainerRef.current; const inner = messagesInnerRef.current; if (!inner || !container) return; resizeObserverPrevSHRef.current = container.scrollHeight; let prevClientWidth = container.clientWidth; const observer = new ResizeObserver(() => { if (container.clientHeight === 0) return; // Effect may re-initialize while container is display:none (ref=0). // Re-seed with current dimensions and skip — no reliable "was at bottom" data. if (resizeObserverPrevSHRef.current === 0) { resizeObserverPrevSHRef.current = container.scrollHeight; prevClientWidth = container.clientWidth; return; } const currentClientWidth = container.clientWidth; const widthChanged = currentClientWidth !== prevClientWidth; prevClientWidth = currentClientWidth; const prevSH = resizeObserverPrevSHRef.current; const wasAtPrevBottom = container.scrollTop + container.clientHeight >= prevSH - SCROLL_TOLERANCE; resizeObserverPrevSHRef.current = container.scrollHeight; if (wasAtPrevBottom) { scrollToBottom(); } else if (widthChanged && prevSH > 0) { const ratio = container.scrollTop / prevSH; container.scrollTop = Math.round(ratio * container.scrollHeight); } }); observer.observe(inner); return () => observer.disconnect(); }, [isLoadingHistory, channelId, privateChatId]); // Auto-scroll on new messages — skip when this channel is hidden (display:none). // biome-ignore lint/correctness/useExhaustiveDependencies: only scroll when messages change, not when isActive changes useEffect(() => { if (!isActive) return; const isNarrowView = window.matchMedia("(max-width: 768px)").matches; const isChatVisible = !isNarrowView || mobileViewActiveColumn === "chatView"; if (wasAtBottomRef.current && isChatVisible) { scrollToBottom(); } }, [displayedMessages, mobileViewActiveColumn, scrollToBottom, isActive]); return ( <> {isLoadingHistory && !isFetchingMore ? (
) : (
{hasMoreMessages && !searchQuery && (
)} {searchQuery && (
{plural(filteredMessages.length, { one: t`Found 1 message matching "${searchQuery}"`, other: t`Found ${filteredMessages.length} messages matching "${searchQuery}"`, })}
)} {eventGroups.map((group) => { if (group.type === "eventGroup") { const firstId = group.messages[0]?.id || ""; const lastId = group.messages[group.messages.length - 1]?.id || ""; const groupKey = `group-${firstId}-${lastId}`; return (
); } const message = group.messages[0]; const originalIndex = channelMessages.findIndex( (m) => m.id === message.id, ); const previousMessage = channelMessages[originalIndex - 1]; const showHeader = !previousMessage || previousMessage.type !== "message" || previousMessage.userId !== message.userId || new Date(message.timestamp).getTime() - new Date(previousMessage.timestamp).getTime() > 5 * 60 * 1000; return (
); })}
)}
> ); }, ); ChannelMessageList.displayName = "ChannelMessageList"; // Wrap with memo so hidden keep-alive channels skip re-renders when their props // haven't changed (e.g. when messageText changes in the input — the only thing // that changes on typing is local state inside ChatArea, not the props we pass here). export const MemoChannelMessageList = memo(ChannelMessageList);
@@ -46,6 +56,12 @@ msgstr "{0} 和 {1} 正在输入..."
msgid "{0} is typing..."
msgstr "{0} 正在输入..."
+#. placeholder {0}: networks.length
+#. placeholder {1}: networks.length === 1 ? "" : "s"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "{0} network{1} — pick one to join"
+msgstr "{0} 个网络{1} — 选择一个加入"
+
#. placeholder {0}: filteredMessages.length - displayedMessages.length
#: src/components/layout/ChannelMessageList.tsx
msgid "{0} older messages"
@@ -69,17 +85,17 @@ msgstr "{0}、{1}、{2} 以及另外 {3} 人正在输入..."
#. placeholder {0}: Math.floor(secs / 86400)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}d ago"
-msgstr "{0} 天前"
+msgstr "{0} 天å"
#. placeholder {0}: Math.floor(secs / 3600)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}h ago"
-msgstr "{0} 小时前"
+msgstr "{0} å°æ¶å"
#. placeholder {0}: Math.floor(secs / 60)
#: src/components/ui/InvitationsPanel.tsx
msgid "{0}m ago"
-msgstr "{0} 分钟前"
+msgstr "{0} åéå"
#: src/lib/eventGrouping.ts
msgid "{c, plural, one {1 time} other {{c} times}}"
@@ -115,7 +131,7 @@ msgstr "*spam*"
#: src/components/ui/InvitationsPanel.tsx
msgid "#channel (optional)"
-msgstr "#频道(可选)"
+msgstr "#é¢éï¼å¯éï¼"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "#new-channel-name"
@@ -205,6 +221,12 @@ msgstr "添加邀请掩码(例如 nick!*@*, *!*@host.com)"
msgid "Add IRC Server"
msgstr "添加 IRC 服务器"
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add Network"
+msgstr "添加网络"
+
#: src/components/message/MessageActions.tsx
#: src/components/message/MessageReactions.tsx
#: src/components/message/MessageReactions.tsx
@@ -224,6 +246,10 @@ msgstr "添加规则"
msgid "Add Server"
msgstr "添加服务器"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Add your first network"
+msgstr "添加您的第一个网络"
+
#: src/components/message/JsonLogMessage.tsx
msgid "Additional Details"
msgstr "更多详情"
@@ -377,6 +403,10 @@ msgstr "返回"
msgid "Back to image"
msgstr "返回图片"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Back to network list"
+msgstr "返回网络列表"
+
#: src/components/ui/ModerationModal.tsx
msgid "Ban {username} by hostmask (prevents them from rejoining from the same IP/host)"
msgstr "通过 hostmask 封禁 {username}(阻止其从相同 IP/主机重新加入)"
@@ -424,6 +454,10 @@ msgstr "浏览服务器上的所有频道"
#: src/components/ui/AddPrivateChatModal.tsx
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ExternalLinkWarningModal.tsx
#: src/components/ui/FloodSettingsModal.tsx
@@ -443,6 +477,11 @@ msgstr "取消连接"
msgid "Cancel reply"
msgstr "取消回复"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/BouncerServerGroup.tsx
+msgid "Change accent color"
+msgstr "更改强调色"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Change the channel name (operators only)"
msgstr "更改频道名称(仅限管理员)"
@@ -564,7 +603,7 @@ msgstr "清除搜索"
#: src/components/ui/InvitationsPanel.tsx
msgid "Click again to confirm"
-msgstr "再次点击以确认"
+msgstr "忬¡ç¹å»ä»¥ç¡®è®¤"
#: src/components/message/JsonLogMessage.tsx
#: src/components/message/JsonLogMessage.tsx
@@ -606,6 +645,8 @@ msgstr "客户端限制 (+l)"
#: src/components/layout/ChannelList.tsx
#: src/components/message/ServerNoticesPopup.tsx
#: src/components/message/ServerNoticesPopup.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/MediaViewerModal.tsx
@@ -666,9 +707,10 @@ msgstr "配置通知声音和高亮提示"
#: src/components/ui/InvitationsPanel.tsx
msgid "Confirm?"
-msgstr "确认?"
+msgstr "确认ï¼"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Connect"
msgstr "连接"
@@ -711,11 +753,11 @@ msgstr "已复制"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy"
-msgstr "复制"
+msgstr "å¤å¶"
#: src/components/ui/RawLogViewer.tsx
msgid "Copy all"
-msgstr "全部复制"
+msgstr "å
¨é¨å¤å¶"
#: src/components/message/JsonLogMessage.tsx
msgid "Copy entire JSON"
@@ -731,7 +773,7 @@ msgstr "复制 JSON"
#: src/components/ui/InvitationsPanel.tsx
msgid "Copy link"
-msgstr "复制链接"
+msgstr "å¤å¶é¾æ¥"
#: src/components/ui/ExternalLinkWarningModal.tsx
msgid "Copy URL"
@@ -739,15 +781,15 @@ msgstr "复制链接"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create"
-msgstr "创建"
+msgstr "å建"
#: src/components/ui/InvitationsPanel.tsx
msgid "Create a new invite link"
-msgstr "创建新的邀请链接"
+msgstr "å建æ°çéè¯·é¾æ¥"
#: src/components/ui/UserSettings.tsx
msgid "Create and manage your invite links"
-msgstr "创建并管理您的邀请链接"
+msgstr "åå»ºå¹¶ç®¡çæ¨çéè¯·é¾æ¥"
#: src/components/ui/ChannelListModal.tsx
msgid "Created After (min ago)"
@@ -814,27 +856,52 @@ msgstr "删除频道"
msgid "Delete message"
msgstr "删除消息"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete network"
+msgstr "删除网络"
+
#: src/components/layout/ChannelList.tsx
msgid "Delete Private Chat"
msgstr "删除私聊"
#: src/components/ui/InvitationsPanel.tsx
msgid "Delete this invite"
-msgstr "删除此邀请"
+msgstr "å 餿¤é请"
#: src/components/ui/MediaCommentsSidebar.tsx
msgid "Delete this message? This cannot be undone."
msgstr "删除此消息?此操作无法撤销。"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Delete this network?"
+msgstr "删除此网络?"
+
#: src/components/ui/InvitationsPanel.tsx
msgid "Description (optional, e.g. \"Beta testers Q3\")"
-msgstr "描述(可选,例如 \"第三季度 Beta 测试者\")"
+msgstr "æè¿°ï¼å¯éï¼ä¾å¦ \"第ä¸å£åº¦ Beta æµè¯è
\"ï¼"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+#: src/components/ui/BouncerNetworksPanel.tsx
msgid "Disconnect"
msgstr "断开连接"
+#. placeholder {0}: network.name
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect <0>{0}0>?"
+msgstr "断开 <0>{0}0> 的连接?"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "Disconnect from soju bouncer?"
+msgstr "断开与 soju bouncer 的连接?"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "Disconnect network?"
+msgstr "断开网络连接?"
+
#: src/components/layout/ChannelList.tsx
msgid "Discover"
msgstr "发现"
@@ -891,20 +958,31 @@ msgstr "下载"
#: src/components/layout/ChatArea.tsx
msgid "Drop files to upload"
-msgstr "拖放文件以上传"
+msgstr "ææ¾æä»¶ä»¥ä¸ä¼ "
+
+#: src/components/ui/AddServerModal.tsx
+msgid "e.g. <0>wss://host:port/socket0>"
+msgstr "例如 <0>wss://host:port/socket0>"
#: src/components/ui/ChannelSettingsModal.tsx
msgid "e.g., 100:1440"
msgstr "例如:100:1440"
+#: src/components/ui/BouncerNetworksPanel.tsx
#: src/components/ui/ChannelSettingsModal.tsx
msgid "Edit"
msgstr "编辑"
+#. placeholder {0}: editingNetwork?.attributes.name || editingNetwork?.netid
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Edit {0}"
+msgstr "编辑 {0}"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Edit Profile"
msgstr "编辑资料"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
#: src/components/mobile/ServerBottomSheet.tsx
msgid "Edit Server"
@@ -1124,6 +1202,7 @@ msgstr "主页"
msgid "Homepage"
msgstr "主页"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
msgid "Host"
msgstr "主机"
@@ -1285,7 +1364,7 @@ msgstr "已加入频道"
#: src/components/ui/InvitationsPanel.tsx
msgid "just now"
-msgstr "刚刚"
+msgstr "åå"
#: src/components/ui/ModerationModal.tsx
#: src/components/ui/UserContextMenu.tsx
@@ -1323,7 +1402,7 @@ msgstr "离开频道"
#: src/components/ui/InvitationsPanel.tsx
msgid "Leave channel blank for a generic network invite. Description is just for your records — visible only to you in this list."
-msgstr "频道留空可用于通用网络邀请。描述仅用于您自己的记录——只有您能在此列表中看到。"
+msgstr "é¢éç空å¯ç¨äºéç¨ç½ç»é请ãæè¿°ä»
ç¨äºæ¨èªå·±çè®°å½ââåªææ¨è½å¨æ¤å表ä¸çå°ã"
#: src/lib/eventGrouping.ts
msgid "left"
@@ -1347,6 +1426,10 @@ msgstr "已离开频道"
msgid "Let others know when you are typing"
msgstr "让其他人知道您正在输入"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Libera Chat"
+msgstr "Libera Chat"
+
#: src/components/message/LinkPreview.tsx
msgid "Link preview"
msgstr "链接预览"
@@ -1375,6 +1458,10 @@ msgstr "正在加载 GIF..."
msgid "Loading more channels..."
msgstr "正在加载更多频道..."
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Loading networks from your bouncer…"
+msgstr "正在从您的 bouncer 加载网络…"
+
#: src/components/ui/UserProfileModal.tsx
msgid "Loading WHOIS data..."
msgstr "正在加载 WHOIS 数据..."
@@ -1563,12 +1650,23 @@ msgstr "名称:"
#: src/components/ui/InvitationsPanel.tsx
msgid "network"
-msgstr "网络"
+msgstr "ç½ç»"
+
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "Network bound through soju bouncer"
+msgstr "通过 soju bouncer 绑定的网络"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Network Name"
msgstr "网络名称"
+#. placeholder {0}: server?.name ?? bouncerServerId
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Networks on {0}"
+msgstr "{0} 上的网络"
+
#: src/components/ui/QuickActions.tsx
msgid "New DM"
msgstr "新私信"
@@ -1591,6 +1689,7 @@ msgid "nick!user@host (e.g., spam*!*@*, *!*@badhost.com)"
msgstr "nick!user@host(例如:spam*!*@*, *!*@badhost.com)"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
msgid "Nickname"
@@ -1650,6 +1749,10 @@ msgstr "未选择文件"
msgid "No flood profile"
msgstr "无洪水防护配置"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "no host set"
+msgstr "未设置主机"
+
#: src/components/ui/ChannelSettingsModal.tsx
msgid "No invitations found"
msgstr "未找到邀请"
@@ -1672,7 +1775,7 @@ msgstr "未加载任何媒体预览。"
#: src/components/ui/RawLogViewer.tsx
msgid "No raw IRC traffic captured yet. Try connecting or sending a message."
-msgstr "尚未捕获任何原始 IRC 流量。请尝试连接或发送一条消息。"
+msgstr "å°æªæè·ä»»ä½åå§ IRC æµéã请å°è¯è¿æ¥æåé䏿¡æ¶æ¯ã"
#: src/components/ui/QuickActions.tsx
msgid "No results found"
@@ -1680,7 +1783,7 @@ msgstr "未找到结果"
#: src/components/ui/InvitationsPanel.tsx
msgid "No server is selected. Pick a server from the sidebar first; invite links are managed per-server."
-msgstr "未选择服务器。请先从侧边栏选择一个服务器;邀请链接按服务器分别管理。"
+msgstr "æªéæ©æå¡å¨ã请å
ä»ä¾§è¾¹æ éæ©ä¸ä¸ªæå¡å¨ï¼éè¯·é¾æ¥ææå¡å¨åå«ç®¡çã"
#: src/components/ui/HomeScreen.tsx
msgid "No servers found."
@@ -1698,6 +1801,10 @@ msgstr "未设置主题"
msgid "No unread mentions or messages"
msgstr "没有未读提及或消息"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "No upstream networks yet."
+msgstr "尚无上游网络。"
+
#: src/components/ui/AddPrivateChatModal.tsx
msgid "No users available"
msgstr "没有可用用户"
@@ -1784,6 +1891,10 @@ msgstr "哎呀!网络分裂!⚠️"
msgid "Op"
msgstr "管理员"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Open"
+msgstr "打开"
+
#: src/components/ui/QuickActions/uiActionConfig.tsx
msgid "Open channel configuration settings"
msgstr "打开频道配置设置"
@@ -1887,6 +1998,10 @@ msgstr "置顶私聊"
msgid "Pin this private message conversation"
msgstr "置顶此私信对话"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Plaintext"
+msgstr "明文"
+
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
#: src/components/message/MediaPreview.tsx
@@ -1915,6 +2030,7 @@ msgid "PM User"
msgstr "私信用户"
#: src/components/ui/AddServerModal.tsx
+#: src/components/ui/BouncerNetworkForm.tsx
msgid "Port"
msgstr "端口"
@@ -2006,6 +2122,7 @@ msgstr "对此消息做出了回应"
msgid "Read more"
msgstr "阅读更多"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/UserProfileModal.tsx
#: src/components/ui/UserSettings.tsx
#: src/lib/settings/definitions/allSettings.ts
@@ -2024,6 +2141,7 @@ msgstr "原因"
msgid "Reason (optional)"
msgstr "原因(可选)"
+#: src/components/layout/BouncerServerGroup.tsx
#: src/components/layout/ServerList.tsx
msgid "Reconnect to server"
msgstr "重新连接服务器"
@@ -2031,7 +2149,7 @@ msgstr "重新连接服务器"
#: src/components/ui/InvitationsPanel.tsx
#: src/components/ui/InvitationsPanel.tsx
msgid "Refresh"
-msgstr "刷新"
+msgstr "å·æ°"
#: src/components/ui/AddServerModal.tsx
msgid "Register for an account"
@@ -2095,6 +2213,7 @@ msgstr "规则"
msgid "Safe"
msgstr "安全"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/ChannelSettingsModal.tsx
#: src/components/ui/TopicModal.tsx
#: src/components/ui/UserSettings.tsx
@@ -2187,6 +2306,10 @@ msgstr "跳转"
msgid "Select a channel"
msgstr "选择频道"
+#: src/components/layout/ChatHeader.tsx
+msgid "Select a Network"
+msgstr "选择网络"
+
#: src/components/ui/AutocompleteDropdown.tsx
msgid "Select Member"
msgstr "选择成员"
@@ -2276,6 +2399,10 @@ msgstr "网络上的服务器管理员可能读取您的消息"
msgid "Server Password"
msgstr "服务器密码"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Server Password (PASS)"
+msgstr "服务器密码 (PASS)"
+
#: src/components/ui/LinkSecurityWarningModal.tsx
msgid "Server-to-server communication may use unencrypted connections"
msgstr "服务器之间的通信可能使用未加密的连接"
@@ -2380,6 +2507,12 @@ msgstr "登录时间"
msgid "Software:"
msgstr "软件:"
+#: src/components/layout/BouncerServerGroup.tsx
+#: src/components/layout/ChannelList.tsx
+#: src/components/layout/ServerList.tsx
+msgid "soju bouncer (control)"
+msgstr "soju bouncer(控制)"
+
#: src/components/ui/ChannelListModal.tsx
msgid "Sort by Name"
msgstr "按名称排序"
@@ -2457,7 +2590,11 @@ msgstr "此图片已过期"
#: src/components/ui/InvitationsPanel.tsx
msgid "This many people registered through this link"
-msgstr "通过此链接注册的人数"
+msgstr "éè¿æ¤é¾æ¥æ³¨åç人æ°"
+
+#: src/components/ui/BouncerNetworkDisconnectConfirmModal.tsx
+msgid "This removes the network from your soju bouncer. To use it again, you'll need to add it back."
+msgstr "这会将网络从您的 soju bouncer 中移除。若要再次使用,您需要重新添加。"
#: src/components/ui/UserSettings.tsx
msgid "This server does not support extended profile metadata (IRCv3 METADATA extension). Additional fields like avatar, display name, and status are not available."
@@ -2465,12 +2602,21 @@ msgstr "此服务器不支持扩展个人资料元数据(IRCv3 METADATA 扩展
#: src/components/ui/InvitationsPanel.tsx
msgid "This server doesn't support invite links (the<0>obby.world/invitation0>capability isn't advertised). You can still chat normally; this panel is for obbyircd-powered networks."
-msgstr "此服务器不支持邀请链接(未声明 <0>obby.world/invitation0> 能力)。您仍可正常聊天;此面板适用于由 obbyircd 提供支持的网络。"
+msgstr "æ¤æå¡å¨ä¸æ¯æéè¯·é¾æ¥ï¼æªå£°æ <0>obby.world/invitation0> è½åï¼ãæ¨ä»å¯æ£å¸¸èå¤©ï¼æ¤é¢æ¿éç¨äºç± obbyircd æä¾æ¯æçç½ç»ã"
#: src/components/ui/AddServerModal.tsx
msgid "This server only supports one connection type"
msgstr "此服务器仅支持一种连接类型"
+#. placeholder {0}: children.length
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the {0} bound networks below."
+msgstr "这还将关闭下方 {0} 个绑定的网络。"
+
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "This will also close the bound network below."
+msgstr "这还将关闭下方绑定的网络。"
+
#: src/components/ui/FloodSettingsModal.tsx
msgid "Time (min)"
msgstr "时间(分钟)"
@@ -2479,6 +2625,10 @@ msgstr "时间(分钟)"
msgid "Time Window (seconds)"
msgstr "时间窗口(秒)"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "TLS"
+msgstr "TLS"
+
#: src/components/message/WhisperMessage.tsx
#: src/components/message/WhisperMessage.tsx
msgid "to"
@@ -2527,6 +2677,10 @@ msgstr "话题:"
msgid "Total: {0}"
msgstr "总计:{0}"
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Transport"
+msgstr "传输方式"
+
#: src/components/ui/UserSettings.tsx
msgid "Trusted Sources"
msgstr "受信任来源"
@@ -2615,7 +2769,7 @@ msgstr "通配符:* 匹配任意字符,? 匹配单个字符。示例:nick!
#: src/components/ui/InvitationsPanel.tsx
msgid "used"
-msgstr "已使用"
+msgstr "已使ç¨"
#: src/components/message/JsonLogMessage.tsx
#: src/components/ui/UserProfileModal.tsx
@@ -2641,6 +2795,7 @@ msgstr "用户资料"
msgid "User Settings"
msgstr "用户设置"
+#: src/components/ui/BouncerNetworkForm.tsx
#: src/components/ui/InviteUserModal.tsx
#: src/components/ui/ModerationModal.tsx
msgid "Username"
@@ -2788,6 +2943,10 @@ msgstr "宽泛 – 更广的保护范围"
msgid "Will default to 'no reason' if left empty"
msgstr "留空将默认显示\"无原因\""
+#: src/components/ui/BouncerNetworkForm.tsx
+msgid "Yes, delete"
+msgstr "是,删除"
+
#: src/components/message/CollapsedEventMessage.tsx
#: src/components/message/EventMessage.tsx
msgid "You"
@@ -2808,12 +2967,17 @@ msgstr "您有未保存的更改。确定要关闭而不保存吗?"
#: src/components/ui/InvitationsPanel.tsx
msgid "You haven't created any invite links yet. Use the form above to mint your first one."
-msgstr "您还没有创建任何邀请链接。使用上面的表单来创建您的第一个。"
+msgstr "æ¨è¿æ²¡æå建任ä½éè¯·é¾æ¥ã使ç¨ä¸é¢çè¡¨åæ¥å建æ¨ç第ä¸ä¸ªã"
#: src/store/handlers/users.ts
msgid "You invited {target} to join {channel}"
msgstr "您邀请了 {target} 加入 {channel}"
+#. placeholder {0}: parent.name
+#: src/components/ui/BouncerDisconnectConfirmModal.tsx
+msgid "You're connected to <0>{0}0>."
+msgstr "已连接到 <0>{0}0>。"
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your account password for authentication"
msgstr "您的账户认证密码"
@@ -2822,13 +2986,17 @@ msgstr "您的账户认证密码"
msgid "Your account username for authentication"
msgstr "您的账户认证用户名"
+#: src/components/ui/BouncerNetworksPanel.tsx
+msgid "Your bouncer doesn't have any networks yet. Add one to get started."
+msgstr "您的 bouncer 尚未添加任何网络。添加一个以开始使用。"
+
#: src/lib/settings/definitions/allSettings.ts
msgid "Your default nickname for all servers"
msgstr "所有服务器的默认昵称"
#: src/components/ui/InvitationsPanel.tsx
msgid "Your invite links"
-msgstr "您的邀请链接"
+msgstr "æ¨çéè¯·é¾æ¥"
#: src/components/ui/UserSettings.tsx
msgid "Your messages and settings are stored locally on your device"
diff --git a/src/store/handlers/auth.ts b/src/store/handlers/auth.ts
index 88628c14..cf20cef4 100644
--- a/src/store/handlers/auth.ts
+++ b/src/store/handlers/auth.ts
@@ -639,7 +639,7 @@ export function registerAuthHandlers(store: StoreApi): void {
}
if (!preventCapEnd) {
- ircClient.sendRaw(serverId, "CAP END");
+ ircClient.sendCapEnd(serverId);
ircClient.userOnConnect(serverId);
}
});
diff --git a/src/store/handlers/bouncer.ts b/src/store/handlers/bouncer.ts
new file mode 100644
index 00000000..c52e5b4c
--- /dev/null
+++ b/src/store/handlers/bouncer.ts
@@ -0,0 +1,196 @@
+import { v5 as uuidv5 } from "uuid";
+import type { StoreApi } from "zustand";
+import ircClient from "../../lib/ircClient";
+import type { BouncerState } from "../../types";
+import type { AppState } from "../index";
+
+// Mirrors CHANNEL_NAMESPACE in src/store/index.ts.
+const CHILD_NAMESPACE = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
+
+// Module-scope so a single childId only ever dispatches one bind across
+// the burst of BOUNCER_NETWORK events that arrive during the initial
+// LISTNETWORKS dump.
+const autoBindAttempted = new Set();
+
+function autoBindConnectedNetworks(
+ store: StoreApi,
+ bouncerServerId: string,
+) {
+ const state = store.getState();
+ // Skip events that fired against a bouncer CHILD (which itself
+ // negotiates the same cap); otherwise we'd recursively bind
+ // "grandchildren" off each child.
+ const sourceServer = state.servers.find((s) => s.id === bouncerServerId);
+ if (sourceServer?.bouncerNetid) return;
+ const bouncer = state.bouncers[bouncerServerId];
+ if (!bouncer) return;
+ for (const net of Object.values(bouncer.networks)) {
+ if (net.attributes.state !== "connected") continue;
+ const childId = uuidv5(`${bouncerServerId}:${net.netid}`, CHILD_NAMESPACE);
+ if (autoBindAttempted.has(childId)) continue;
+ if (state.servers.some((s) => s.id === childId)) {
+ autoBindAttempted.add(childId);
+ continue;
+ }
+ autoBindAttempted.add(childId);
+ void state.bouncerConnectNetwork(bouncerServerId, net.netid);
+ }
+}
+
+// Helper that lazily creates a BouncerState entry for a serverId.
+// We can't always know in advance which servers will turn out to be
+// bouncers, so we treat the first BOUNCER-* event from a serverId as
+// implicit setup.
+function ensureBouncer(
+ state: AppState,
+ serverId: string,
+ patch: Partial = {},
+): AppState["bouncers"] {
+ const existing = state.bouncers[serverId];
+ const base: BouncerState = existing ?? {
+ serverId,
+ supported: false,
+ notifyEnabled: false,
+ networks: {},
+ listed: false,
+ };
+ return { ...state.bouncers, [serverId]: { ...base, ...patch } };
+}
+
+export function registerBouncerHandlers(store: StoreApi): void {
+ // BOUNCER NETWORK . Either a snapshot (full attrs,
+ // e.g. inside a LISTNETWORKS batch or an initial -notify dump) or an
+ // incremental update (only changed attrs, in notify mode).
+ ircClient.on(
+ "BOUNCER_NETWORK",
+ ({ serverId, netid, deleted, attributes }) => {
+ store.setState((state) => {
+ const existing = state.bouncers[serverId];
+ const base: BouncerState = existing ?? {
+ serverId,
+ supported: false,
+ notifyEnabled: false,
+ networks: {},
+ listed: false,
+ };
+ if (deleted) {
+ const { [netid]: _, ...rest } = base.networks;
+ return {
+ bouncers: {
+ ...state.bouncers,
+ [serverId]: { ...base, networks: rest },
+ },
+ };
+ }
+ // Spec: in notify mode, an attr with an empty value is a deletion
+ // for that attr. Merge incoming on top of existing and strip those.
+ const prev = base.networks[netid]?.attributes ?? {};
+ const merged: Record = { ...prev };
+ for (const [k, v] of Object.entries(attributes)) {
+ if (v === "") delete merged[k];
+ else merged[k] = v;
+ }
+ return {
+ bouncers: {
+ ...state.bouncers,
+ [serverId]: {
+ ...base,
+ networks: {
+ ...base.networks,
+ [netid]: { netid, attributes: merged },
+ },
+ },
+ },
+ };
+ });
+ if (attributes.state === "connected" || (!deleted && attributes.state)) {
+ autoBindConnectedNetworks(store, serverId);
+ }
+ // Drop the bound child when soju reports state=disconnected (or
+ // the network deleted), and clear the auto-bind memory for it
+ // so a subsequent state=connected (e.g. after a CHANGENETWORK
+ // that forces an upstream reconnect) re-binds instead of
+ // skipping because the Set still thinks the bind was attempted.
+ const dropLocalChild =
+ deleted || (!deleted && attributes.state === "disconnected");
+ if (dropLocalChild) {
+ const childId = uuidv5(`${serverId}:${netid}`, CHILD_NAMESPACE);
+ autoBindAttempted.delete(childId);
+ const live = store.getState().servers.find((s) => s.id === childId);
+ if (live) store.getState().deleteServer(childId);
+ }
+ },
+ );
+
+ // ACKs from the server confirming our ADD / CHANGE / DEL took effect.
+ // The accompanying BOUNCER NETWORK update has already updated state;
+ // these events exist primarily so UI can dismiss "saving..." spinners
+ // and close modals. The store doesn't need to mutate anything here,
+ // but we expose the events to consumers via the IRCClient EventMap.
+
+ // Errors from any subcommand. Stash on the bouncer state so the UI
+ // can pick them up reactively (toast / inline form error).
+ ircClient.on(
+ "BOUNCER_FAIL",
+ ({ serverId, code, subcommand, attribute, netid, description }) => {
+ store.setState((state) => ({
+ bouncers: ensureBouncer(state, serverId, {
+ lastError: { code, subcommand, attribute, netid, description },
+ }),
+ }));
+ },
+ );
+
+ // CAP ACK plumbing: CAP_ACKNOWLEDGED fires once per acked cap with the
+ // cap name in `key`. Mark supported when bouncer-networks is acked, and
+ // notifyEnabled when the -notify variant is acked (the latter lets us
+ // skip an explicit LISTNETWORKS since the server pushes the initial
+ // dump unprompted).
+ ircClient.on("CAP_ACKNOWLEDGED", ({ serverId, key }) => {
+ const supported = key === "soju.im/bouncer-networks";
+ const notify = key === "soju.im/bouncer-networks-notify";
+ if (!supported && !notify) return;
+ store.setState((state) => ({
+ bouncers: ensureBouncer(state, serverId, {
+ supported: supported || state.bouncers[serverId]?.supported || false,
+ notifyEnabled:
+ notify || state.bouncers[serverId]?.notifyEnabled || false,
+ }),
+ servers:
+ supported && !state.servers.find((s) => s.id === serverId)?.bouncerNetid
+ ? state.servers.map((s) =>
+ s.id === serverId ? { ...s, isBouncerControl: true } : s,
+ )
+ : state.servers,
+ }));
+ });
+
+ // ISUPPORT BOUNCER_NETID tells us this connection is currently bound
+ // to a specific upstream network. Empty value (or missing) means it's
+ // a control connection.
+ ircClient.on("ISUPPORT", ({ serverId, key, value }) => {
+ if (key !== "BOUNCER_NETID") return;
+ store.setState((state) => ({
+ bouncers: ensureBouncer(state, serverId, {
+ boundNetid: value || undefined,
+ }),
+ }));
+ });
+
+ // BATCH_END for a soju.im/bouncer-networks batch finalises the
+ // "listed" flag so the UI can swap from a skeleton to the list. We
+ // listen on BATCH_START to know the type and stash it; on BATCH_END
+ // we look it up.
+ const batchTypes = new Map(); // batchId -> type
+ ircClient.on("BATCH_START", ({ batchId, type }) => {
+ if (type === "soju.im/bouncer-networks") batchTypes.set(batchId, type);
+ });
+ ircClient.on("BATCH_END", ({ serverId, batchId }) => {
+ if (batchTypes.get(batchId) !== "soju.im/bouncer-networks") return;
+ batchTypes.delete(batchId);
+ store.setState((state) => ({
+ bouncers: ensureBouncer(state, serverId, { listed: true }),
+ }));
+ autoBindConnectedNetworks(store, serverId);
+ });
+}
diff --git a/src/store/handlers/connection.ts b/src/store/handlers/connection.ts
index 87598ba7..68e41a6d 100644
--- a/src/store/handlers/connection.ts
+++ b/src/store/handlers/connection.ts
@@ -42,6 +42,15 @@ export function registerConnectionHandlers(store: StoreApi): void {
store.getState().appendRawLogLine({ serverId, direction, line });
});
+ ircClient.on("ISUPPORT", ({ serverId, key, value }) => {
+ if (key !== "NETWORK" || !value) return;
+ store.setState((state) => ({
+ servers: state.servers.map((s) =>
+ s.id === serverId ? { ...s, networkName: value } : s,
+ ),
+ }));
+ });
+
ircClient.on("connectionStateChange", ({ serverId, connectionState }) => {
// Allow the ready handler to re-run metadata restoration after reconnect
if (connectionState === "disconnected") {
@@ -271,6 +280,14 @@ export function registerConnectionHandlers(store: StoreApi): void {
}
}
+ // Skip client-side rejoin for bouncer sessions; soju replays after BIND.
+ const reconnectingServer = store
+ .getState()
+ .servers.find((s) => s.id === serverId);
+ const isBouncerSession =
+ !!reconnectingServer?.isBouncerControl ||
+ !!reconnectingServer?.bouncerNetid;
+
// Get the saved channel order for this server
const savedChannelOrder = store.getState().channelOrder[serverId];
@@ -284,20 +301,23 @@ export function registerConnectionHandlers(store: StoreApi): void {
channelsToJoin = savedServer.channels;
}
- for (const channelName of channelsToJoin) {
- if (channelName) {
- store.getState().joinChannel(serverId, channelName);
+ if (!isBouncerSession) {
+ for (const channelName of channelsToJoin) {
+ if (channelName) {
+ store.getState().joinChannel(serverId, channelName);
+ }
}
}
- // chathistoryRequested is reset to false on disconnect — re-fetch missed history
- // for channels that were already joined (ircClient.joinChannel early-returns for them,
- // so CHATHISTORY never gets sent through the normal join path)
+ // Re-fetch CHATHISTORY for already-joined channels: ircClient.joinChannel
+ // early-returns for them, so the normal join path doesn't send it.
+ // soju bouncer control sessions have no real channels — skip.
setTimeout(() => {
const reconnectedServer = store
.getState()
.servers.find((s) => s.id === serverId);
if (!reconnectedServer) return;
+ if (reconnectedServer.isBouncerControl) return;
for (const ch of reconnectedServer.channels) {
if (!ch.chathistoryRequested) {
ircClient.sendRaw(serverId, `CHATHISTORY LATEST ${ch.name} * 50`);
diff --git a/src/store/handlers/index.ts b/src/store/handlers/index.ts
index 91b7e0a6..9ecae2e9 100644
--- a/src/store/handlers/index.ts
+++ b/src/store/handlers/index.ts
@@ -2,6 +2,7 @@ import type { StoreApi } from "zustand";
import type { AppState } from "../index";
import { registerAuthHandlers } from "./auth";
import { registerBatchHandlers } from "./batches";
+import { registerBouncerHandlers } from "./bouncer";
import { registerChannelHandlers } from "./channels";
import { registerConnectionHandlers } from "./connection";
import { registerInvitelinkHandlers } from "./invitelink";
@@ -26,4 +27,5 @@ export function registerAllHandlers(store: StoreApi): void {
registerNamedModesHandlers(store);
registerReadMarkerHandlers(store);
registerTicTacToeHandlers(store);
+ registerBouncerHandlers(store);
}
diff --git a/src/store/handlers/users.ts b/src/store/handlers/users.ts
index c56e5783..d452b961 100644
--- a/src/store/handlers/users.ts
+++ b/src/store/handlers/users.ts
@@ -126,7 +126,9 @@ export function registerUserHandlers(store: StoreApi): void {
);
if (exists) return {};
+ // soju bouncer control session has no real chathistory.
const hasChathistory =
+ !server.isBouncerControl &&
!!server.capabilities?.includes("draft/chathistory");
newChannelHadCap = hasChathistory;
diff --git a/src/store/index.ts b/src/store/index.ts
index 8d6e4cb6..75225d7f 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,5 +1,6 @@
import { v4 as uuidv4, v5 as uuidv5 } from "uuid";
import { create } from "zustand";
+import { encodeBouncerAttrs } from "../lib/bouncerAttrs";
import ircClient from "../lib/ircClient";
import { makeLabel } from "../lib/labeledResponse";
import {
@@ -7,6 +8,7 @@ import {
registerAllProtocolHandlers,
} from "../protocol";
import type {
+ BouncerState,
InviteLink,
Message,
PrivateChat,
@@ -406,6 +408,16 @@ interface UIState {
isAddServerModalOpen: boolean | undefined;
isEditServerModalOpen: boolean;
editServerId: string | null;
+ // Bouncer-network edit hand-off: set by editBouncerNetwork(),
+ // consumed by BouncerNetworksPanel on mount to open its inline form
+ // on the named netid. Cleared once the panel reads it.
+ pendingBouncerEdit?: { bouncerServerId: string; netid: string } | null;
+ disconnectConfirmTarget?: string | null;
+ disconnectNetworkConfirmTarget?: {
+ bouncerServerId: string;
+ netid: string;
+ childServerId: string;
+ } | null;
isTwoFactorSettingsOpen: boolean;
twoFactorSettingsServerId: string | null;
isSettingsModalOpen: boolean;
@@ -561,6 +573,32 @@ export interface AppState {
metadataFetchInProgress: Record; // serverId -> is fetching own metadata
userMetadataRequested: Record>; // serverId -> Set of usernames we've requested metadata for
metadataChangeCounter: number; // Counter incremented on metadata changes for reactivity
+ // soju.im/bouncer-networks: per-bouncer (i.e. per control-connection)
+ // state, keyed by the serverId of the control connection. Set by the
+ // bouncer event handlers as CAP / BOUNCER lines arrive.
+ bouncers: Record;
+ // Public actions for managing bouncer networks. These wrap the
+ // raw IRCClient.bouncer* sends with attr encoding and state tracking
+ // so call sites (UI components and tests) stay decoupled from wire
+ // formatting.
+ bouncerListNetworks: (bouncerServerId: string) => void;
+ bouncerAddNetwork: (
+ bouncerServerId: string,
+ attrs: Record,
+ ) => void;
+ bouncerChangeNetwork: (
+ bouncerServerId: string,
+ netid: string,
+ attrs: Record,
+ ) => void;
+ bouncerDelNetwork: (bouncerServerId: string, netid: string) => void;
+ // Open a child IRC connection to the bouncer that is bound to the
+ // given upstream network via BOUNCER BIND before CAP END.
+ // Reuses the parent's credentials. Resolves with the new Server.
+ bouncerConnectNetwork: (
+ bouncerServerId: string,
+ netid: string,
+ ) => Promise;
// WHOIS data cache
whoisData: Record>; // serverId -> nickname -> whois data
/**
@@ -613,6 +651,8 @@ export interface AppState {
};
// Channel order persistence
channelOrder: ChannelOrderMap; // serverId -> ordered array of channel names
+ // Per-bouncer accent color (hex string, e.g. "#fcd34d")
+ bouncerGroupAccents: Record;
// Message deduplication tracking
processedMessageIds: Set; // Set of msgid values that have already been processed
// Auto-connect prevention
@@ -831,6 +871,17 @@ export interface AppState {
prefillDetails?: ConnectionDetails | null,
) => void;
toggleEditServerModal: (isOpen?: boolean, serverId?: string | null) => void;
+ // Edit a bouncer-bound child server through the BouncerNetworksPanel
+ // (parent's inline form) instead of the generic AddServerModal --
+ // most ServerConfig fields are unused / inherited from the parent
+ // for bouncer-bound rows, so the panel's narrower form is the right UX.
+ editBouncerNetwork: (childServerId: string) => void;
+ consumePendingBouncerEdit: () => void;
+ requestDeleteServer: (serverId: string) => void;
+ cancelDeleteServer: () => void;
+ cancelDisconnectNetwork: () => void;
+ confirmDisconnectNetwork: () => void;
+ setBouncerGroupAccent: (parentServerId: string, hex: string | null) => void;
toggleSettingsModal: (isOpen?: boolean) => void;
toggleQuickActions: (isOpen?: boolean) => void;
requestChatInputFocus: () => void;
@@ -1012,6 +1063,7 @@ const useStore = create((set, get) => ({
metadataFetchInProgress: {},
userMetadataRequested: {},
metadataChangeCounter: 0,
+ bouncers: {},
whoisData: {},
inviteLinks: {},
pendingRegistration: null,
@@ -1021,6 +1073,7 @@ const useStore = create((set, get) => ({
pendingTwofaChallenge: null,
tictactoe: { games: {}, open: null },
channelOrder: loadChannelOrder(),
+ bouncerGroupAccents: storage.bouncerGroupAccents.load(),
processedMessageIds: new Set(),
hasConnectedToSavedServers: false,
selectedServerId: null,
@@ -1038,6 +1091,9 @@ const useStore = create((set, get) => ({
isTwoFactorSettingsOpen: false,
twoFactorSettingsServerId: null,
editServerId: null,
+ pendingBouncerEdit: null,
+ disconnectConfirmTarget: null,
+ disconnectNetworkConfirmTarget: null,
isSettingsModalOpen: false,
isQuickActionsOpen: false,
isDarkMode: true,
@@ -1459,18 +1515,23 @@ const useStore = create((set, get) => ({
return server;
});
- // Update localStorage with the new channel
- const savedServers = loadSavedServers();
+ // Skip for bouncer sessions: savedServer lookup is host:port-keyed
+ // and a soju bouncer's parent + every bound child share that.
const currentServer = state.servers.find((s) => s.id === serverId);
- const savedServer = savedServers.find(
- (s) =>
- normalizeHost(s.host) ===
- normalizeHost(currentServer?.host || "") &&
- s.port === currentServer?.port,
- );
- if (savedServer && !savedServer.channels.includes(channel.name)) {
- savedServer.channels.push(channel.name);
- saveServersToLocalStorage(savedServers);
+ const isBouncerSession =
+ !!currentServer?.isBouncerControl || !!currentServer?.bouncerNetid;
+ if (!isBouncerSession) {
+ const savedServers = loadSavedServers();
+ const savedServer = savedServers.find(
+ (s) =>
+ normalizeHost(s.host) ===
+ normalizeHost(currentServer?.host || "") &&
+ s.port === currentServer?.port,
+ );
+ if (savedServer && !savedServer.channels.includes(channel.name)) {
+ savedServer.channels.push(channel.name);
+ saveServersToLocalStorage(savedServers);
+ }
}
// Update channelOrder state to include the new channel
@@ -1511,17 +1572,23 @@ const useStore = create((set, get) => ({
return server;
});
- // Update localStorage to remove the channel
- const savedServers = loadSavedServers();
+ // Skip for bouncer sessions (see joinChannel).
const currentServer = updatedServers.find((s) => s.id === serverId);
- const savedServer = savedServers.find(
- (s) =>
- normalizeHost(s.host) === normalizeHost(currentServer?.host || "") &&
- s.port === currentServer?.port,
- );
- if (savedServer) {
- savedServer.channels = currentServer?.channels.map((c) => c.name) || [];
- saveServersToLocalStorage(savedServers);
+ const isBouncerSession =
+ !!currentServer?.isBouncerControl || !!currentServer?.bouncerNetid;
+ if (!isBouncerSession) {
+ const savedServers = loadSavedServers();
+ const savedServer = savedServers.find(
+ (s) =>
+ normalizeHost(s.host) ===
+ normalizeHost(currentServer?.host || "") &&
+ s.port === currentServer?.port,
+ );
+ if (savedServer) {
+ savedServer.channels =
+ currentServer?.channels.map((c) => c.name) || [];
+ saveServersToLocalStorage(savedServers);
+ }
}
// Update channelOrder to remove the channel
@@ -2320,44 +2387,36 @@ const useStore = create((set, get) => ({
reorderChannels: (serverId, channelIds) => {
set((state) => {
- // Also update the savedServer.channels array to match the new order
const server = state.servers.find((s) => s.id === serverId);
- if (server) {
+ if (!server) return {};
+
+ const isBouncerSession =
+ !!server.isBouncerControl || !!server.bouncerNetid;
+
+ const channelNames = channelIds
+ .map((id) => server.channels.find((c) => c.id === id)?.name)
+ .filter((name): name is string => name !== undefined);
+
+ const newChannelOrder = {
+ ...state.channelOrder,
+ [serverId]: channelNames,
+ };
+ saveChannelOrder(newChannelOrder);
+
+ if (!isBouncerSession) {
const savedServers = loadSavedServers();
const savedServer = savedServers.find(
(s) =>
normalizeHost(s.host) === normalizeHost(server.host) &&
s.port === server.port,
);
-
if (savedServer) {
- // Convert channel IDs to channel names in the correct order
- const channelNames = channelIds
- .map((id) => {
- const channel = server.channels.find((c) => c.id === id);
- return channel?.name;
- })
- .filter((name): name is string => name !== undefined);
-
savedServer.channels = channelNames;
saveServersToLocalStorage(savedServers);
-
- // Store channel names in channelOrder state (not IDs)
- const newChannelOrder = {
- ...state.channelOrder,
- [serverId]: channelNames,
- };
-
- saveChannelOrder(newChannelOrder);
-
- return {
- channelOrder: newChannelOrder,
- };
}
}
- // Fallback if server not found
- return {};
+ return { channelOrder: newChannelOrder };
});
},
@@ -2861,9 +2920,19 @@ const useStore = create((set, get) => ({
runPendingMigrations();
const savedServers = loadSavedServers();
- const connectionPromises = [];
- for (const savedServer of savedServers) {
+ // Reconnect only parent / standalone servers here. Bouncer children
+ // are no longer dispatched from saved state -- the bouncer's own
+ // `state=connected` set is the source of truth, and the bouncer
+ // reducer auto-binds each one after the parent's LISTNETWORKS lands
+ // (see store/handlers/bouncer.ts `autoBindConnectedNetworks`).
+ // This removes a race where the saved children's SASL/BIND would
+ // hit soju before the parent's auth completed and surface as an
+ // "Authentication required" loop (#120 followup).
+ const parents = savedServers.filter((s) => !s.bouncerNetid);
+
+ const connectionPromises: Promise[] = [];
+ for (const savedServer of parents) {
const {
id,
name,
@@ -2871,33 +2940,32 @@ const useStore = create((set, get) => ({
port,
nickname,
password,
- channels,
saslEnabled,
saslAccountName,
saslPassword,
} = savedServer;
- // Ensure host is in URL format (handles old hostname-only entries)
const urlHost = ensureUrlFormat(host, port);
- // Check if server already exists in store using normalized comparison
const existingServer = get().servers.find(
(s) =>
normalizeHost(s.host) === normalizeHost(urlHost) && s.port === port,
);
if (!existingServer) {
- // Add server to store with connecting state
const connectingServer: Server = {
id,
name: name || normalizeHost(urlHost),
- host: normalizeHost(urlHost), // Store normalized hostname in state
+ host: normalizeHost(urlHost),
port,
channels: [],
privateChats: [],
isConnected: false,
connectionState: "connecting",
users: [],
+ bouncerServerId: savedServer.bouncerServerId,
+ bouncerNetid: savedServer.bouncerNetid,
+ isBouncerControl: savedServer.isBouncerControl,
};
set((state) => ({
@@ -2905,38 +2973,35 @@ const useStore = create((set, get) => ({
}));
}
- const connectionPromise = get()
- .connect(
- name || normalizeHost(urlHost),
- urlHost, // Use full URL
- port,
- nickname,
- saslEnabled,
- password,
- saslAccountName,
- saslPassword,
- )
- .catch((error) => {
- console.error(`Failed to reconnect to server ${urlHost}`, error);
- // Update server state to disconnected using normalized comparison
- set((state) => ({
- servers: state.servers.map((s) =>
- normalizeHost(s.host) === normalizeHost(urlHost) &&
- s.port === port
- ? { ...s, connectionState: "disconnected" as const }
- : s,
- ),
- }));
- });
+ let connectionPromise: Promise = get().connect(
+ name || normalizeHost(urlHost),
+ urlHost,
+ port,
+ nickname,
+ saslEnabled,
+ password,
+ saslAccountName,
+ saslPassword,
+ );
+ connectionPromise = connectionPromise.catch((error) => {
+ console.error(`Failed to reconnect to server ${urlHost}`, error);
+ set((state) => ({
+ servers: state.servers.map((s) =>
+ normalizeHost(s.host) === normalizeHost(urlHost) && s.port === port
+ ? { ...s, connectionState: "disconnected" as const }
+ : s,
+ ),
+ }));
+ });
connectionPromises.push(connectionPromise);
}
- // Wait for all connections to complete
+ // Wait for the parent burst to settle. Bouncer children come back
+ // asynchronously via the bouncer reducer's autoBindConnectedNetworks
+ // hook once the parent has posted LISTNETWORKS -- they don't block
+ // initial app boot.
await Promise.all(connectionPromises);
-
- // Note: UI selection is now loaded immediately from localStorage in initial state,
- // so no need for delayed restoration here
},
reconnectServer: async (serverId: string) => {
@@ -3001,34 +3066,40 @@ const useStore = create((set, get) => ({
},
deleteServer: (serverId) => {
- clearServerConnectionTimeout(serverId);
- ircClient.removeServer(serverId);
+ // Cascade: when target is a bouncer control session, also drop every
+ // bound child. Each child shares the parent's socket-side identity
+ // with soju and has no useful life beyond the control session.
+ const preState = get();
+ const target = preState.servers.find((s) => s.id === serverId);
+ const cascadeIds: string[] = target?.isBouncerControl
+ ? preState.servers
+ .filter((s) => s.bouncerServerId === serverId)
+ .map((s) => s.id)
+ : [];
+ const toRemove = new Set([serverId, ...cascadeIds]);
+
+ for (const id of toRemove) {
+ clearServerConnectionTimeout(id);
+ ircClient.removeServer(id);
+ }
set((state) => {
- const serverToDelete = state.servers.find(
- (server) => server.id === serverId,
- );
-
const savedServers = loadSavedServers();
- const updatedServers = savedServers.filter(
- (s) =>
- normalizeHost(s.host) !== normalizeHost(serverToDelete?.host || "") ||
- s.port !== serverToDelete?.port,
- );
+ const updatedServers = savedServers.filter((s) => !toRemove.has(s.id));
saveServersToLocalStorage(updatedServers);
const savedMetadata = loadSavedMetadata();
- delete savedMetadata[serverId];
+ for (const id of toRemove) delete savedMetadata[id];
saveMetadataToLocalStorage(savedMetadata);
const remainingServers = state.servers.filter(
- (server) => server.id !== serverId,
+ (server) => !toRemove.has(server.id),
);
const newSelectedServerId =
remainingServers.length > 0 ? remainingServers[0].id : null;
const clearConnectionState =
- state.connectingServerId === serverId
+ state.connectingServerId && toRemove.has(state.connectingServerId)
? { isConnecting: false, connectingServerId: null }
: {};
@@ -3041,6 +3112,7 @@ const useStore = create((set, get) => ({
selectedChannelId: newSelectedServerId
? remainingServers[0].channels[0]?.id || null
: null,
+ disconnectConfirmTarget: null,
},
};
});
@@ -3077,6 +3149,88 @@ const useStore = create((set, get) => ({
}));
},
+ editBouncerNetwork: (childServerId) => {
+ const state = get();
+ const child = state.servers.find((s) => s.id === childServerId);
+ if (!child?.bouncerServerId || !child.bouncerNetid) return;
+ const parent = state.servers.find((s) => s.id === child.bouncerServerId);
+ if (!parent) return;
+ set((s) => ({
+ ui: {
+ ...s.ui,
+ selectedServerId: parent.id,
+ pendingBouncerEdit: {
+ bouncerServerId: parent.id,
+ netid: child.bouncerNetid as string,
+ },
+ },
+ }));
+ },
+
+ consumePendingBouncerEdit: () => {
+ set((state) => ({
+ ui: { ...state.ui, pendingBouncerEdit: null },
+ }));
+ },
+
+ requestDeleteServer: (serverId) => {
+ const state = get();
+ const target = state.servers.find((s) => s.id === serverId);
+ if (target?.isBouncerControl) {
+ set((s) => ({
+ ui: { ...s.ui, disconnectConfirmTarget: serverId },
+ }));
+ return;
+ }
+ if (target?.bouncerServerId && target.bouncerNetid) {
+ set((s) => ({
+ ui: {
+ ...s.ui,
+ disconnectNetworkConfirmTarget: {
+ bouncerServerId: target.bouncerServerId as string,
+ netid: target.bouncerNetid as string,
+ childServerId: serverId,
+ },
+ },
+ }));
+ return;
+ }
+ get().deleteServer(serverId);
+ },
+
+ cancelDeleteServer: () => {
+ set((state) => ({
+ ui: { ...state.ui, disconnectConfirmTarget: null },
+ }));
+ },
+
+ cancelDisconnectNetwork: () => {
+ set((state) => ({
+ ui: { ...state.ui, disconnectNetworkConfirmTarget: null },
+ }));
+ },
+
+ confirmDisconnectNetwork: () => {
+ const state = get();
+ const target = state.ui.disconnectNetworkConfirmTarget;
+ if (!target) return;
+ state.bouncerDelNetwork(target.bouncerServerId, target.netid);
+ state.deleteServer(target.childServerId);
+ set((s) => ({
+ ui: { ...s.ui, disconnectNetworkConfirmTarget: null },
+ }));
+ },
+
+ setBouncerGroupAccent: (parentServerId, hex) => {
+ set((state) => {
+ const next = { ...state.bouncerGroupAccents };
+ if (hex) next[parentServerId] = hex;
+ else delete next[parentServerId];
+ storage.bouncerGroupAccents.save(next);
+ return { bouncerGroupAccents: next };
+ });
+ },
+
tictactoeInvite: (serverId, opponent) =>
tictactoeActions.invite(set, get, serverId, opponent),
tictactoeAccept: (serverId, opponent) =>
@@ -3865,6 +4019,105 @@ const useStore = create((set, get) => ({
}
},
+ bouncerListNetworks: (bouncerServerId) => {
+ ircClient.bouncerListNetworks(bouncerServerId);
+ },
+ bouncerAddNetwork: (bouncerServerId, attrs) => {
+ ircClient.bouncerAddNetwork(bouncerServerId, encodeBouncerAttrs(attrs));
+ },
+ bouncerChangeNetwork: (bouncerServerId, netid, attrs) => {
+ ircClient.bouncerChangeNetwork(
+ bouncerServerId,
+ netid,
+ encodeBouncerAttrs(attrs),
+ );
+ },
+ bouncerDelNetwork: (bouncerServerId, netid) => {
+ ircClient.bouncerDelNetwork(bouncerServerId, netid);
+ },
+
+ bouncerConnectNetwork: async (bouncerServerId, netid) => {
+ const state = get();
+ const parent = state.servers.find((s) => s.id === bouncerServerId);
+ if (!parent) return undefined;
+ // Idempotent: if we already have a child server for this binding,
+ // do nothing -- whatever its current connectionState is, a fresh
+ // connect would re-issue BIND on top of the existing socket. The
+ // user can use the per-server reconnect affordance to retry.
+ const childId = uuidv5(`${bouncerServerId}:${netid}`, CHANNEL_NAMESPACE);
+ if (state.servers.some((s) => s.id === childId)) return undefined;
+ // Reuse the parent's persisted credentials. Bouncers authenticate
+ // the user once on the control connection and then accept child
+ // connections from the same client with the same auth.
+ const savedParent = storage.servers
+ .load()
+ .find((s) => s.id === bouncerServerId);
+ if (!savedParent) return undefined;
+
+ // Network name from the bouncer's BOUNCER NETWORK announcement (if
+ // any) -- a friendly label for the new Server.
+ const bouncer = state.bouncers[bouncerServerId];
+ const network = bouncer?.networks[netid];
+ const friendly = network?.attributes.name || `${parent.name}/${netid}`;
+
+ // Persist the child config so a subsequent connectToSavedServers
+ // can restore the same child. ServerConfig carries the
+ // bouncerServerId + bouncerNetid linkage.
+ const savedServers = storage.servers.load();
+ if (!savedServers.find((s) => s.id === childId)) {
+ savedServers.push({
+ ...savedParent,
+ id: childId,
+ name: friendly,
+ bouncerServerId,
+ bouncerNetid: netid,
+ isBouncerControl: false,
+ addedAt: Date.now(),
+ });
+ storage.servers.save(savedServers);
+ }
+
+ // Seed an in-memory Server row in "connecting" state so the UI
+ // can show it immediately.
+ set((state) => {
+ if (state.servers.some((s) => s.id === childId)) return state;
+ const seed: Server = {
+ id: childId,
+ name: friendly,
+ host: parent.host,
+ port: parent.port,
+ channels: [],
+ privateChats: [],
+ users: [],
+ isConnected: false,
+ connectionState: "connecting",
+ bouncerServerId,
+ bouncerNetid: netid,
+ };
+ return { servers: [...state.servers, seed] };
+ });
+
+ // Queue the BIND so the next CAP END for this new connection
+ // emits `BOUNCER BIND ` first.
+ ircClient.setPendingBouncerBind(childId, netid);
+
+ // The in-memory Server.host is stripped to a bare hostname during
+ // the original parent connect, which loses the protocol + path for
+ // WSS URLs (e.g. wss://obby.t3ks.com:6662/socket -> obby.t3ks.com).
+ // Use the persisted savedParent.host instead so the child landing
+ // on the same listener actually opens /socket, not the bare root.
+ return ircClient.connect(
+ friendly,
+ savedParent.host,
+ savedParent.port,
+ savedParent.nickname,
+ savedParent.password,
+ savedParent.saslAccountName,
+ savedParent.saslPassword,
+ childId,
+ );
+ },
+
sendRaw: (serverId, command) => {
ircClient.sendRaw(serverId, command);
},
diff --git a/src/store/localStorage.ts b/src/store/localStorage.ts
index 3fe45807..ac632491 100644
--- a/src/store/localStorage.ts
+++ b/src/store/localStorage.ts
@@ -15,8 +15,24 @@ const KEYS = {
PINNED_PMS: "pinnedPrivateChats",
UI_SELECTION: "uiSelections",
MIGRATION_VERSION: "migrationVersion",
+ BOUNCER_GROUP_ACCENTS: "bouncerGroupAccents",
} as const;
+export const bouncerGroupAccents = {
+ load: (): Record => {
+ try {
+ return JSON.parse(
+ localStorage.getItem(KEYS.BOUNCER_GROUP_ACCENTS) || "{}",
+ );
+ } catch {
+ return {};
+ }
+ },
+ save: (map: Record